Skip to main content

problemreductions/rules/
exactcoverby3sets_boundeddiameterspanningtree.rs

1//! Reduction from ExactCoverBy3Sets to BoundedDiameterSpanningTree.
2//!
3//! Given an X3C instance with universe X (|X| = 3q) and collection
4//! C = [S_0, ..., S_{m-1}] of 3-element subsets of X, build a weighted graph
5//! and parameters (B, D) so that the BDST instance is feasible iff the source
6//! has an exact cover.
7//!
8//! Construction (Garey & Johnson, ND4):
9//!
10//! * Vertex set V = {r, v_1, v_2} ∪ {s_0, ..., s_{m-1}} ∪ {e_0, ..., e_{3q-1}}.
11//!   Indices: r = 0, v_1 = 1, v_2 = 2, s_i = 3 + i, e_j = 3 + m + j.
12//! * Edges (all weights ∈ {1, 2}):
13//!     - Forced-center path: (r, v_1) weight 1, (v_1, v_2) weight 1.
14//!     - Root-to-set: (r, s_i) weight 2 for every i ∈ [0, m).
15//!     - Set-to-element: (s_i, e_j) weight 1 whenever j ∈ S_i.
16//!     - Set clique: (s_i, s_j) weight 1 for all 0 ≤ i < j < m.
17//! * Diameter bound D = 4.
18//! * Weight bound B = 4q + m + 2.
19//!
20//! Forward direction: an exact cover C' = {S_{i_1}, ..., S_{i_q}} yields a
21//! spanning tree of total weight 2q + (m - q) + 3q + 2 = 4q + m + 2 = B and
22//! every vertex within distance 2 of r, so the diameter is ≤ 4.
23//!
24//! Backward direction: any feasible spanning tree must keep every vertex
25//! within distance 2 of r (otherwise dist to v_2 exceeds 4). Element vertices
26//! only attach through set vertices, so each e_j sits at depth 2 below some
27//! s_i that is directly attached to r. Budget counting then forces the number
28//! of root-to-set edges to be exactly q, and pigeonhole on the 3q element
29//! attachments forces the q chosen sets to be pairwise disjoint -- an exact
30//! cover.
31//!
32//! The solution extractor reads the binary indicator of each root-to-set edge.
33
34use crate::models::graph::BoundedDiameterSpanningTree;
35use crate::models::set::ExactCoverBy3Sets;
36use crate::reduction;
37use crate::rules::traits::{ReduceTo, ReductionResult};
38use crate::topology::SimpleGraph;
39use std::collections::HashSet;
40
41/// Result of reducing ExactCoverBy3Sets to BoundedDiameterSpanningTree.
42#[derive(Debug, Clone)]
43pub struct ReductionX3CToBoundedDiameterSpanningTree {
44    target: BoundedDiameterSpanningTree<SimpleGraph, i64>,
45    source_num_subsets: usize,
46}
47
48impl ReductionX3CToBoundedDiameterSpanningTree {
49    /// Counts for the incidence construction, checked before allocating it.
50    fn dimensions(
51        universe_size: usize,
52        m: usize,
53    ) -> Result<(usize, usize, i64), crate::rules::ReductionError> {
54        let overflow = || {
55            crate::rules::ReductionError::integer_overflow::<
56                ExactCoverBy3Sets,
57                BoundedDiameterSpanningTree<SimpleGraph, i64>,
58            >("computing bounded-diameter incidence construction dimensions")
59        };
60        let vertices = universe_size
61            .checked_add(m)
62            .and_then(|v| v.checked_add(3))
63            .ok_or_else(overflow)?;
64        let clique = match m {
65            0 => 0,
66            _ => m.checked_mul(m - 1).ok_or_else(overflow)? / 2,
67        };
68        let edges = m
69            .checked_mul(4)
70            .and_then(|v| v.checked_add(clique))
71            .and_then(|v| v.checked_add(2))
72            .ok_or_else(overflow)?;
73        let bound = (universe_size / 3)
74            .checked_mul(4)
75            .and_then(|v| v.checked_add(m))
76            .and_then(|v| v.checked_add(2))
77            .and_then(|v| i64::try_from(v).ok())
78            .ok_or_else(overflow)?;
79        Ok((vertices, edges, bound))
80    }
81}
82
83impl ReductionResult for ReductionX3CToBoundedDiameterSpanningTree {
84    type Source = ExactCoverBy3Sets;
85    type Target = BoundedDiameterSpanningTree<SimpleGraph, i64>;
86
87    fn target_problem(&self) -> &Self::Target {
88        &self.target
89    }
90
91    /// Extract the chosen source subsets from the spanning-tree configuration.
92    ///
93    /// The construction places the m root-to-set edges (r, s_i) at edge indices
94    /// 2..2+m (right after the forced-center path edges). For a YES-instance,
95    /// the optimal target witness selects exactly q of these edges, which
96    /// correspond to the q chosen subsets.
97    fn extract_solution(
98        &self,
99        target_solution: &<Self::Target as crate::traits::Problem>::Solution,
100    ) -> crate::rules::ExtractionResult<<Self::Source as crate::traits::Problem>::Solution> {
101        crate::rules::traits::validate_target_witness(
102            self.target_problem(),
103            target_solution,
104            |value| value.0,
105            "target edge selection is not a feasible bounded-diameter spanning tree",
106        )?;
107
108        Ok({
109            let m = self.source_num_subsets;
110            let root_to_set_offset = 2;
111            (0..m)
112                .map(|i| target_solution[root_to_set_offset + i])
113                .collect()
114        })
115    }
116}
117
118#[crate::aggregate_reduction(identity)]
119impl crate::rules::AggregateReductionResult for ReductionX3CToBoundedDiameterSpanningTree {}
120
121#[reduction(
122    transform = upper_bound {
123        num_vertices = "num_subsets + universe_size + 3",
124        num_edges = "2 + 4 * num_subsets + num_subsets * (num_subsets - 1) / 2",
125    })]
126impl ReduceTo<BoundedDiameterSpanningTree<SimpleGraph, i64>> for ExactCoverBy3Sets {
127    type Result = ReductionX3CToBoundedDiameterSpanningTree;
128
129    fn reduce_to(&self) -> Result<Self::Result, crate::rules::ReductionError> {
130        let universe_size = self.universe_size();
131        let m = self.num_subsets();
132        // Every element must occur in the collection before an exact cover can
133        // exist. Count the union without allocating an array of universe_size.
134        let covered: HashSet<_> = self.subsets().iter().flatten().copied().collect();
135        if covered.len() != universe_size {
136            // Two isolated vertices have no spanning tree. No certificate can
137            // pass validation, so the ordinary extractor is never reached.
138            return Ok(ReductionX3CToBoundedDiameterSpanningTree {
139                target: BoundedDiameterSpanningTree::new(SimpleGraph::empty(2), vec![], 1, 4),
140                source_num_subsets: m,
141            });
142        }
143        let (num_vertices, num_edges, weight_bound) =
144            ReductionX3CToBoundedDiameterSpanningTree::dimensions(universe_size, m)?;
145
146        // Vertex indexing matches the docstring.
147        let s_index = |i: usize| 3 + i;
148        let e_index = |j: usize| 3 + m + j;
149
150        let mut edges: Vec<(usize, usize)> = Vec::with_capacity(num_edges);
151        let mut weights: Vec<i64> = Vec::with_capacity(num_edges);
152
153        // Forced-center path edges (indices 0 and 1).
154        edges.push((0, 1)); // (r, v_1)
155        weights.push(1);
156        edges.push((1, 2)); // (v_1, v_2)
157        weights.push(1);
158
159        // Root-to-set edges (indices 2..2+m). The extractor relies on this layout.
160        for i in 0..m {
161            edges.push((0, s_index(i)));
162            weights.push(2);
163        }
164        // Invariant: extract_solution reads edges[2..2 + m] to recover the
165        // selected subsets. If this loop is ever reordered or moved, the
166        // extractor must be updated to match.
167        debug_assert!(
168            (0..m).all(|i| edges[2 + i] == (0, s_index(i))),
169            "root-to-set edges must occupy indices 2..2+m for extract_solution to work"
170        );
171
172        // Set-to-element edges. Subsets are already sorted in `ExactCoverBy3Sets::new`.
173        for (i, subset) in self.subsets().iter().enumerate() {
174            for &j in subset {
175                edges.push((s_index(i), e_index(j)));
176                weights.push(1);
177            }
178        }
179
180        // Set clique edges.
181        for i in 0..m {
182            for j in (i + 1)..m {
183                edges.push((s_index(i), s_index(j)));
184                weights.push(1);
185            }
186        }
187
188        let diameter_bound: usize = 4;
189
190        let graph = SimpleGraph::new(num_vertices, edges);
191        let target = BoundedDiameterSpanningTree::new(graph, weights, weight_bound, diameter_bound);
192
193        Ok(ReductionX3CToBoundedDiameterSpanningTree {
194            target,
195            source_num_subsets: m,
196        })
197    }
198}
199
200#[cfg(feature = "example-db")]
201pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::RuleExampleSpec> {
202    use crate::export::SolutionPair;
203
204    vec![crate::example_db::specs::RuleExampleSpec {
205        id: "exactcoverby3sets_to_boundeddiameterspanningtree",
206        build: || {
207            // q = 2, m = 2: X = {0..5}, C = [{0,1,2}, {3,4,5}].
208            // Exact cover: both subsets. Target has 11 vertices and 11 edges,
209            // so brute-force solving stays well under the 5s test budget.
210            let source = ExactCoverBy3Sets::new(6, vec![[0, 1, 2], [3, 4, 5]]);
211
212            // Source: select both subsets.
213            let source_config = vec![true, true];
214
215            // Target spanning tree (n = 11, n-1 = 10 edges):
216            //   (r,v1)=idx 0, (v1,v2)=idx 1, (r,s0)=idx 2, (r,s1)=idx 3,
217            //   (s0,e0)=idx 4, (s0,e1)=idx 5, (s0,e2)=idx 6,
218            //   (s1,e3)=idx 7, (s1,e4)=idx 8, (s1,e5)=idx 9,
219            //   (s0,s1)=idx 10 (unused clique edge).
220            //
221            // Layout (from `reduce_to`):
222            //   edge order = forced(2) + root-to-set(m=2) + set-to-element(6) + clique(1)
223            //              = indices 0..1, 2..3, 4..9, 10
224            // Select every edge except the clique edge.
225            let target_config = vec![
226                true, true, true, true, true, true, true, true, true, true, false,
227            ];
228
229            crate::example_db::specs::rule_example_with_witness::<
230                _,
231                BoundedDiameterSpanningTree<SimpleGraph, i64>,
232            >(
233                source,
234                SolutionPair {
235                    source_config: serde_json::to_value(source_config)
236                        .expect("solution serialization must succeed"),
237                    target_config: serde_json::to_value(target_config)
238                        .expect("solution serialization must succeed"),
239                },
240            )
241        },
242    }]
243}
244
245#[cfg(test)]
246#[path = "../unit_tests/rules/exactcoverby3sets_boundeddiameterspanningtree.rs"]
247mod tests;