Skip to content

Commit a8ee2fa

Browse files
isPANNclaude
andcommitted
Add rule: ExactCoverBy3Sets -> BoundedDiameterSpanningTree (#913)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 8a747ea commit a8ee2fa

4 files changed

Lines changed: 340 additions & 0 deletions

File tree

docs/paper/reductions.typ

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18608,6 +18608,54 @@ The following table shows concrete variable overhead for example instances, take
1860818608
_Solution extraction._ The X3C configuration equals the Subset Product configuration: select subset $j$ iff $x_j = 1$.
1860918609
]
1861018610

18611+
// ExactCoverBy3Sets → BoundedDiameterSpanningTree (#913)
18612+
#let x3c_bdst = load-example("ExactCoverBy3Sets", "BoundedDiameterSpanningTree")
18613+
#let x3c_bdst_sol = x3c_bdst.solutions.at(0)
18614+
#let x3c_bdst_nv = graph-num-vertices(x3c_bdst.target.instance)
18615+
#let x3c_bdst_ne = graph-num-edges(x3c_bdst.target.instance)
18616+
#reduction-rule("ExactCoverBy3Sets", "BoundedDiameterSpanningTree",
18617+
example: true,
18618+
example-caption: [$|U| = #x3c_bdst.source.instance.universe_size$, $|cal(C)| = #x3c_bdst.source.instance.subsets.len()$ subsets; target $D = #x3c_bdst.target.instance.diameter_bound$, $B = #x3c_bdst.target.instance.weight_bound$],
18619+
extra: [
18620+
#pred-commands(
18621+
"pred create --example " + problem-spec(x3c_bdst.source) + " -o x3c.json",
18622+
"pred reduce x3c.json --to " + target-spec(x3c_bdst) + " -o bundle.json",
18623+
"pred solve bundle.json",
18624+
"pred evaluate x3c.json --config " + x3c_bdst_sol.source_config.map(str).join(","),
18625+
)
18626+
18627+
#let q = x3c_bdst.source.instance.universe_size / 3
18628+
#let m = x3c_bdst.source.instance.subsets.len()
18629+
*Step 1 -- Source instance.* The X3C fixture has universe $U = {0, dots, #(x3c_bdst.source.instance.universe_size - 1)}$ with $q = #q$ and candidate triples
18630+
#for (i, s) in x3c_bdst.source.instance.subsets.enumerate() [
18631+
$C_#i = {#s.map(str).join(", ")}$#if i < m - 1 [, ] else [.]
18632+
]
18633+
18634+
*Step 2 -- Build the spanning-tree gadget.* Create a root $r$, two forced-path vertices $v_1, v_2$, one set vertex $s_i$ per triple, and one element vertex $e_j$ per universe element. The target therefore has $#x3c_bdst_nv = 3 + #m + #(x3c_bdst.source.instance.universe_size)$ vertices and $#x3c_bdst_ne$ weighted edges: the forced path $(r, v_1), (v_1, v_2)$ at weight $1$, the root-to-set edges $(r, s_i)$ at weight $2$, the set-to-element edges $(s_i, e_j)$ for $j in C_i$ at weight $1$, and the set clique $(s_i, s_(i'))$ at weight $1$. The bounds are $D = #x3c_bdst.target.instance.diameter_bound$ and $B = 4q + m + 2 = #x3c_bdst.target.instance.weight_bound$.
18635+
18636+
*Step 3 -- Verify the canonical witness.* The stored source configuration $(#x3c_bdst_sol.source_config.map(str).join(", "))$ selects subsets ${#x3c_bdst_sol.source_config.enumerate().filter(((i, x)) => x == 1).map(((i, x)) => "C_" + str(i)).join(", ")}$. The corresponding tree keeps the forced path, every root-to-set edge for a selected $s_i$, every $(s_i, e_j)$ for $j in C_i$, and one clique edge to attach each remaining set vertex. With $q = #q$ selected sets it has total weight $2 + 2q + 3q + (m - q) = #(2 + 2 * q + 3 * q + m - q) = B$ and every vertex sits within distance $2$ of $r$, so the diameter is at most $4$ #sym.checkmark.
18637+
18638+
*Multiplicity:* The fixture stores one canonical witness. Any feasible spanning tree of weight $B$ and diameter $D = 4$ corresponds to an exact cover via the same extractor, so additional witnesses, when they exist, just enumerate other exact covers.
18639+
],
18640+
)[
18641+
This $O(m^2 + q)$ reduction @garey1979[ND4] embeds X3C into the spanning-tree gadget of Bounded Diameter Spanning Tree. The constructed graph has $3 + m + 3q$ vertices and $2 + 4m + binom(m, 2)$ edges with weights in ${1, 2}$. Setting $D = 4$ and $B = 4q + m + 2$, the BDST instance is feasible if and only if the X3C instance has an exact cover.
18642+
][
18643+
_Construction._ Let the X3C instance be $(U, cal(C))$ with $|U| = 3q$ and $cal(C) = {C_0, dots, C_(m-1)}$. Introduce a root vertex $r$, two forced-path vertices $v_1, v_2$, set vertices $s_0, dots, s_(m-1)$, and element vertices $e_0, dots, e_(3q-1)$. Add the edges
18644+
$
18645+
(r, v_1) " and " (v_1, v_2) " of weight " 1, quad (r, s_i) " of weight " 2 " for every " i,
18646+
$
18647+
$
18648+
(s_i, e_j) " of weight " 1 " whenever " j in C_i, quad (s_i, s_(i')) " of weight " 1 " for all " 0 <= i < i' < m.
18649+
$
18650+
Set the diameter bound $D = 4$ and the weight bound $B = 4q + m + 2$.
18651+
18652+
_Correctness._ ($arrow.r.double$) Let $cal(C)' = {C_(i_1), dots, C_(i_q)}$ be an exact cover. Pick the forced-path edges, the $q$ root-to-set edges for the chosen indices, the $3q$ set-to-element edges that match the cover, and for every unselected set $C_i$ a single clique edge to some chosen set. This is a spanning tree of weight $2 + 2q + 3q + (m - q) = 4q + m + 2 = B$, and every vertex lies within distance $2$ of $r$, so the diameter is at most $4 = D$.
18653+
18654+
($arrow.l.double$) Suppose $T$ is a spanning tree of weight at most $B$ and diameter at most $4$. Because $"dist"_T(r, v_2) = 2$ in any tree containing the forced edges, every other vertex must sit within distance $2$ of $r$; otherwise its distance to $v_2$ would exceed $4$. Element vertices $e_j$ have neighbors only among set vertices, so each $e_j$ is at depth $2$ and connects through some $s_i$ that is directly attached to $r$. Let $k$ be the number of root-to-set edges in $T$. The cheapest way to spawn the remaining $m - k$ set vertices uses clique edges of weight $1$, so the minimum tree weight is $k dot 2 + (m - k) dot 1 + 3q dot 1 + 2 dot 1 = k + m + 3q + 2$. Feasibility forces $k <= q$. Each chosen set covers at most three element vertices, so covering all $3q$ elements requires $k >= q$, hence $k = q$. The $q$ chosen sets contribute exactly $3q$ element attachments, so they must be pairwise disjoint and form an exact cover.
18655+
18656+
_Solution extraction._ The target configuration has one coordinate per edge in the order produced by the construction. The $m$ coordinates indexing the root-to-set edges $(r, s_i)$ are the X3C selection vector: $x_i = 1$ iff $(r, s_i) in T$.
18657+
]
18658+
1861118659
// 8. SubsetSum → IntegerExpressionMembership (#569)
1861218660
#let ss_iem = load-example("SubsetSum", "IntegerExpressionMembership")
1861318661
#let ss_iem_sol = ss_iem.solutions.at(0)
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
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+
34+
use crate::models::graph::BoundedDiameterSpanningTree;
35+
use crate::models::set::ExactCoverBy3Sets;
36+
use crate::reduction;
37+
use crate::rules::traits::{ReduceTo, ReductionResult};
38+
use crate::topology::SimpleGraph;
39+
40+
/// Result of reducing ExactCoverBy3Sets to BoundedDiameterSpanningTree.
41+
#[derive(Debug, Clone)]
42+
pub struct ReductionX3CToBoundedDiameterSpanningTree {
43+
target: BoundedDiameterSpanningTree<SimpleGraph, i32>,
44+
source_num_subsets: usize,
45+
}
46+
47+
impl 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+
})]
84+
impl 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 = universe_size / 3;
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+
112+
// Set-to-element edges. Subsets are already sorted in `ExactCoverBy3Sets::new`.
113+
for (i, subset) in self.subsets().iter().enumerate() {
114+
for &j in subset {
115+
edges.push((s_index(i), e_index(j)));
116+
weights.push(1);
117+
}
118+
}
119+
120+
// Set clique edges.
121+
for i in 0..m {
122+
for j in (i + 1)..m {
123+
edges.push((s_index(i), s_index(j)));
124+
weights.push(1);
125+
}
126+
}
127+
128+
let weight_bound: i32 = (4 * q + m + 2) as i32;
129+
let diameter_bound: usize = 4;
130+
131+
let graph = SimpleGraph::new(num_vertices, edges);
132+
let target = BoundedDiameterSpanningTree::new(graph, weights, weight_bound, diameter_bound);
133+
134+
ReductionX3CToBoundedDiameterSpanningTree {
135+
target,
136+
source_num_subsets: m,
137+
}
138+
}
139+
}
140+
141+
#[cfg(feature = "example-db")]
142+
pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::RuleExampleSpec> {
143+
use crate::export::SolutionPair;
144+
145+
vec![crate::example_db::specs::RuleExampleSpec {
146+
id: "exactcoverby3sets_to_boundeddiameterspanningtree",
147+
build: || {
148+
// q = 2, m = 2: X = {0..5}, C = [{0,1,2}, {3,4,5}].
149+
// Exact cover: both subsets. Target has 11 vertices and 11 edges,
150+
// so brute-force solving stays well under the 5s test budget.
151+
let source = ExactCoverBy3Sets::new(6, vec![[0, 1, 2], [3, 4, 5]]);
152+
153+
// Source: select both subsets.
154+
let source_config = vec![1, 1];
155+
156+
// Target spanning tree (n = 11, n-1 = 10 edges):
157+
// (r,v1)=idx 0, (v1,v2)=idx 1, (r,s0)=idx 2, (r,s1)=idx 3,
158+
// (s0,e0)=idx 4, (s0,e1)=idx 5, (s0,e2)=idx 6,
159+
// (s1,e3)=idx 7, (s1,e4)=idx 8, (s1,e5)=idx 9,
160+
// (s0,s1)=idx 10 (unused clique edge).
161+
//
162+
// Layout (from `reduce_to`):
163+
// edge order = forced(2) + root-to-set(m=2) + set-to-element(6) + clique(1)
164+
// = indices 0..1, 2..3, 4..9, 10
165+
// Select every edge except the clique edge.
166+
let target_config = vec![1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0];
167+
168+
crate::example_db::specs::rule_example_with_witness::<
169+
_,
170+
BoundedDiameterSpanningTree<SimpleGraph, i32>,
171+
>(
172+
source,
173+
SolutionPair {
174+
source_config,
175+
target_config,
176+
},
177+
)
178+
},
179+
}]
180+
}
181+
182+
#[cfg(test)]
183+
#[path = "../unit_tests/rules/exactcoverby3sets_boundeddiameterspanningtree.rs"]
184+
mod tests;

src/rules/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ pub(crate) mod decisionminimumdominatingset_minimumsummulticenter;
1818
pub(crate) mod decisionminimumdominatingset_minmaxmulticenter;
1919
pub(crate) mod decisionminimumvertexcover_hamiltoniancircuit;
2020
pub(crate) mod exactcoverby3sets_algebraicequationsovergf2;
21+
pub(crate) mod exactcoverby3sets_boundeddiameterspanningtree;
2122
pub(crate) mod exactcoverby3sets_maximumsetpacking;
2223
pub(crate) mod exactcoverby3sets_minimumaxiomset;
2324
pub(crate) mod exactcoverby3sets_minimumfaultdetectiontestset;
@@ -415,6 +416,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::Ru
415416
specs.extend(closestvectorproblem_qubo::canonical_rule_example_specs());
416417
specs.extend(coloring_qubo::canonical_rule_example_specs());
417418
specs.extend(exactcoverby3sets_algebraicequationsovergf2::canonical_rule_example_specs());
419+
specs.extend(exactcoverby3sets_boundeddiameterspanningtree::canonical_rule_example_specs());
418420
specs.extend(exactcoverby3sets_minimumfaultdetectiontestset::canonical_rule_example_specs());
419421
specs.extend(exactcoverby3sets_minimumaxiomset::canonical_rule_example_specs());
420422
specs.extend(exactcoverby3sets_subsetproduct::canonical_rule_example_specs());
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
use super::*;
2+
use crate::rules::test_helpers::assert_satisfaction_round_trip_from_satisfaction_target;
3+
use crate::solvers::BruteForce;
4+
use crate::topology::Graph;
5+
use crate::traits::Problem;
6+
use crate::types::Or;
7+
8+
/// q = 2, m = 2: X = {0..5} with C = [{0,1,2}, {3,4,5}].
9+
/// Both subsets together form the unique exact cover.
10+
fn yes_instance_simple() -> ExactCoverBy3Sets {
11+
ExactCoverBy3Sets::new(6, vec![[0, 1, 2], [3, 4, 5]])
12+
}
13+
14+
/// q = 2, m = 2 but the two subsets overlap on element 0,
15+
/// so no exact cover exists.
16+
fn no_instance_simple() -> ExactCoverBy3Sets {
17+
ExactCoverBy3Sets::new(6, vec![[0, 1, 2], [0, 3, 4]])
18+
}
19+
20+
#[test]
21+
fn test_exactcoverby3sets_to_boundeddiameterspanningtree_closed_loop() {
22+
let source = yes_instance_simple();
23+
let reduction = ReduceTo::<BoundedDiameterSpanningTree<SimpleGraph, i32>>::reduce_to(&source);
24+
25+
assert_satisfaction_round_trip_from_satisfaction_target(
26+
&source,
27+
&reduction,
28+
"ExactCoverBy3Sets -> BoundedDiameterSpanningTree closed loop",
29+
);
30+
}
31+
32+
#[test]
33+
fn test_exactcoverby3sets_to_boundeddiameterspanningtree_structure() {
34+
let source = yes_instance_simple();
35+
let reduction = ReduceTo::<BoundedDiameterSpanningTree<SimpleGraph, i32>>::reduce_to(&source);
36+
let target = reduction.target_problem();
37+
38+
let m = source.num_subsets();
39+
let q = source.universe_size() / 3;
40+
// n = 3 + m + 3q
41+
assert_eq!(target.num_vertices(), 3 + m + source.universe_size());
42+
// Expected edge count: 2 forced + m root-to-set + 3m set-to-element + m(m-1)/2 clique.
43+
let expected_edges = 2 + m + 3 * m + m * (m - 1) / 2;
44+
assert_eq!(target.num_edges(), expected_edges);
45+
46+
// Diameter bound is always 4 in the canonical construction.
47+
assert_eq!(target.diameter_bound(), 4);
48+
// Weight bound B = 4q + m + 2.
49+
let expected_weight_bound = (4 * q + m + 2) as i32;
50+
assert_eq!(*target.weight_bound(), expected_weight_bound);
51+
52+
// Verify the first two edges are the forced-center path with weight 1.
53+
let edges = target.graph().edges();
54+
let weights = target.edge_weights();
55+
assert_eq!(edges[0], (0, 1));
56+
assert_eq!(weights[0], 1);
57+
assert_eq!(edges[1], (1, 2));
58+
assert_eq!(weights[1], 1);
59+
60+
// Root-to-set edges follow, at indices 2..2+m, weight 2.
61+
for i in 0..m {
62+
assert_eq!(edges[2 + i], (0, 3 + i));
63+
assert_eq!(weights[2 + i], 2);
64+
}
65+
}
66+
67+
#[test]
68+
fn test_exactcoverby3sets_to_boundeddiameterspanningtree_extract_solution() {
69+
let source = yes_instance_simple();
70+
let reduction = ReduceTo::<BoundedDiameterSpanningTree<SimpleGraph, i32>>::reduce_to(&source);
71+
72+
// Build a target config that selects both root-to-set edges (indices 2 and 3).
73+
// The remaining selections do not matter for extraction.
74+
let mut target_config = vec![0; reduction.target_problem().num_edges()];
75+
target_config[2] = 1;
76+
target_config[3] = 1;
77+
let extracted = reduction.extract_solution(&target_config);
78+
assert_eq!(extracted, vec![1, 1]);
79+
80+
// Only s_0 selected via root edge.
81+
let mut target_config = vec![0; reduction.target_problem().num_edges()];
82+
target_config[2] = 1;
83+
let extracted = reduction.extract_solution(&target_config);
84+
assert_eq!(extracted, vec![1, 0]);
85+
}
86+
87+
#[test]
88+
fn test_exactcoverby3sets_to_boundeddiameterspanningtree_no_instance() {
89+
let source = no_instance_simple();
90+
let reduction = ReduceTo::<BoundedDiameterSpanningTree<SimpleGraph, i32>>::reduce_to(&source);
91+
let target = reduction.target_problem();
92+
93+
// The target should be infeasible: no spanning tree satisfies both weight
94+
// bound B = 4q + m + 2 = 12 and diameter bound D = 4.
95+
let witness = BruteForce::new().find_witness(target);
96+
assert_eq!(
97+
target.evaluate(&witness.clone().unwrap_or_default()),
98+
Or(false)
99+
);
100+
// BruteForce::find_witness returns the lexicographically-first optimal
101+
// assignment; for an Or-valued infeasible target, even that witness
102+
// evaluates to false.
103+
if let Some(w) = witness {
104+
assert!(!target.evaluate(&w).0);
105+
}
106+
}

0 commit comments

Comments
 (0)