Skip to main content

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;