problemreductions/rules/threedimensionalmatching_minimumweightdecoding.rs
1//! Reduction from ThreeDimensionalMatching to MinimumWeightDecoding.
2//!
3//! This is the classical Berlekamp–McEliece–van Tilborg (1978) construction
4//! (Garey & Johnson MS7) that establishes NP-hardness of the minimum-weight
5//! codeword problem. Each triple `t_j = (a_j, b_j, c_j)` becomes a column of
6//! a `3q × m` parity-check matrix `H` with exactly three 1s (one per row
7//! block `W`, `X`, `Y`), and the syndrome is the all-ones vector `1^{3q}`.
8//!
9//! **Bridge.** This is a *witness* reduction from `ThreeDimensionalMatching`
10//! (`Value = Or`) to `MinimumWeightDecoding` (`Value = Min<usize>`):
11//!
12//! `source.evaluate(S) == Or(true)` ⇔ `target.evaluate(x) == Min(Some(q))`,
13//!
14//! where `S = { t_j ∈ T : x_j = 1 }`. We rely on the witness-extraction
15//! route `source.evaluate(extract_solution(x))` rather than comparing the
16//! optimum value directly, mirroring `partition_sumofsquarespartition.rs`.
17//!
18//! **Sentinel branch.** `MinimumWeightDecoding::new` panics on zero-row or
19//! zero-column matrices, so degenerate inputs (`q = 0` or `T = []`) emit a
20//! fixed `1×1` sentinel `H = [[1]]` with syndrome `s = [0]`. The unique
21//! feasible codeword `x = (0)` decodes to the empty subset `S = ∅`, and
22//! `source.evaluate(∅)` correctly returns `Or(true)` iff `q = 0`.
23
24use crate::models::algebraic::MinimumWeightDecoding;
25use crate::models::set::ThreeDimensionalMatching;
26use crate::reduction;
27use crate::rules::traits::{ReduceTo, ReductionResult};
28
29/// Result of reducing ThreeDimensionalMatching to MinimumWeightDecoding.
30#[derive(Debug, Clone)]
31pub struct ReductionThreeDimensionalMatchingToMinimumWeightDecoding {
32 target: MinimumWeightDecoding,
33 /// Number of triples in the original 3DM instance.
34 /// Used to return a correctly-sized witness when the sentinel path is
35 /// taken (i.e. `q == 0` or `num_triples == 0`).
36 source_num_triples: usize,
37}
38
39impl ReductionResult for ReductionThreeDimensionalMatchingToMinimumWeightDecoding {
40 type Source = ThreeDimensionalMatching;
41 type Target = MinimumWeightDecoding;
42
43 fn target_problem(&self) -> &Self::Target {
44 &self.target
45 }
46
47 /// The target codeword prefix is the source subset indicator over the same
48 /// triple index set. The sentinel target appends one synthetic column, so
49 /// an empty source maps back to the empty prefix.
50 fn extract_solution(
51 &self,
52 target_solution: &<Self::Target as crate::traits::Problem>::Solution,
53 ) -> crate::rules::ExtractionResult<<Self::Source as crate::traits::Problem>::Solution> {
54 crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?;
55 if target_solution.len() != self.target.num_cols() {
56 return Err(crate::rules::ExtractionError::invalid(format!(
57 "expected {} target codeword bits, got {}",
58 self.target.num_cols(),
59 target_solution.len()
60 )));
61 }
62
63 Ok(target_solution[..self.source_num_triples].to_vec())
64 }
65}
66
67#[reduction(
68 transform = exact {
69 num_rows = "3 * universe_size",
70 num_cols = "num_triples",
71 })]
72impl ReduceTo<MinimumWeightDecoding> for ThreeDimensionalMatching {
73 type Result = ReductionThreeDimensionalMatchingToMinimumWeightDecoding;
74
75 fn reduce_to(&self) -> Result<Self::Result, crate::rules::ReductionError> {
76 let q = self.universe_size();
77 let m = self.num_triples();
78
79 if q == 0 || m == 0 {
80 // Sentinel: MinimumWeightDecoding::new panics on empty matrices
81 // or zero-column matrices. Build a fixed 1×1 instance whose only
82 // feasible codeword is x = (0), decoding to S = ∅. The source's
83 // own evaluate on the empty set gives the correct answer:
84 // q = 0 → Or(true) (empty matching of empty universe)
85 // q ≥ 1 → Or(false) (no triples cannot cover non-empty universe).
86 return Ok(ReductionThreeDimensionalMatchingToMinimumWeightDecoding {
87 target: MinimumWeightDecoding::new(vec![vec![true]], vec![false]),
88 source_num_triples: m,
89 });
90 }
91
92 // Main branch: build H ∈ {0,1}^{3q × m} with row blocks W, X, Y and
93 // one 1 per row block per column at the triple's coordinate.
94 let num_rows = 3 * q;
95 let mut matrix = vec![vec![false; m]; num_rows];
96 for (j, &(a, b, c)) in self.triples().iter().enumerate() {
97 matrix[a][j] = true;
98 matrix[q + b][j] = true;
99 matrix[2 * q + c][j] = true;
100 }
101 let syndrome = vec![true; num_rows];
102
103 Ok(ReductionThreeDimensionalMatchingToMinimumWeightDecoding {
104 target: MinimumWeightDecoding::new(matrix, syndrome),
105 source_num_triples: m,
106 })
107 }
108}
109
110#[cfg(feature = "example-db")]
111pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::RuleExampleSpec> {
112 use crate::export::SolutionPair;
113
114 vec![crate::example_db::specs::RuleExampleSpec {
115 id: "threedimensionalmatching_to_minimumweightdecoding",
116 build: || {
117 // q = 2, T = [(0,0,0), (1,1,1), (0,1,0), (1,0,1)].
118 // Perfect matchings: {t_0, t_1} and {t_2, t_3} -- both attain
119 // target minimum weight = q = 2.
120 crate::example_db::specs::rule_example_with_witness::<_, MinimumWeightDecoding>(
121 ThreeDimensionalMatching::new(2, vec![(0, 0, 0), (1, 1, 1), (0, 1, 0), (1, 0, 1)]),
122 SolutionPair {
123 source_config: serde_json::json!(vec![true, true, false, false]),
124 target_config: serde_json::json!(vec![true, true, false, false]),
125 },
126 )
127 },
128 }]
129}
130
131#[cfg(test)]
132#[path = "../unit_tests/rules/threedimensionalmatching_minimumweightdecoding.rs"]
133mod tests;