Skip to content

Commit 09fb3fd

Browse files
isPANNclaude
andcommitted
Fix nits: MinimumMaximalMatching -> MaximumAchromaticNumber (#846)
Address three review nits on commit 1826867: 1. Canonical example richness: swap the path P4 canonical example for a "T-tree" on 5 vertices (spider v0-v1-v2-v3 with extra leaf v1-v4), which exposes two strictly suboptimal maximal matchings besides the minimum (mm = 1 vs. two size-2 maximal matchings), comfortably passing the >=2-suboptimals rule-of-thumb. Update the canonical builder, the paper worked example, and the closed-loop test to match. 2. extract_solution: replace the per-call HashMap rebuild with a single pass over source_edges that checks whether each edge's endpoints share a color. For bipartite G all color classes have size <= 2, so this is equivalent and avoids any auxiliary allocation. 3. Unit test imports: drop the catch-all "use super::*" in favour of an explicit "use" list mirroring the MVC->MIS reference test. cargo test rules::minimummaximalmatching_maximumachromaticnumber: 5 passed (closed-loop, complement structure, known coloring, suboptimal recovery, identity); make paper builds cleanly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent a8ee2fa commit 09fb3fd

3 files changed

Lines changed: 109 additions & 106 deletions

File tree

docs/paper/reductions.typ

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14429,7 +14429,7 @@ The following reductions to Integer Linear Programming are straightforward formu
1442914429
example: true,
1443014430
example-source-variant: (graph: "BipartiteGraph"),
1443114431
example-target-variant: (graph: "SimpleGraph"),
14432-
example-caption: [Path $P_4$ as a bipartite graph with $A = {v_0, v_2}$, $B = {v_1, v_3}$.],
14432+
example-caption: [T-tree on $5$ vertices (spider with three legs at $v_1$): $v_0 - v_1 - v_2 - v_3$ plus the leaf $v_1 - v_4$, encoded as bipartite with $A = {v_0, v_2, v_4}$, $B = {v_1, v_3}$.],
1443314433
extra: [
1443414434
#{
1443514435
let source-edges = mmm_ach.target.instance.graph.edges
@@ -14448,15 +14448,15 @@ The following reductions to Integer Linear Programming are straightforward formu
1444814448
"pred evaluate mmm.json --config " + mmm_ach_sol.source_config.map(str).join(","),
1444914449
)
1445014450

14451-
*Step 1 -- Source instance.* Path $P_4$ encoded as a bipartite graph with bipartition $A = {v_0, v_2}$ and $B = {v_1, v_3}$. In unified indices the vertex set is ${0, 1, 2, 3}$ (left vertices first), $n = #n-source$, and the $m = #m-source$ edges are #source-edges.map(e => $(#e.at(0), #e.at(1))$).join(", ").
14451+
*Step 1 -- Source instance.* The T-tree on $5$ vertices is the spider graph with centre $v_1$ and legs to $v_0$, $v_2$, $v_4$, plus the pendant edge $v_2 - v_3$. It is bipartite with $A = {v_0, v_2, v_4}$ and $B = {v_1, v_3}$. In unified indices the vertex set is ${0, 1, 2, 3, 4}$ (left vertices first, mapping $v_0 mapsto 0$, $v_2 mapsto 1$, $v_4 mapsto 2$, $v_1 mapsto 3$, $v_3 mapsto 4$), so $n = #n-source$ and the $m = #m-source$ edges are #source-edges.map(e => $(#e.at(0), #e.at(1))$).join(", ").
1445214452

14453-
*Step 2 -- Complement graph $H = overline(G)$.* The non-edges of $G$ in $K_4$ give the target edge set, with $|E(H)| = #m-target$ edges (#mmm_ach.target.instance.graph.edges.map(e => $(#e.at(0), #e.at(1))$).join(", ")). The decision threshold transforms as $K' = n - K$.
14453+
*Step 2 -- Complement graph $H = overline(G)$.* The non-edges of $G$ in $K_5$ give the target edge set, with $|E(H)| = #m-target$ edges (#mmm_ach.target.instance.graph.edges.map(e => $(#e.at(0), #e.at(1))$).join(", ")). The decision threshold transforms as $K' = n - K$.
1445414454

14455-
*Step 3 -- Source optimum.* The minimum maximal matching uses the middle edge, so $"mm"(G) = #matched.len() = 1$ (source index $#matched.at(0)$).
14455+
*Step 3 -- Source optimum.* The minimum maximal matching uses the central edge $(v_1, v_2)$, so $"mm"(G) = #matched.len() = 1$ (source index $#matched.at(0)$). The T-tree also admits two strictly larger maximal matchings $\{(v_0, v_1), (v_2, v_3)\}$ and $\{(v_1, v_4), (v_2, v_3)\}$, both of size $2$ -- this richness is the reason for choosing the T-tree over the path $P_4$ as the canonical example.
1445614456

1445714457
*Step 4 -- Target optimum.* The achromatic coloring stored in the fixture is $#color-of.map(str).join(", ")$. The size-$2$ color class corresponds to the source edge selected in Step 3, and the singletons contribute the remaining $n - 2$ classes, so the achromatic number is $psi(H) = n - "mm"(G) = #n-source - 1 = #(n-source - 1) #sym.checkmark$.
1445814458

14459-
*Multiplicity:* The fixture stores one canonical witness; other valid achromatic $3$-colorings exist and would extract to other minimum maximal matchings.
14459+
*Multiplicity:* The fixture stores one canonical witness; other valid achromatic $4$-colorings exist and would extract to the same minimum maximal matching after relabelling colors.
1446014460
]
1446114461
}
1446214462
],

src/rules/minimummaximalmatching_maximumachromaticnumber.rs

Lines changed: 46 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ use crate::models::graph::{MaximumAchromaticNumber, MinimumMaximalMatching};
1111
use crate::reduction;
1212
use crate::rules::traits::{ReduceTo, ReductionResult};
1313
use crate::topology::{BipartiteGraph, Graph, SimpleGraph};
14-
use std::collections::HashMap;
1514

1615
/// Result of reducing `MinimumMaximalMatching<BipartiteGraph>` to
1716
/// `MaximumAchromaticNumber<SimpleGraph>`.
@@ -38,39 +37,16 @@ impl ReductionResult for ReductionMMMToAchromatic {
3837
/// Extract a maximal matching of the source graph from an achromatic
3938
/// coloring of `complement(G)`.
4039
///
41-
/// Each color class of size exactly 2 corresponds to a clique-in-G of
42-
/// size 2, i.e., a single source edge. Marking those edges yields the
43-
/// maximal matching `M` with `|M| = |V| - k`, where `k` is the number of
44-
/// colors used.
40+
/// Each color class of size exactly 2 in `complement(G)` is an independent
41+
/// set there and thus a clique in `G`. For bipartite `G` such a clique has
42+
/// size 2, i.e., a source edge. A source edge `(u, v)` belongs to the
43+
/// extracted matching iff `u` and `v` share a color, which we detect in a
44+
/// single pass over `source_edges`.
4545
fn extract_solution(&self, target_solution: &[usize]) -> Vec<usize> {
46-
let num_source_edges = self.source_edges.len();
47-
let mut source_config = vec![0usize; num_source_edges];
48-
49-
// Group vertices by color.
50-
let mut color_to_vertices: HashMap<usize, Vec<usize>> = HashMap::new();
51-
for (vertex, &color) in target_solution.iter().enumerate() {
52-
color_to_vertices.entry(color).or_default().push(vertex);
53-
}
54-
55-
// Build an edge lookup keyed by canonical (min, max) pairs.
56-
let mut edge_index: HashMap<(usize, usize), usize> = HashMap::new();
57-
for (idx, &(u, v)) in self.source_edges.iter().enumerate() {
58-
let key = if u < v { (u, v) } else { (v, u) };
59-
edge_index.insert(key, idx);
60-
}
61-
62-
// Color classes of size 2 must be edges of G (cliques in G of size 2).
63-
for vertices in color_to_vertices.values() {
64-
if vertices.len() == 2 {
65-
let (a, b) = (vertices[0], vertices[1]);
66-
let key = if a < b { (a, b) } else { (b, a) };
67-
if let Some(&idx) = edge_index.get(&key) {
68-
source_config[idx] = 1;
69-
}
70-
}
71-
}
72-
73-
source_config
46+
self.source_edges
47+
.iter()
48+
.map(|&(u, v)| usize::from(target_solution[u] == target_solution[v]))
49+
.collect()
7450
}
7551
}
7652

@@ -119,50 +95,63 @@ pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::Ru
11995
vec![crate::example_db::specs::RuleExampleSpec {
12096
id: "minimummaximalmatching_to_maximumachromaticnumber",
12197
build: || {
122-
// Path P4 as a bipartite graph: A = {v0, v2}, B = {v1, v3}.
98+
// "T" tree (spider with three legs at the centre v1):
99+
// v0 -- v1 -- v2 -- v3
100+
// |
101+
// v4
123102
//
124-
// BipartiteGraph encoding (left_size = 2, right_size = 2):
125-
// left local 0 -> v0, local 1 -> v2
103+
// Five vertices and four edges. Bipartite with
104+
// A = {v0, v2, v4} and B = {v1, v3}.
105+
//
106+
// BipartiteGraph encoding (left_size = 3, right_size = 2):
107+
// left local 0 -> v0, local 1 -> v2, local 2 -> v4
126108
// right local 0 -> v1, local 1 -> v3
127109
// edges (left_idx, right_idx):
128110
// (v0, v1) -> (0, 0)
129-
// (v1, v2) -> (1, 0) (v2 is left=1, v1 is right=0)
111+
// (v1, v2) -> (1, 0)
130112
// (v2, v3) -> (1, 1)
113+
// (v1, v4) -> (2, 0)
131114
//
132115
// Unified vertex labels:
133-
// 0 = v0 (left 0)
134-
// 1 = v2 (left 1)
135-
// 2 = v1 (right 0)
136-
// 3 = v3 (right 1)
116+
// 0 = v0 (left 0), 1 = v2 (left 1), 2 = v4 (left 2),
117+
// 3 = v1 (right 0), 4 = v3 (right 1).
118+
//
119+
// Unified edges from Graph::edges() (in source order):
120+
// (0, 3), (1, 3), (1, 4), (2, 3).
137121
//
138-
// Unified edges from Graph::edges():
139-
// (0, 2), (1, 2), (1, 3)
122+
// Maximal matchings of G:
123+
// - {(v1, v2)} size 1 <-- mm(G) = 1
124+
// - {(v0, v1), (v2, v3)} size 2 (suboptimal 1)
125+
// - {(v1, v4), (v2, v3)} size 2 (suboptimal 2)
126+
// So the canonical example exhibits >=2 suboptimal maximal
127+
// matchings besides the optimum.
140128
//
141-
// mm(G) = 1, achieved by selecting the middle edge (v1, v2),
142-
// which is unified edge index 1 (i.e., (1, 2)).
143-
// So source_config = [0, 1, 0].
129+
// Canonical optimum: pick the central edge (v1, v2), which is
130+
// source edge index 1. source_config = [0, 1, 0, 0].
144131
//
145-
// complement(G) edges: (0, 1), (0, 3), (2, 3).
132+
// complement(G) on K_5: 10 - 4 = 6 edges, namely
133+
// (0, 1), (0, 2), (0, 4), (1, 2), (2, 4), (3, 4).
146134
//
147-
// Achromatic 3-coloring of complement(G):
148-
// v0 (idx 0) -> color 0
149-
// v2 (idx 1) -> color 1
150-
// v1 (idx 2) -> color 1 (paired with v2 = G-edge (v1, v2))
151-
// v3 (idx 3) -> color 2
152-
// target_config = [0, 1, 1, 2].
135+
// Canonical achromatic 4-coloring of complement(G):
136+
// v0 (idx 0) -> color 1
137+
// v2 (idx 1) -> color 0 (paired with v1 = G-edge (v1, v2))
138+
// v4 (idx 2) -> color 3
139+
// v1 (idx 3) -> color 0
140+
// v3 (idx 4) -> color 2
141+
// target_config = [1, 0, 3, 0, 2]; psi(H) = |V| - mm(G) = 4.
153142
let source = MinimumMaximalMatching::new(BipartiteGraph::new(
143+
3,
154144
2,
155-
2,
156-
vec![(0, 0), (1, 0), (1, 1)],
145+
vec![(0, 0), (1, 0), (1, 1), (2, 0)],
157146
));
158147
crate::example_db::specs::rule_example_with_witness::<
159148
_,
160149
MaximumAchromaticNumber<SimpleGraph>,
161150
>(
162151
source,
163152
SolutionPair {
164-
source_config: vec![0, 1, 0],
165-
target_config: vec![0, 1, 1, 2],
153+
source_config: vec![0, 1, 0, 0],
154+
target_config: vec![1, 0, 3, 0, 2],
166155
},
167156
)
168157
},

src/unit_tests/rules/minimummaximalmatching_maximumachromaticnumber.rs

Lines changed: 58 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,49 @@
1-
use super::*;
1+
use crate::models::graph::{MaximumAchromaticNumber, MinimumMaximalMatching};
2+
use crate::rules::traits::{ReduceTo, ReductionResult};
23
use crate::solvers::{BruteForce, Solver};
4+
use crate::topology::{BipartiteGraph, Graph, SimpleGraph};
35
use crate::traits::Problem;
46
use crate::types::{Max, Min};
57

6-
/// Build the canonical issue example: path P4 represented as a bipartite graph
7-
/// with bipartition A = {v0, v2}, B = {v1, v3} and edges (v0,v1), (v1,v2),
8-
/// (v2,v3). See the canonical builder for the unified-index encoding.
9-
fn p4_bipartite() -> BipartiteGraph {
10-
// left_size = 2 (A), right_size = 2 (B)
8+
/// Build the canonical T-tree example: spider with three legs at the centre
9+
/// v1, namely v0--v1--v2--v3 with an extra leaf v1--v4. The bipartition is
10+
/// A = {v0, v2, v4}, B = {v1, v3}. See the canonical builder for the full
11+
/// unified-index encoding.
12+
fn t_tree_bipartite() -> BipartiteGraph {
13+
// left_size = 3 (A), right_size = 2 (B)
1114
// edges in (left_idx, right_idx) form:
1215
// (v0, v1) -> (0, 0)
1316
// (v1, v2) -> (1, 0)
1417
// (v2, v3) -> (1, 1)
15-
BipartiteGraph::new(2, 2, vec![(0, 0), (1, 0), (1, 1)])
18+
// (v1, v4) -> (2, 0)
19+
BipartiteGraph::new(3, 2, vec![(0, 0), (1, 0), (1, 1), (2, 0)])
1620
}
1721

1822
#[test]
1923
fn test_minimummaximalmatching_to_maximumachromaticnumber_closed_loop() {
20-
let source = MinimumMaximalMatching::new(p4_bipartite());
24+
let source = MinimumMaximalMatching::new(t_tree_bipartite());
2125
let reduction = ReduceTo::<MaximumAchromaticNumber<SimpleGraph>>::reduce_to(&source);
2226
let target = reduction.target_problem();
2327

24-
// |V| = 4, |E(G)| = 3, so |E(H)| = C(4,2) - 3 = 6 - 3 = 3
25-
assert_eq!(target.num_vertices(), 4);
26-
assert_eq!(target.num_edges(), 3);
28+
// |V| = 5, |E(G)| = 4, so |E(H)| = C(5,2) - 4 = 10 - 4 = 6
29+
assert_eq!(target.num_vertices(), 5);
30+
assert_eq!(target.num_edges(), 6);
2731

2832
let solver = BruteForce::new();
2933

30-
// Source MMM(P4) = 1 (the single middle edge is the minimum maximal matching).
34+
// Source MMM(T-tree) = 1 (the central edge (v1, v2) is a minimum maximal
35+
// matching on its own).
3136
assert_eq!(solver.solve(&source), Min(Some(1)));
3237

33-
// Target achromatic number of complement(P4) = |V| - mm(G) = 4 - 1 = 3.
34-
assert_eq!(solver.solve(target), Max(Some(3)));
38+
// Target achromatic number of complement(G) = |V| - mm(G) = 5 - 1 = 4.
39+
assert_eq!(solver.solve(target), Max(Some(4)));
3540

3641
// Closed-loop: every optimal target witness extracts to a valid maximal
3742
// matching with size mm(G).
3843
let target_witnesses = solver.find_all_witnesses(target);
3944
assert!(
4045
!target_witnesses.is_empty(),
41-
"complement(P4) must admit an achromatic 3-coloring"
46+
"complement(T-tree) must admit an achromatic 4-coloring"
4247
);
4348
for witness in &target_witnesses {
4449
let extracted = reduction.extract_solution(witness);
@@ -52,51 +57,60 @@ fn test_minimummaximalmatching_to_maximumachromaticnumber_closed_loop() {
5257

5358
#[test]
5459
fn test_target_complement_structure() {
55-
let source = MinimumMaximalMatching::new(p4_bipartite());
60+
let source = MinimumMaximalMatching::new(t_tree_bipartite());
5661
let reduction = ReduceTo::<MaximumAchromaticNumber<SimpleGraph>>::reduce_to(&source);
5762
let target = reduction.target_problem();
5863

59-
// Source unified edges: (0,2), (1,2), (1,3).
60-
// Complement edges in K_4 minus source: (0,1), (0,3), (2,3).
64+
// Source unified edges: (0,3), (1,3), (1,4), (2,3).
65+
// Complement edges in K_5 minus source: (0,1), (0,2), (0,4), (1,2), (2,4), (3,4).
6166
let mut target_edges = target.graph().edges();
6267
target_edges.sort();
63-
assert_eq!(target_edges, vec![(0, 1), (0, 3), (2, 3)]);
68+
assert_eq!(
69+
target_edges,
70+
vec![(0, 1), (0, 2), (0, 4), (1, 2), (2, 4), (3, 4)]
71+
);
6472
}
6573

6674
#[test]
6775
fn test_extract_solution_known_coloring() {
68-
let source = MinimumMaximalMatching::new(p4_bipartite());
76+
let source = MinimumMaximalMatching::new(t_tree_bipartite());
6977
let reduction = ReduceTo::<MaximumAchromaticNumber<SimpleGraph>>::reduce_to(&source);
7078

71-
// Coloring v0=0, v2=1, v1=1, v3=2 (unified order: v0,v2,v1,v3).
72-
// Size-2 class {1, 2} = {v2, v1}, which is the G-edge (v1, v2) =
73-
// unified edge (1, 2), source-edge index 1 in our edges list.
74-
let coloring = vec![0, 1, 1, 2];
79+
// Canonical 4-coloring of complement(G) (unified order v0,v2,v4,v1,v3):
80+
// v0 -> 1, v2 -> 0, v4 -> 3, v1 -> 0, v3 -> 2.
81+
// The single size-2 class {v2, v1} is the G-edge (v1, v2) =
82+
// unified edge (1, 3), source-edge index 1 in the edges list.
83+
let coloring = vec![1, 0, 3, 0, 2];
7584
let extracted = reduction.extract_solution(&coloring);
76-
assert_eq!(extracted, vec![0, 1, 0]);
85+
assert_eq!(extracted, vec![0, 1, 0, 0]);
7786
assert_eq!(source.evaluate(&extracted), Min(Some(1)));
7887
}
7988

8089
#[test]
81-
fn test_no_instance_higher_k_unreachable() {
82-
// For source K = 0, the source decision is NO because mm(P4) = 1 > 0.
83-
// The reduction sets K' = |V| - K = 4. The target asks for achromatic >= 4.
84-
// complement(P4) on 4 vertices admits at most achromatic number 3, so the
85-
// target decision is also NO (no 4-coloring is both proper and complete).
86-
let source = MinimumMaximalMatching::new(p4_bipartite());
90+
fn test_extract_solution_recovers_suboptimal_matchings() {
91+
// The T-tree exposes >=2 suboptimal maximal matchings besides the
92+
// optimum, exactly the situation that motivated the richer canonical
93+
// example. We feed the achromatic colorings induced by these size-2
94+
// maximal matchings and check the extractor recovers each one.
95+
let source = MinimumMaximalMatching::new(t_tree_bipartite());
8796
let reduction = ReduceTo::<MaximumAchromaticNumber<SimpleGraph>>::reduce_to(&source);
88-
let target = reduction.target_problem();
8997

90-
let solver = BruteForce::new();
91-
let target_value = solver.solve(target);
92-
93-
// Achromatic value is exactly 3 on this complement (verified above), so
94-
// the threshold 4 is unreachable.
95-
if let Max(Some(value)) = target_value {
96-
assert!(value < 4, "complement(P4) cannot reach achromatic = 4");
97-
} else {
98-
panic!("target must have some achromatic number");
99-
}
98+
// Unified labels: v0=0, v2=1, v4=2, v1=3, v3=4.
99+
// Suboptimal matching {(v0,v1), (v2,v3)} -> color v0,v1 the same and
100+
// v2,v3 the same; v4 takes a third color.
101+
// Source edges in unified order: (0,3), (1,3), (1,4), (2,3).
102+
// Edge 0 = (v0, v1) selected; edge 2 = (v2, v3) selected.
103+
let coloring_a = vec![0, 1, 2, 0, 1];
104+
let extracted_a = reduction.extract_solution(&coloring_a);
105+
assert_eq!(extracted_a, vec![1, 0, 1, 0]);
106+
assert_eq!(source.evaluate(&extracted_a), Min(Some(2)));
107+
108+
// Suboptimal matching {(v1, v4), (v2, v3)} -> pair v1 with v4 and v2
109+
// with v3; v0 takes a singleton color. Edge 2 = (v2, v3); edge 3 = (v1, v4).
110+
let coloring_b = vec![2, 0, 1, 1, 0];
111+
let extracted_b = reduction.extract_solution(&coloring_b);
112+
assert_eq!(extracted_b, vec![0, 0, 1, 1]);
113+
assert_eq!(source.evaluate(&extracted_b), Min(Some(2)));
100114
}
101115

102116
#[test]
@@ -108,11 +122,11 @@ fn test_identity_on_random_bipartite_instances() {
108122
// Small library of bipartite graphs:
109123
// - K_{2,2} (4-cycle viewed as bipartite, left=2, right=2)
110124
// - K_{1,3} (claw / star), and
111-
// - the canonical P4 above.
125+
// - the canonical T-tree above.
112126
let instances = vec![
113127
BipartiteGraph::new(2, 2, vec![(0, 0), (0, 1), (1, 0), (1, 1)]),
114128
BipartiteGraph::new(1, 3, vec![(0, 0), (0, 1), (0, 2)]),
115-
p4_bipartite(),
129+
t_tree_bipartite(),
116130
];
117131

118132
for graph in instances {

0 commit comments

Comments
 (0)