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;