Compare commits

..

2 Commits

Author SHA1 Message Date
c4d450f12d Add rainbow-base counterexample search and notes 2026-03-26 13:01:56 +08:00
3c935a5d5d Add Matroid Rainbow Base Cover notes 2026-03-26 12:22:39 +08:00
3 changed files with 638 additions and 0 deletions

View File

@@ -0,0 +1,168 @@
---
title: Counterexample Search Notes
---
# Goal
Try to find a counterexample to the rainbow-base-cover conjecture:
> If a rank-`r` matroid on `2r` elements decomposes into two disjoint bases, and the elements are colored by `r` colors each used twice, then three rainbow bases cover the ground set.
I also tracked the stronger special case of the double-cover conjecture in the partition-matroid setting:
> For the rainbow bases, are there always four of them covering each element exactly twice?
# Search Design
## Encoding
Fix a pairing of the `2r` elements into color classes. A rainbow basis chooses exactly one element from each pair, so for fixed coloring there are at most `2^r` rainbow candidates.
For a matroid `M`, I tested:
1. whether `M` has two disjoint bases;
2. for each pairing, which of the `2^r` transversals are actual bases;
3. whether some 3 rainbow bases cover all `2r` elements;
4. whether some 4 rainbow bases cover every element exactly twice.
This was implemented in [search_rainbow_counterexample.py](/Users/congyu/rainbow_base_cover/search_rainbow_counterexample.py).
## Exhaustive part
Sage's `AllMatroids(2r, r)` is available up to `r = 4`, so I checked all unlabeled rank-`r` matroids on `2r` elements for `r = 1,2,3,4`, and all pairings of the ground set:
- `r = 1`: `1` pairing
- `r = 2`: `3` pairings
- `r = 3`: `15` pairings
- `r = 4`: `105` pairings
## Additional rank-5 probes
Since Sage does not exhaust all rank-5 matroids on 10 elements, I also sampled random linear matroids over small fields, and exhaustively checked some graphic families:
- random rank-5 linear matroids over `GF(2), GF(3), GF(4), GF(5)`;
- all simple graphic matroids coming from 8-edge graphs on 5 vertices.
# Results
## Exhaustive search for `r <= 4`
No counterexample appeared.
| rank `r` | matroids checked | qualifying matroids | max observed minimum cover | any 3-cover failure? | any 4-double-cover failure? |
|---|---:|---:|---:|---|---|
| 1 | 2 | 1 | 2 | no | no |
| 2 | 7 | 3 | 2 | no | no |
| 3 | 38 | 17 | 3 | no | no |
| 4 | 940 | 730 | 3 | no | no |
So:
- the rainbow-cover conjecture holds for every matroid in Sage's complete database with `2r <= 8`;
- in the same range, the stronger partition-matroid double-cover statement also holds.
## Explicit rank-4 witness requiring 3 bases
The first rank-4 example I found with minimum cover number exactly `3` is
- matroid: `all_n08_r04_#493`
- pairing: `((0,5),(1,4),(2,3),(6,7))`
For this pairing there are exactly `8` rainbow bases:
`(0,1,3,6)`, `(0,1,3,7)`, `(0,2,4,6)`, `(0,2,4,7)`, `(1,2,5,6)`, `(1,2,5,7)`, `(3,4,5,6)`, `(3,4,5,7)`.
This instance still has:
- minimum rainbow cover size `= 3`;
- a 4-rainbow exact double cover.
So the search really is reaching the sharp bound `3`, not just easy cases with cover number `2`.
## Graphic search
The known lower-bound example `K_4` is reproduced computationally:
- all simple graphs on 4 vertices with 6 edges: `1` graph checked;
- maximum minimum cover number: `3`;
- no 3-cover failure;
- no 4-double-cover failure.
I also checked all simple 8-edge graphs on 5 vertices:
- graphs checked: `45`;
- connected graphs: `45`;
- qualifying graphic matroids: `45`;
- maximum minimum cover number: `3`;
- no 3-cover failure;
- no 4-double-cover failure.
One concrete simple graphic rank-4 witness with minimum cover `3` is the graph with edge set
`((0,1),(0,2),(0,3),(0,4),(1,2),(1,3),(1,4),(2,3))`
and pairing
`((0,3),(1,5),(2,4),(6,7))`.
## Random rank-5 linear search
No sampled rank-5 linear matroid produced a counterexample.
Finished runs:
| field | samples | distinct matroids checked | qualifying matroids | max observed minimum cover | any 3-cover failure? | any 4-double-cover failure? |
|---|---:|---:|---:|---:|---|---|
| `GF(2)` | 200 | 200 | 92 | 3 | no | no |
| `GF(2)` | 500 | 500 | 245 | 3 | no | no |
| `GF(3)` | 200 | 200 | 180 | 3 | no | no |
| `GF(4)` | 200 | 200 | 99 | 3 | no | no |
| `GF(5)` | 200 | 200 | 200 | 2 | no | no |
# Observations
## Small-rank evidence is strong
Up through rank 4, the search is exhaustive, not heuristic. In that range I found no obstruction even to the stronger four-basis exact double-cover property.
## Rank 4 already has nontrivial tight examples
The bound `3` is still best possible in rank 4: there are pairings where 2 rainbow bases do not suffice, but 3 do.
## The partition-matroid special case looks robust
At least computationally, the partition-matroid case of the double-cover conjecture behaves better than expected:
- exhaustive success for all matroids on 8 elements of rank 4;
- no failures in the rank-5 linear samples over four small fields;
- no failures in the checked graphic families.
# What I would try next
## More targeted rank-5 search
The next most plausible places to look are:
1. exhaustive or semi-exhaustive graphic search on 10 edges and 6 vertices;
2. sparse paving matroids of rank 5 on 10 elements;
3. biased random linear constructions, especially sparse or highly structured matrices rather than dense uniform random ones;
4. direct search for the stronger four-basis exact double-cover failure, since that might break before the three-cover statement does.
## Structural reformulation
For a fixed pairing, the rainbow bases form a subset `F ⊆ {0,1}^r`. Then:
- 3-cover means there exist `x,y,z in F` such that in every coordinate, not all of `x_i,y_i,z_i` are equal;
- exact 4-double-cover means there exist `x_1,x_2,x_3,x_4 in F` such that every coordinate has exactly two `0`s and two `1`s.
This reformulation may be a better starting point for a structural attack than thinking directly in matroid language.
# Current Status
I did not find a counterexample.
The strongest completed evidence from this turn is:
- exhaustive verification for all rank-`r` matroids on `2r` elements with `r <= 4`;
- exhaustive verification for simple graphic rank-4 instances on 8 edges;
- no sampled failure among several hundred rank-5 linear matroids over `GF(2), GF(3), GF(4), GF(5)`.

View File

@@ -0,0 +1,26 @@
----
title: Matroid Rainbow Base Cover
----
Let $M=(E,\mathcal B)$ be a rank-$r$ matroid whose ground set decomposes into two disjoint bases. Furthermore,
assume that $E$ is colored by $r$ colors, each color appearing exactly twice. A basis of $M$ is called rainbow if
it does not contain two elements of the same color.
::: Problem
What is the minimum number of rainbow bases needed to cover $E$?
:::
Kristóf Bérczi conjectured the minimum number is 3.
Currently known bounds:
- upperbound: $\floor{\log_2 |E|}+1$ by matroid intersection;
- lowerbound: $3$ on graphic matroid of $K_4$.
::: {.Conjecture #conj-doublecover}
Let $M_1$ and $M_2$ be two matroid on the same ground set $E$. Assume that $E$ decomposes into two bases in both of them.
Then $M_1$ and $M_2$ has four common bases that cover each element exactly twice.
:::
Let $M_1$ be the partition matroid of colors and let $M_2$ be $M$.
If [@conj-doublecover] is true, then 3 common bases will be enough to cover $E$.

View File

@@ -0,0 +1,444 @@
from __future__ import annotations
import argparse
import itertools
import json
import random
from dataclasses import dataclass
from typing import Iterable, Iterator
from sage.all import GF, Graph, Matrix, matroids
from sage.matroids.constructor import Matroid
def all_pairings(elements: tuple[int, ...]) -> Iterator[tuple[tuple[int, int], ...]]:
if not elements:
yield ()
return
first = elements[0]
for i in range(1, len(elements)):
pair = (first, elements[i])
rest = elements[1:i] + elements[i + 1 :]
for tail in all_pairings(rest):
yield (pair,) + tail
def pairing_to_masks(pairing: tuple[tuple[int, int], ...]) -> tuple[int, ...]:
masks = []
for choices in itertools.product([0, 1], repeat=len(pairing)):
mask = 0
for bit, pair in zip(choices, pairing):
mask |= 1 << pair[bit]
masks.append(mask)
return tuple(masks)
def min_cover_size(rainbow_masks: tuple[int, ...], full_mask: int) -> int | None:
for k in range(1, len(rainbow_masks) + 1):
for combo in itertools.combinations(rainbow_masks, k):
union = 0
for mask in combo:
union |= mask
if union == full_mask:
return k
return None
def has_double_cover(rainbow_masks: tuple[int, ...], n: int) -> bool:
target = tuple(2 for _ in range(n))
for combo in itertools.combinations_with_replacement(rainbow_masks, 4):
counts = [0] * n
for mask in combo:
bitmask = mask
idx = 0
while bitmask:
if bitmask & 1:
counts[idx] += 1
bitmask >>= 1
idx += 1
if tuple(counts) == target:
return True
return False
def subset_to_mask(subset: Iterable[int]) -> int:
mask = 0
for e in subset:
mask |= 1 << int(e)
return mask
@dataclass(frozen=True)
class PairingResult:
pairing: tuple[tuple[int, int], ...]
rainbow_count: int
min_cover: int | None
has_double_cover: bool
@dataclass(frozen=True)
class MatroidResult:
name: str
rank: int
num_bases: int
complementary_basis_pairs: int
worst_cover: int | None
no_three_cover_pairings: tuple[PairingResult, ...]
no_double_cover_pairings: tuple[PairingResult, ...]
def analyze_base_mask_set(
*,
name: str,
n: int,
r: int,
base_masks: tuple[int, ...],
pairing_data,
) -> MatroidResult | None:
full_mask = (1 << n) - 1
base_mask_set = set(base_masks)
complementary_basis_pairs = sum(1 for mask in base_masks if (full_mask ^ mask) in base_mask_set) // 2
if complementary_basis_pairs == 0:
return None
no_three_cover = []
no_double_cover = []
worst_cover = 0
for pairing, transversal_masks in pairing_data:
rainbow_masks = tuple(mask for mask in transversal_masks if mask in base_mask_set)
if not rainbow_masks:
result = PairingResult(pairing, 0, None, False)
no_three_cover.append(result)
no_double_cover.append(result)
worst_cover = None
continue
cover = min_cover_size(rainbow_masks, full_mask)
double_cover = has_double_cover(rainbow_masks, n)
result = PairingResult(pairing, len(rainbow_masks), cover, double_cover)
if cover is None or cover > 3:
no_three_cover.append(result)
worst_cover = None if cover is None else max(worst_cover, cover)
else:
worst_cover = max(worst_cover, cover)
if not double_cover:
no_double_cover.append(result)
return MatroidResult(
name=name,
rank=r,
num_bases=len(base_masks),
complementary_basis_pairs=complementary_basis_pairs,
worst_cover=worst_cover,
no_three_cover_pairings=tuple(no_three_cover),
no_double_cover_pairings=tuple(no_double_cover),
)
def analyze_matroid(M, pairing_data) -> MatroidResult | None:
ground = tuple(sorted(int(e) for e in M.groundset()))
n = len(ground)
r = M.rank()
assert n == 2 * r
base_masks = tuple(sorted(subset_to_mask(B) for B in M.bases()))
return analyze_base_mask_set(
name=str(M),
n=n,
r=r,
base_masks=base_masks,
pairing_data=pairing_data,
)
def is_spanning_tree(mask: int, chosen_edges: tuple[tuple[int, int], ...], num_vertices: int) -> bool:
parent = list(range(num_vertices))
def find(x: int) -> int:
while parent[x] != x:
parent[x] = parent[parent[x]]
x = parent[x]
return x
edges_used = 0
for label, (u, v) in enumerate(chosen_edges):
if not (mask >> label) & 1:
continue
edges_used += 1
ru = find(u)
rv = find(v)
if ru == rv:
return False
parent[ru] = rv
if edges_used != num_vertices - 1:
return False
root = find(0)
return all(find(v) == root for v in range(num_vertices))
def exhaustive_search(ranks: list[int], limit_per_rank: int | None = None) -> dict:
summary = {"mode": "exhaustive", "ranks": []}
for r in ranks:
n = 2 * r
pairings = tuple(all_pairings(tuple(range(n))))
pairing_data = tuple((pairing, pairing_to_masks(pairing)) for pairing in pairings)
checked = 0
qualifying = 0
max_cover = 0
counterexamples = []
double_cover_failures = []
for idx, M in enumerate(matroids.AllMatroids(n, r)):
if limit_per_rank is not None and idx >= limit_per_rank:
break
checked += 1
result = analyze_matroid(M, pairing_data)
if result is None:
continue
qualifying += 1
if result.worst_cover is None:
max_cover = None
elif max_cover is not None:
max_cover = max(max_cover, result.worst_cover)
if result.no_three_cover_pairings:
counterexamples.append(
{
"matroid": result.name,
"pairings": [
{
"pairing": pr.pairing,
"rainbow_count": pr.rainbow_count,
"min_cover": pr.min_cover,
"has_double_cover": pr.has_double_cover,
}
for pr in result.no_three_cover_pairings
],
}
)
if result.no_double_cover_pairings:
double_cover_failures.append(
{
"matroid": result.name,
"pairings": [
{
"pairing": pr.pairing,
"rainbow_count": pr.rainbow_count,
"min_cover": pr.min_cover,
}
for pr in result.no_double_cover_pairings
],
}
)
summary["ranks"].append(
{
"rank": r,
"elements": n,
"pairings": len(pairings),
"matroids_checked": checked,
"qualifying_matroids": qualifying,
"max_min_cover": max_cover,
"three_cover_counterexamples": counterexamples,
"double_cover_failures": double_cover_failures,
}
)
return summary
def random_full_rank_matrix(field_size: int, rows: int, cols: int, rng: random.Random):
while True:
entries = [rng.randrange(field_size) for _ in range(rows * cols)]
A = Matrix(GF(field_size), rows, cols, entries)
if A.rank() == rows:
return A
def random_linear_search(rank: int, field_size: int, samples: int, seed: int) -> dict:
n = 2 * rank
rng = random.Random(seed)
pairings = tuple(all_pairings(tuple(range(n))))
pairing_data = tuple((pairing, pairing_to_masks(pairing)) for pairing in pairings)
seen = set()
qualifying = 0
max_cover = 0
counterexamples = []
double_cover_failures = []
for _ in range(samples):
A = random_full_rank_matrix(field_size, rank, n, rng)
M = Matroid(matrix=A)
key = tuple(sorted(tuple(sorted(int(e) for e in B)) for B in M.bases()))
if key in seen:
continue
seen.add(key)
result = analyze_matroid(M, pairing_data)
if result is None:
continue
qualifying += 1
if result.worst_cover is None:
max_cover = None
elif max_cover is not None:
max_cover = max(max_cover, result.worst_cover)
if result.no_three_cover_pairings:
counterexamples.append(
{
"matroid": result.name,
"matrix": str(A),
"pairings": [
{
"pairing": pr.pairing,
"rainbow_count": pr.rainbow_count,
"min_cover": pr.min_cover,
"has_double_cover": pr.has_double_cover,
}
for pr in result.no_three_cover_pairings
],
}
)
if result.no_double_cover_pairings:
double_cover_failures.append(
{
"matroid": result.name,
"matrix": str(A),
"pairings": [
{
"pairing": pr.pairing,
"rainbow_count": pr.rainbow_count,
"min_cover": pr.min_cover,
}
for pr in result.no_double_cover_pairings
],
}
)
return {
"mode": "random_linear",
"rank": rank,
"elements": n,
"field_size": field_size,
"samples_requested": samples,
"distinct_matroids_checked": len(seen),
"qualifying_matroids": qualifying,
"max_min_cover": max_cover,
"three_cover_counterexamples": counterexamples,
"double_cover_failures": double_cover_failures,
}
def simple_graphic_search(num_vertices: int, num_edges: int) -> dict:
complete_edges = tuple(itertools.combinations(range(num_vertices), 2))
pairings = tuple(all_pairings(tuple(range(num_edges))))
pairing_data = tuple((pairing, pairing_to_masks(pairing)) for pairing in pairings)
checked = 0
connected = 0
qualifying = 0
max_cover = 0
counterexamples = []
double_cover_failures = []
for chosen_edges in itertools.combinations(complete_edges, num_edges):
checked += 1
G = Graph()
G.add_vertices(range(num_vertices))
for label, (u, v) in enumerate(chosen_edges):
G.add_edge(u, v, label)
if not G.is_connected():
continue
connected += 1
base_masks = []
for labels in itertools.combinations(range(num_edges), num_vertices - 1):
mask = 0
for label in labels:
mask |= 1 << label
if is_spanning_tree(mask, chosen_edges, num_vertices):
base_masks.append(mask)
result = analyze_base_mask_set(
name=str(chosen_edges),
n=num_edges,
r=num_vertices - 1,
base_masks=tuple(sorted(base_masks)),
pairing_data=pairing_data,
)
if result is None:
continue
qualifying += 1
if result.worst_cover is None:
max_cover = None
elif max_cover is not None:
max_cover = max(max_cover, result.worst_cover)
if result.no_three_cover_pairings:
counterexamples.append(
{
"graph_edges": chosen_edges,
"matroid": result.name,
"pairings": [
{
"pairing": pr.pairing,
"rainbow_count": pr.rainbow_count,
"min_cover": pr.min_cover,
"has_double_cover": pr.has_double_cover,
}
for pr in result.no_three_cover_pairings
],
}
)
if result.no_double_cover_pairings:
double_cover_failures.append(
{
"graph_edges": chosen_edges,
"matroid": result.name,
"pairings": [
{
"pairing": pr.pairing,
"rainbow_count": pr.rainbow_count,
"min_cover": pr.min_cover,
}
for pr in result.no_double_cover_pairings
],
}
)
return {
"mode": "simple_graphic",
"vertices": num_vertices,
"edges": num_edges,
"graphs_checked": checked,
"connected_graphs": connected,
"qualifying_matroids": qualifying,
"pairings": len(pairings),
"max_min_cover": max_cover,
"three_cover_counterexamples": counterexamples,
"double_cover_failures": double_cover_failures,
}
def main() -> None:
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers(dest="mode", required=True)
exhaustive = subparsers.add_parser("exhaustive")
exhaustive.add_argument("--ranks", nargs="+", type=int, required=True)
exhaustive.add_argument("--limit-per-rank", type=int, default=None)
random_linear = subparsers.add_parser("random-linear")
random_linear.add_argument("--rank", type=int, required=True)
random_linear.add_argument("--field-size", type=int, required=True)
random_linear.add_argument("--samples", type=int, default=1000)
random_linear.add_argument("--seed", type=int, default=0)
simple_graphic = subparsers.add_parser("simple-graphic")
simple_graphic.add_argument("--vertices", type=int, required=True)
simple_graphic.add_argument("--edges", type=int, required=True)
args = parser.parse_args()
if args.mode == "exhaustive":
result = exhaustive_search(args.ranks, args.limit_per_rank)
elif args.mode == "random-linear":
result = random_linear_search(args.rank, args.field_size, args.samples, args.seed)
else:
result = simple_graphic_search(args.vertices, args.edges)
print(json.dumps(result, indent=2, sort_keys=True))
if __name__ == "__main__":
main()