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;
39
40/// Result of reducing ExactCoverBy3Sets to BoundedDiameterSpanningTree.
41#[derive(Debug, Clone)]
42pub struct ReductionX3CToBoundedDiameterSpanningTree {
43    target: BoundedDiameterSpanningTree<SimpleGraph, i32>,
44    source_num_subsets: usize,
45}
46
47impl ReductionResult for ReductionX3CToBoundedDiameterSpanningTree {
48    type Source = ExactCoverBy3Sets;
49    type Target = BoundedDiameterSpanningTree<SimpleGraph, i32>;
50
51    fn target_problem(&self) -> &Self::Target {
52        &self.target
53    }
54
55    /// Extract the chosen source subsets from the spanning-tree configuration.
56    ///
57    /// The construction places the m root-to-set edges (r, s_i) at edge indices
58    /// 2..2+m (right after the forced-center path edges). For a YES-instance,
59    /// the optimal target witness selects exactly q of these edges, which
60    /// correspond to the q chosen subsets.
61    fn extract_solution(&self, target_solution: &[usize]) -> Vec<usize> {
62        let m = self.source_num_subsets;
63        let root_to_set_offset = 2;
64        (0..m)
65            .map(|i| {
66                usize::from(
67                    target_solution
68                        .get(root_to_set_offset + i)
69                        .copied()
70                        .unwrap_or(0)
71                        == 1,
72                )
73            })
74            .collect()
75    }
76}
77
78#[reduction(overhead = {
79    num_vertices = "num_subsets + universe_size + 3",
80    num_edges = "2 + 4 * num_subsets + num_subsets * (num_subsets - 1) / 2",
81    weight_bound = "4 * universe_size / 3 + num_subsets + 2",
82    diameter_bound = "4",
83})]
84impl ReduceTo<BoundedDiameterSpanningTree<SimpleGraph, i32>> for ExactCoverBy3Sets {
85    type Result = ReductionX3CToBoundedDiameterSpanningTree;
86
87    fn reduce_to(&self) -> Self::Result {
88        let universe_size = self.universe_size();
89        let m = self.num_subsets();
90        let q = self.q();
91
92        // Vertex indexing matches the docstring.
93        let s_index = |i: usize| 3 + i;
94        let e_index = |j: usize| 3 + m + j;
95        let num_vertices = 3 + m + universe_size;
96
97        let mut edges: Vec<(usize, usize)> = Vec::new();
98        let mut weights: Vec<i32> = Vec::new();
99
100        // Forced-center path edges (indices 0 and 1).
101        edges.push((0, 1)); // (r, v_1)
102        weights.push(1);
103        edges.push((1, 2)); // (v_1, v_2)
104        weights.push(1);
105
106        // Root-to-set edges (indices 2..2+m). The extractor relies on this layout.
107        for i in 0..m {
108            edges.push((0, s_index(i)));
109            weights.push(2);
110        }
111        // Invariant: extract_solution reads edges[2..2 + m] to recover the
112        // selected subsets. If this loop is ever reordered or moved, the
113        // extractor must be updated to match.
114        debug_assert!(
115            (0..m).all(|i| edges[2 + i] == (0, s_index(i))),
116            "root-to-set edges must occupy indices 2..2+m for extract_solution to work"
117        );
118
119        // Set-to-element edges. Subsets are already sorted in `ExactCoverBy3Sets::new`.
120        for (i, subset) in self.subsets().iter().enumerate() {
121            for &j in subset {
122                edges.push((s_index(i), e_index(j)));
123                weights.push(1);
124            }
125        }
126
127        // Set clique edges.
128        for i in 0..m {
129            for j in (i + 1)..m {
130                edges.push((s_index(i), s_index(j)));
131                weights.push(1);
132            }
133        }
134
135        let weight_bound: i32 = (4 * q + m + 2) as i32;
136        let diameter_bound: usize = 4;
137
138        let graph = SimpleGraph::new(num_vertices, edges);
139        let target = BoundedDiameterSpanningTree::new(graph, weights, weight_bound, diameter_bound);
140
141        ReductionX3CToBoundedDiameterSpanningTree {
142            target,
143            source_num_subsets: m,
144        }
145    }
146}
147
148#[cfg(feature = "example-db")]
149pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::RuleExampleSpec> {
150    use crate::export::SolutionPair;
151
152    vec![crate::example_db::specs::RuleExampleSpec {
153        id: "exactcoverby3sets_to_boundeddiameterspanningtree",
154        build: || {
155            // q = 2, m = 2: X = {0..5}, C = [{0,1,2}, {3,4,5}].
156            // Exact cover: both subsets. Target has 11 vertices and 11 edges,
157            // so brute-force solving stays well under the 5s test budget.
158            let source = ExactCoverBy3Sets::new(6, vec![[0, 1, 2], [3, 4, 5]]);
159
160            // Source: select both subsets.
161            let source_config = vec![1, 1];
162
163            // Target spanning tree (n = 11, n-1 = 10 edges):
164            //   (r,v1)=idx 0, (v1,v2)=idx 1, (r,s0)=idx 2, (r,s1)=idx 3,
165            //   (s0,e0)=idx 4, (s0,e1)=idx 5, (s0,e2)=idx 6,
166            //   (s1,e3)=idx 7, (s1,e4)=idx 8, (s1,e5)=idx 9,
167            //   (s0,s1)=idx 10 (unused clique edge).
168            //
169            // Layout (from `reduce_to`):
170            //   edge order = forced(2) + root-to-set(m=2) + set-to-element(6) + clique(1)
171            //              = indices 0..1, 2..3, 4..9, 10
172            // Select every edge except the clique edge.
173            let target_config = vec![1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0];
174
175            crate::example_db::specs::rule_example_with_witness::<
176                _,
177                BoundedDiameterSpanningTree<SimpleGraph, i32>,
178            >(
179                source,
180                SolutionPair {
181                    source_config,
182                    target_config,
183                },
184            )
185        },
186    }]
187}
188
189#[cfg(test)]
190#[path = "../unit_tests/rules/exactcoverby3sets_boundeddiameterspanningtree.rs"]
191mod tests;