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(
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;