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(
97 &self,
98 target_solution: &<Self::Target as crate::traits::Problem>::Solution,
99 ) -> crate::rules::ExtractionResult<<Self::Source as crate::traits::Problem>::Solution> {
100 crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?;
101
102 Ok({
103 let graph = self.source.graph();
104 let edges = graph.edges();
105 let num_source_edges = edges.len();
106 let m = graph.left_size();
107 let target_ones = self.target.ones();
108
109 // Step 1: map selected target 1-entries back to source edge indices.
110 // The reduction places source edge `(l_i, r_j)` (in bipartite-local
111 // form) at matrix cell `(i, m + j)`, which equals the global edge
112 // `(i, m + j)` returned by `Graph::edges()`. Build the lookup from
113 // matrix cell -> source edge index so we are robust to any ordering
114 // discrepancy between `Graph::edges()` and row-major 1-entries.
115 let cell_to_source_edge: std::collections::HashMap<(usize, usize), usize> = edges
116 .iter()
117 .enumerate()
118 .map(|(idx, &(u, v))| {
119 // Source edge endpoints in bipartite global coords are
120 // (left_idx, m + right_idx); matrix cell is (row=left, col=m+right).
121 let (row, col) = if u < m { (u, v) } else { (v, u) };
122 ((row, col), idx)
123 })
124 .collect();
125 let mut d: Vec<usize> = target_solution
126 .iter()
127 .zip(target_ones.iter())
128 .filter_map(|(&sel, &cell)| {
129 if sel {
130 Some(cell_to_source_edge.get(&cell).copied().ok_or_else(|| {
131 crate::rules::ExtractionError::invalid(format!(
132 "selected matrix cell {cell:?} has no source edge"
133 ))
134 }))
135 } else {
136 None
137 }
138 })
139 .collect::<crate::rules::ExtractionResult<_>>()?;
140
141 // Step 2: Yannakakis-Gavril EDS -> independent EDS (maximal matching).
142 // Loop invariants: `d` is an EDS of the source graph; each iteration
143 // strictly decreases either |d| or the number of (unordered) pairs of
144 // adjacent edges inside `d`.
145 loop {
146 // Find an adjacent pair (e1_idx, e2_idx) inside `d`, sharing vertex v.
147 let pair = find_adjacent_pair(&d, &edges);
148 let Some((e1_idx, e2_idx, _shared)) = pair else {
149 break; // `d` is a matching; we are done.
150 };
151
152 // Try dropping e1_idx or e2_idx if the remainder is still an EDS.
153 let mut without_e1 = d.clone();
154 let e1_position = d.iter().position(|&x| x == e1_idx).ok_or_else(|| {
155 crate::rules::ExtractionError::invalid(
156 "edge-domination transformation lost its selected edge",
157 )
158 })?;
159 without_e1.swap_remove(e1_position);
160 if is_edge_dominating_set(&without_e1, &edges) {
161 d = without_e1;
162 continue;
163 }
164 let mut without_e2 = d.clone();
165 let e2_position = d.iter().position(|&x| x == e2_idx).ok_or_else(|| {
166 crate::rules::ExtractionError::invalid(
167 "edge-domination transformation lost its selected edge",
168 )
169 })?;
170 without_e2.swap_remove(e2_position);
171 if is_edge_dominating_set(&without_e2, &edges) {
172 d = without_e2;
173 continue;
174 }
175
176 // Neither drop works -> perform a swap on one of e1 or e2.
177 // Choose endpoint not shared with the other edge: for e1=(u, v),
178 // e2=(v, w), the "non-shared" endpoint of e1 is u.
179 let (e1_a, e1_b) = edges[e1_idx];
180 let (e2_a, e2_b) = edges[e2_idx];
181 let shared = if e1_a == e2_a || e1_a == e2_b {
182 e1_a
183 } else {
184 e1_b
185 };
186 let u = if e1_a == shared { e1_b } else { e1_a };
187 let w = if e2_a == shared { e2_b } else { e2_a };
188
189 // Try to swap e1 := (u, x) where x ∉ V(d \ {e1}). The YG proof
190 // guarantees such x exists when neither drop succeeded.
191 if let Some(new_idx) = find_swap_edge(u, e1_idx, &d, &edges) {
192 d[e1_position] = new_idx;
193 continue;
194 }
195 // Symmetric swap on e2.
196 if let Some(new_idx) = find_swap_edge(w, e2_idx, &d, &edges) {
197 d[e2_position] = new_idx;
198 continue;
199 }
200
201 // YG guarantees that for an EDS at least one of the four moves
202 // above succeeds. Reaching this point implies the input was not
203 // a valid EDS (i.e., not a feasible MMD witness on the constructed
204 // instance), which violates the reduction's precondition.
205 return Err(crate::rules::ExtractionError::invalid(
206 "target matrix entries do not encode an edge-dominating set",
207 ));
208 }
209
210 // Step 3: encode the matching as a binary configuration over source edges.
211 let mut config = vec![false; num_source_edges];
212 for &idx in &d {
213 config[idx] = true;
214 }
215 config
216 })
217 }
218}
219
220/// Return `Some((i, j, v))` where `i`, `j` are indices in `d` of two edges that
221/// share vertex `v`, or `None` if all edges in `d` are pairwise independent.
222fn find_adjacent_pair(d: &[usize], edges: &[(usize, usize)]) -> Option<(usize, usize, usize)> {
223 for (a_pos, &i) in d.iter().enumerate() {
224 let (iu, iv) = edges[i];
225 for &j in &d[a_pos + 1..] {
226 let (ju, jv) = edges[j];
227 if iu == ju || iu == jv {
228 return Some((i, j, iu));
229 }
230 if iv == ju || iv == jv {
231 return Some((i, j, iv));
232 }
233 }
234 }
235 None
236}
237
238/// Check whether the edge set `d` (indices into `edges`) dominates every edge
239/// of `edges`. An edge `f` is dominated iff `f ∈ d` or `f` shares an endpoint
240/// with some edge in `d`.
241fn is_edge_dominating_set(d: &[usize], edges: &[(usize, usize)]) -> bool {
242 // Vertex cover of the candidate EDS.
243 let mut covered_vertices: std::collections::HashSet<usize> = std::collections::HashSet::new();
244 for &i in d {
245 let (u, v) = edges[i];
246 covered_vertices.insert(u);
247 covered_vertices.insert(v);
248 }
249 edges.iter().enumerate().all(|(f_idx, (u, v))| {
250 d.contains(&f_idx) || covered_vertices.contains(u) || covered_vertices.contains(v)
251 })
252}
253
254/// Find an edge index in `edges` that is (i) incident to vertex `endpoint`,
255/// (ii) different from `excluded_idx`, and (iii) whose other endpoint lies
256/// outside `V(d \ {excluded_idx})`.
257///
258/// This is the swap candidate `(u, x)` from the Yannakakis-Gavril argument
259/// when the drop move is not available for `excluded_idx`.
260fn find_swap_edge(
261 endpoint: usize,
262 excluded_idx: usize,
263 d: &[usize],
264 edges: &[(usize, usize)],
265) -> Option<usize> {
266 // Vertex cover of d \ {excluded_idx}.
267 let mut other_cover: std::collections::HashSet<usize> = std::collections::HashSet::new();
268 for &i in d {
269 if i == excluded_idx {
270 continue;
271 }
272 let (u, v) = edges[i];
273 other_cover.insert(u);
274 other_cover.insert(v);
275 }
276 for (k, &(u, v)) in edges.iter().enumerate() {
277 if k == excluded_idx {
278 continue;
279 }
280 let (e_endpoint, other) = if u == endpoint {
281 (u, v)
282 } else if v == endpoint {
283 (v, u)
284 } else {
285 continue;
286 };
287 debug_assert_eq!(e_endpoint, endpoint);
288 if !other_cover.contains(&other) {
289 return Some(k);
290 }
291 }
292 None
293}
294
295#[reduction(
296 transform = exact {
297 num_rows = "num_vertices",
298 num_cols = "num_vertices",
299 num_ones = "num_edges",
300 }
301)]
302impl ReduceTo<MinimumMatrixDomination> for MinimumMaximalMatching<BipartiteGraph> {
303 type Result = ReductionMMMToMatrixDomination;
304
305 fn reduce_to(&self) -> Result<Self::Result, crate::rules::ReductionError> {
306 let g = self.graph();
307 let m = g.left_size();
308 let n = g.right_size();
309 let big_n = m + n;
310
311 // Build the N x N matrix:
312 // upper-right m x n block = biadjacency matrix B*
313 // all other entries = 0
314 // The matrix is upper triangular: 1-entries lie strictly in rows
315 // 0..m and columns m..m+n.
316 let mut matrix = vec![vec![false; big_n]; big_n];
317 for &(left_idx, right_idx) in g.left_edges() {
318 // Row = l_left_idx (in 0..m), Column = m + right_idx (in m..m+n).
319 matrix[left_idx][m + right_idx] = true;
320 }
321
322 let target = MinimumMatrixDomination::new(matrix);
323
324 Ok(ReductionMMMToMatrixDomination {
325 target,
326 source: self.clone(),
327 })
328 }
329}
330
331#[cfg(feature = "example-db")]
332pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::RuleExampleSpec> {
333 use crate::export::SolutionPair;
334
335 vec![crate::example_db::specs::RuleExampleSpec {
336 id: "minimummaximalmatching_to_minimummatrixdomination",
337 build: || {
338 // Canonical YES instance from the issue.
339 //
340 // Bipartite graph B with L = {l0, l1}, R = {r0, r1, r2} and edges
341 // F = {(l0, r0), (l0, r1), (l0, r2), (l1, r1), (l1, r2)}.
342 //
343 // Source edge indices (in BipartiteGraph::edges() order):
344 // 0: (l0, r0) = (0, 0)
345 // 1: (l0, r1) = (0, 1)
346 // 2: (l0, r2) = (0, 2)
347 // 3: (l1, r1) = (1, 1)
348 // 4: (l1, r2) = (1, 2)
349 //
350 // mm(B) = 2; one optimum is M = {(l0, r0), (l1, r1)} ->
351 // source_config = [1, 0, 0, 1, 0].
352 //
353 // Constructed N x N matrix with N = 5; 1-entries in row-major
354 // order (matching the source edge order above):
355 // idx 0: (0, 2) <- (l0, r0)
356 // idx 1: (0, 3) <- (l0, r1)
357 // idx 2: (0, 4) <- (l0, r2)
358 // idx 3: (1, 3) <- (l1, r1)
359 // idx 4: (1, 4) <- (l1, r2)
360 //
361 // Selecting target_config = [1, 0, 0, 1, 0] picks 1-entries
362 // {(0, 2), (1, 3)}, which together dominate every other 1-entry by
363 // shared row 0 or row 1.
364 let source = MinimumMaximalMatching::new(BipartiteGraph::new(
365 2,
366 3,
367 vec![(0, 0), (0, 1), (0, 2), (1, 1), (1, 2)],
368 ));
369 crate::example_db::specs::rule_example_with_witness::<_, MinimumMatrixDomination>(
370 source,
371 SolutionPair {
372 source_config: serde_json::json!(vec![true, false, false, true, false]),
373 target_config: serde_json::json!(vec![true, false, false, true, false]),
374 },
375 )
376 },
377 }]
378}
379
380#[cfg(test)]
381#[path = "../unit_tests/rules/minimummaximalmatching_minimummatrixdomination.rs"]
382mod tests;