problemreductions/rules/minimummaximalmatching_minimummatrixdomination.rs
1//! Reduction from MinimumMaximalMatching (on a bipartite graph) to
2//! MinimumMatrixDomination.
3//!
4//! Classical reduction of Yannakakis and Gavril (1980) establishing
5//! NP-completeness of MATRIX DOMINATION (Garey & Johnson MS12). For a bipartite
6//! graph `B = (L, R, F)` with `|L| = m` and `|R| = n`, construct the `N x N`
7//! binary matrix `M` (with `N = m + n`) whose upper-right `m x n` block is the
8//! biadjacency matrix `B*` of `B` and whose remaining entries are zero. The
9//! 1-entries of `M` are in bijection with the edges of `B`, and two 1-entries
10//! share a row or column iff the corresponding edges share an endpoint. Hence a
11//! dominating set of 1-entries in `M` corresponds to an edge dominating set of
12//! `B`, and by Yannakakis and Gavril (1980), the minimum edge dominating set
13//! size equals the minimum maximal matching size.
14//!
15//! ## Witness extraction
16//!
17//! Solving Minimum Matrix Domination on the constructed instance yields a
18//! minimum edge dominating set of `B`, which is in general NOT a matching.
19//! Yannakakis and Gavril (1980) prove that any edge dominating set `D` can be
20//! transformed in polynomial time into an independent edge dominating set
21//! (a maximal matching) `M` of the same or smaller size. We implement this
22//! polynomial transformation directly: repeatedly resolve adjacent pairs in
23//! `D` by either dropping a redundant edge (when its endpoint is already
24//! dominated by `D \ {e}`) or swapping it for an edge whose new endpoint lies
25//! outside the current vertex cover. The procedure runs in `O(|F|^3)` worst
26//! case and never enumerates configurations.
27//!
28//! ## Source variant
29//!
30//! The reduction requires the bipartite (`BipartiteGraph`) variant of
31//! `MinimumMaximalMatching`. The biadjacency matrix faithfully represents the
32//! edge structure of a bipartite graph (each edge -> exactly one 1-entry),
33//! whereas an undirected adjacency matrix would produce two symmetric 1-entries
34//! per edge that do not preserve the row/column sharing pattern.
35
36use crate::models::algebraic::MinimumMatrixDomination;
37use crate::models::graph::MinimumMaximalMatching;
38use crate::reduction;
39use crate::rules::traits::{ReduceTo, ReductionResult};
40use crate::topology::{BipartiteGraph, Graph};
41
42/// Result of reducing `MinimumMaximalMatching<BipartiteGraph>` to
43/// `MinimumMatrixDomination`.
44///
45/// Holds the constructed target matrix-domination instance together with a copy
46/// of the source bipartite-matching problem. The source copy is used by
47/// `extract_solution` to perform the Yannakakis-Gavril conversion from an edge
48/// dominating set to an equally-sized maximal matching.
49#[derive(Debug, Clone)]
50pub struct ReductionMMMToMatrixDomination {
51 target: MinimumMatrixDomination,
52 source: MinimumMaximalMatching<BipartiteGraph>,
53}
54
55impl ReductionResult for ReductionMMMToMatrixDomination {
56 type Source = MinimumMaximalMatching<BipartiteGraph>;
57 type Target = MinimumMatrixDomination;
58
59 fn target_problem(&self) -> &Self::Target {
60 &self.target
61 }
62
63 /// Extract a maximal matching of the source bipartite graph from a
64 /// matrix-domination witness via the Yannakakis-Gavril (1980) polynomial
65 /// EDS-to-IEDS transformation.
66 ///
67 /// The target witness identifies a set of 1-entries of `M`. Each selected
68 /// 1-entry in the upper-right block `B*` corresponds bijectively to a
69 /// source edge, so the selection induces an edge set `D` of `B` that is an
70 /// edge dominating set (EDS). Arbitrary optimal MMD witnesses may select
71 /// 1-entries whose corresponding source edges form a connected subgraph
72 /// rather than a matching (e.g. two edges sharing a left endpoint), so
73 /// `D` is not in general independent.
74 ///
75 /// The Yannakakis-Gavril transformation (Theorem 1 of @yannakakis1980)
76 /// converts any EDS into an independent EDS (a maximal matching) of the
77 /// same or smaller size by repeatedly applying one of the following
78 /// reductions while `D` contains two adjacent edges `e1 = (u, v)` and
79 /// `e2 = (v, w)`:
80 ///
81 /// - **Drop:** if every edge of `B` incident to `u` is already dominated
82 /// by `D \ {e1}`, set `D := D \ {e1}` (size strictly decreases).
83 /// Symmetric for `w` and `e2`.
84 /// - **Swap:** otherwise, some edge `(u, x)` of `B` is currently dominated
85 /// only by `e1`. This `x` must lie outside `V(D \ {e1})` and is
86 /// therefore distinct from `w`, so `(u, x)` is not adjacent to `e2`.
87 /// Replace `e1` with `(u, x)`: `D := (D \ {e1}) \cup {(u, x)}`. Size is
88 /// preserved and the adjacent pair at `v` is resolved.
89 ///
90 /// Each iteration strictly decreases either `|D|` or the number of
91 /// adjacent pairs, so the loop terminates in `O(|F|^2)` iterations. Each
92 /// iteration scans `O(|F|)` edges to find an adjacent pair, an EDS check,
93 /// and a swap candidate, for a total of `O(|F|^3)` time. The result is a
94 /// matching that is an EDS, i.e. an independent EDS, which is precisely a
95 /// maximal matching.
96 fn extract_solution(&self, target_solution: &[usize]) -> Vec<usize> {
97 let graph = self.source.graph();
98 let edges = graph.edges();
99 let num_source_edges = edges.len();
100 let m = graph.left_size();
101 let target_ones = self.target.ones();
102
103 // Step 1: map selected target 1-entries back to source edge indices.
104 // The reduction places source edge `(l_i, r_j)` (in bipartite-local
105 // form) at matrix cell `(i, m + j)`, which equals the global edge
106 // `(i, m + j)` returned by `Graph::edges()`. Build the lookup from
107 // matrix cell -> source edge index so we are robust to any ordering
108 // discrepancy between `Graph::edges()` and row-major 1-entries.
109 let cell_to_source_edge: std::collections::HashMap<(usize, usize), usize> = edges
110 .iter()
111 .enumerate()
112 .map(|(idx, &(u, v))| {
113 // Source edge endpoints in bipartite global coords are
114 // (left_idx, m + right_idx); matrix cell is (row=left, col=m+right).
115 let (row, col) = if u < m { (u, v) } else { (v, u) };
116 ((row, col), idx)
117 })
118 .collect();
119 let mut d: Vec<usize> = target_solution
120 .iter()
121 .zip(target_ones.iter())
122 .filter_map(|(&sel, &cell)| {
123 if sel == 1 {
124 cell_to_source_edge.get(&cell).copied()
125 } else {
126 None
127 }
128 })
129 .collect();
130
131 // Step 2: Yannakakis-Gavril EDS -> independent EDS (maximal matching).
132 // Loop invariants: `d` is an EDS of the source graph; each iteration
133 // strictly decreases either |d| or the number of (unordered) pairs of
134 // adjacent edges inside `d`.
135 loop {
136 // Find an adjacent pair (e1_idx, e2_idx) inside `d`, sharing vertex v.
137 let pair = find_adjacent_pair(&d, &edges);
138 let Some((e1_idx, e2_idx, _shared)) = pair else {
139 break; // `d` is a matching; we are done.
140 };
141
142 // Try dropping e1_idx or e2_idx if the remainder is still an EDS.
143 let mut without_e1 = d.clone();
144 without_e1.swap_remove(d.iter().position(|&x| x == e1_idx).unwrap());
145 if is_edge_dominating_set(&without_e1, &edges) {
146 d = without_e1;
147 continue;
148 }
149 let mut without_e2 = d.clone();
150 without_e2.swap_remove(d.iter().position(|&x| x == e2_idx).unwrap());
151 if is_edge_dominating_set(&without_e2, &edges) {
152 d = without_e2;
153 continue;
154 }
155
156 // Neither drop works -> perform a swap on one of e1 or e2.
157 // Choose endpoint not shared with the other edge: for e1=(u, v),
158 // e2=(v, w), the "non-shared" endpoint of e1 is u.
159 let (e1_a, e1_b) = edges[e1_idx];
160 let (e2_a, e2_b) = edges[e2_idx];
161 let shared = if e1_a == e2_a || e1_a == e2_b {
162 e1_a
163 } else {
164 e1_b
165 };
166 let u = if e1_a == shared { e1_b } else { e1_a };
167 let w = if e2_a == shared { e2_b } else { e2_a };
168
169 // Try to swap e1 := (u, x) where x ∉ V(d \ {e1}). The YG proof
170 // guarantees such x exists when neither drop succeeded.
171 if let Some(new_idx) = find_swap_edge(u, e1_idx, &d, &edges) {
172 replace_in(&mut d, e1_idx, new_idx);
173 continue;
174 }
175 // Symmetric swap on e2.
176 if let Some(new_idx) = find_swap_edge(w, e2_idx, &d, &edges) {
177 replace_in(&mut d, e2_idx, new_idx);
178 continue;
179 }
180
181 // YG guarantees that for an EDS at least one of the four moves
182 // above succeeds. Reaching this point implies the input was not
183 // a valid EDS (i.e., not a feasible MMD witness on the constructed
184 // instance), which violates the reduction's precondition.
185 unreachable!(
186 "Yannakakis-Gavril EDS->IEDS transformation could not progress; \
187 target witness must be a feasible (dominating) MMD configuration"
188 );
189 }
190
191 // Step 3: encode the matching as a binary configuration over source edges.
192 let mut config = vec![0usize; num_source_edges];
193 for &idx in &d {
194 config[idx] = 1;
195 }
196 config
197 }
198}
199
200/// Return `Some((i, j, v))` where `i`, `j` are indices in `d` of two edges that
201/// share vertex `v`, or `None` if all edges in `d` are pairwise independent.
202fn find_adjacent_pair(d: &[usize], edges: &[(usize, usize)]) -> Option<(usize, usize, usize)> {
203 for (a_pos, &i) in d.iter().enumerate() {
204 let (iu, iv) = edges[i];
205 for &j in &d[a_pos + 1..] {
206 let (ju, jv) = edges[j];
207 if iu == ju || iu == jv {
208 return Some((i, j, iu));
209 }
210 if iv == ju || iv == jv {
211 return Some((i, j, iv));
212 }
213 }
214 }
215 None
216}
217
218/// Check whether the edge set `d` (indices into `edges`) dominates every edge
219/// of `edges`. An edge `f` is dominated iff `f ∈ d` or `f` shares an endpoint
220/// with some edge in `d`.
221fn is_edge_dominating_set(d: &[usize], edges: &[(usize, usize)]) -> bool {
222 // Vertex cover of the candidate EDS.
223 let mut covered_vertices: std::collections::HashSet<usize> = std::collections::HashSet::new();
224 for &i in d {
225 let (u, v) = edges[i];
226 covered_vertices.insert(u);
227 covered_vertices.insert(v);
228 }
229 edges.iter().enumerate().all(|(f_idx, (u, v))| {
230 d.contains(&f_idx) || covered_vertices.contains(u) || covered_vertices.contains(v)
231 })
232}
233
234/// Find an edge index in `edges` that is (i) incident to vertex `endpoint`,
235/// (ii) different from `excluded_idx`, and (iii) whose other endpoint lies
236/// outside `V(d \ {excluded_idx})`.
237///
238/// This is the swap candidate `(u, x)` from the Yannakakis-Gavril argument
239/// when the drop move is not available for `excluded_idx`.
240fn find_swap_edge(
241 endpoint: usize,
242 excluded_idx: usize,
243 d: &[usize],
244 edges: &[(usize, usize)],
245) -> Option<usize> {
246 // Vertex cover of d \ {excluded_idx}.
247 let mut other_cover: std::collections::HashSet<usize> = std::collections::HashSet::new();
248 for &i in d {
249 if i == excluded_idx {
250 continue;
251 }
252 let (u, v) = edges[i];
253 other_cover.insert(u);
254 other_cover.insert(v);
255 }
256 for (k, &(u, v)) in edges.iter().enumerate() {
257 if k == excluded_idx {
258 continue;
259 }
260 let (e_endpoint, other) = if u == endpoint {
261 (u, v)
262 } else if v == endpoint {
263 (v, u)
264 } else {
265 continue;
266 };
267 debug_assert_eq!(e_endpoint, endpoint);
268 if !other_cover.contains(&other) {
269 return Some(k);
270 }
271 }
272 None
273}
274
275/// Replace `old_idx` with `new_idx` inside `d` in-place. Panics if `old_idx`
276/// is not present.
277fn replace_in(d: &mut [usize], old_idx: usize, new_idx: usize) {
278 let pos = d
279 .iter()
280 .position(|&x| x == old_idx)
281 .expect("old_idx must be present in d");
282 d[pos] = new_idx;
283}
284
285#[reduction(
286 overhead = {
287 num_rows = "num_vertices",
288 num_cols = "num_vertices",
289 num_ones = "num_edges",
290 }
291)]
292impl ReduceTo<MinimumMatrixDomination> for MinimumMaximalMatching<BipartiteGraph> {
293 type Result = ReductionMMMToMatrixDomination;
294
295 fn reduce_to(&self) -> Self::Result {
296 let g = self.graph();
297 let m = g.left_size();
298 let n = g.right_size();
299 let big_n = m + n;
300
301 // Build the N x N matrix:
302 // upper-right m x n block = biadjacency matrix B*
303 // all other entries = 0
304 // The matrix is upper triangular: 1-entries lie strictly in rows
305 // 0..m and columns m..m+n.
306 let mut matrix = vec![vec![false; big_n]; big_n];
307 for &(left_idx, right_idx) in g.left_edges() {
308 // Row = l_left_idx (in 0..m), Column = m + right_idx (in m..m+n).
309 matrix[left_idx][m + right_idx] = true;
310 }
311
312 let target = MinimumMatrixDomination::new(matrix);
313
314 ReductionMMMToMatrixDomination {
315 target,
316 source: self.clone(),
317 }
318 }
319}
320
321#[cfg(feature = "example-db")]
322pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::RuleExampleSpec> {
323 use crate::export::SolutionPair;
324
325 vec![crate::example_db::specs::RuleExampleSpec {
326 id: "minimummaximalmatching_to_minimummatrixdomination",
327 build: || {
328 // Canonical YES instance from the issue.
329 //
330 // Bipartite graph B with L = {l0, l1}, R = {r0, r1, r2} and edges
331 // F = {(l0, r0), (l0, r1), (l0, r2), (l1, r1), (l1, r2)}.
332 //
333 // Source edge indices (in BipartiteGraph::edges() order):
334 // 0: (l0, r0) = (0, 0)
335 // 1: (l0, r1) = (0, 1)
336 // 2: (l0, r2) = (0, 2)
337 // 3: (l1, r1) = (1, 1)
338 // 4: (l1, r2) = (1, 2)
339 //
340 // mm(B) = 2; one optimum is M = {(l0, r0), (l1, r1)} ->
341 // source_config = [1, 0, 0, 1, 0].
342 //
343 // Constructed N x N matrix with N = 5; 1-entries in row-major
344 // order (matching the source edge order above):
345 // idx 0: (0, 2) <- (l0, r0)
346 // idx 1: (0, 3) <- (l0, r1)
347 // idx 2: (0, 4) <- (l0, r2)
348 // idx 3: (1, 3) <- (l1, r1)
349 // idx 4: (1, 4) <- (l1, r2)
350 //
351 // Selecting target_config = [1, 0, 0, 1, 0] picks 1-entries
352 // {(0, 2), (1, 3)}, which together dominate every other 1-entry by
353 // shared row 0 or row 1.
354 let source = MinimumMaximalMatching::new(BipartiteGraph::new(
355 2,
356 3,
357 vec![(0, 0), (0, 1), (0, 2), (1, 1), (1, 2)],
358 ));
359 crate::example_db::specs::rule_example_with_witness::<_, MinimumMatrixDomination>(
360 source,
361 SolutionPair {
362 source_config: vec![1, 0, 0, 1, 0],
363 target_config: vec![1, 0, 0, 1, 0],
364 },
365 )
366 },
367 }]
368}
369
370#[cfg(test)]
371#[path = "../unit_tests/rules/minimummaximalmatching_minimummatrixdomination.rs"]
372mod tests;