Skip to main content

problemreductions/rules/
threedimensionalmatching_threepartition.rs

1//! Reduction from ThreeDimensionalMatching to ThreePartition.
2//!
3//! This follows the classical three-step chain:
4//! 1. 3DM -> ABCD-Partition
5//! 2. ABCD-Partition -> 4-Partition
6//! 3. 4-Partition -> 3-Partition
7
8use crate::models::misc::ThreePartition;
9use crate::models::set::ThreeDimensionalMatching;
10use crate::reduction;
11use crate::rules::traits::{ReduceTo, ReductionResult};
12
13#[derive(Debug, Clone, Copy)]
14enum Step2Item {
15    A {
16        source_triple: usize,
17        w: usize,
18        x: usize,
19        y: usize,
20    },
21    B {
22        w: usize,
23        first_occurrence: bool,
24    },
25    C {
26        x: usize,
27        first_occurrence: bool,
28    },
29    D {
30        y: usize,
31        first_occurrence: bool,
32    },
33}
34
35/// Result of reducing ThreeDimensionalMatching to ThreePartition.
36#[derive(Debug, Clone)]
37pub struct ReductionThreeDimensionalMatchingToThreePartition {
38    target: ThreePartition,
39    step2_items: Vec<Step2Item>,
40    pair_keys: Vec<(usize, usize)>,
41    num_source_triples: usize,
42}
43
44impl ReductionThreeDimensionalMatchingToThreePartition {
45    fn num_regulars(&self) -> usize {
46        self.step2_items.len()
47    }
48
49    fn pairing_start(&self) -> usize {
50        self.num_regulars()
51    }
52
53    fn filler_start(&self) -> usize {
54        self.pairing_start() + 2 * self.pair_keys.len()
55    }
56
57    fn decode_real_group(&self, step2_group: [usize; 4]) -> Option<usize> {
58        let mut a_item = None;
59        let mut b_item = None;
60        let mut c_item = None;
61        let mut d_item = None;
62
63        for step2_index in step2_group {
64            match self.step2_items[step2_index] {
65                Step2Item::A {
66                    source_triple,
67                    w,
68                    x,
69                    y,
70                } => {
71                    a_item = Some((source_triple, w, x, y));
72                }
73                Step2Item::B {
74                    w,
75                    first_occurrence,
76                } => {
77                    b_item = Some((w, first_occurrence));
78                }
79                Step2Item::C {
80                    x,
81                    first_occurrence,
82                } => {
83                    c_item = Some((x, first_occurrence));
84                }
85                Step2Item::D {
86                    y,
87                    first_occurrence,
88                } => {
89                    d_item = Some((y, first_occurrence));
90                }
91            }
92        }
93
94        let (source_triple, aw, ax, ay) = a_item?;
95        let (bw, b_first) = b_item?;
96        let (cx, c_first) = c_item?;
97        let (dy, d_first) = d_item?;
98
99        if aw != bw || ax != cx || ay != dy {
100            return None;
101        }
102
103        if b_first && c_first && d_first {
104            Some(source_triple)
105        } else {
106            None
107        }
108    }
109
110    #[cfg(test)]
111    fn build_target_witness(&self, source_solution: &[usize]) -> Vec<usize> {
112        use std::collections::HashMap;
113
114        let mut a_indices = vec![0usize; self.num_source_triples];
115        let mut first_b_by_w = HashMap::new();
116        let mut first_c_by_x = HashMap::new();
117        let mut first_d_by_y = HashMap::new();
118        let mut dummy_bs_by_w: HashMap<usize, Vec<usize>> = HashMap::new();
119        let mut dummy_cs_by_x: HashMap<usize, Vec<usize>> = HashMap::new();
120        let mut dummy_ds_by_y: HashMap<usize, Vec<usize>> = HashMap::new();
121
122        for (step2_index, item) in self.step2_items.iter().copied().enumerate() {
123            match item {
124                Step2Item::A { source_triple, .. } => {
125                    a_indices[source_triple] = step2_index;
126                }
127                Step2Item::B {
128                    w,
129                    first_occurrence,
130                } => {
131                    if first_occurrence {
132                        first_b_by_w.insert(w, step2_index);
133                    } else {
134                        dummy_bs_by_w.entry(w).or_default().push(step2_index);
135                    }
136                }
137                Step2Item::C {
138                    x,
139                    first_occurrence,
140                } => {
141                    if first_occurrence {
142                        first_c_by_x.insert(x, step2_index);
143                    } else {
144                        dummy_cs_by_x.entry(x).or_default().push(step2_index);
145                    }
146                }
147                Step2Item::D {
148                    y,
149                    first_occurrence,
150                } => {
151                    if first_occurrence {
152                        first_d_by_y.insert(y, step2_index);
153                    } else {
154                        dummy_ds_by_y.entry(y).or_default().push(step2_index);
155                    }
156                }
157            }
158        }
159
160        let mut step2_groups = Vec::with_capacity(self.num_source_triples);
161        for source_triple in 0..self.num_source_triples {
162            let Step2Item::A { w, x, y, .. } = self.step2_items[a_indices[source_triple]] else {
163                unreachable!("A indices are populated from A items");
164            };
165
166            let group = if source_solution[source_triple] == 1 {
167                [
168                    a_indices[source_triple],
169                    *first_b_by_w
170                        .get(&w)
171                        .expect("selected triple must have a first-occurrence B item"),
172                    *first_c_by_x
173                        .get(&x)
174                        .expect("selected triple must have a first-occurrence C item"),
175                    *first_d_by_y
176                        .get(&y)
177                        .expect("selected triple must have a first-occurrence D item"),
178                ]
179            } else {
180                [
181                    a_indices[source_triple],
182                    dummy_bs_by_w
183                        .get_mut(&w)
184                        .and_then(|items| items.pop())
185                        .expect("unselected triple must have a dummy B item"),
186                    dummy_cs_by_x
187                        .get_mut(&x)
188                        .and_then(|items| items.pop())
189                        .expect("unselected triple must have a dummy C item"),
190                    dummy_ds_by_y
191                        .get_mut(&y)
192                        .and_then(|items| items.pop())
193                        .expect("unselected triple must have a dummy D item"),
194                ]
195            };
196
197            step2_groups.push(group);
198        }
199
200        let pair_to_index: HashMap<(usize, usize), usize> = self
201            .pair_keys
202            .iter()
203            .copied()
204            .enumerate()
205            .map(|(pair_index, pair)| (pair, pair_index))
206            .collect();
207        let mut pair_used = vec![false; self.pair_keys.len()];
208        let mut target_solution = vec![0usize; self.target.num_elements()];
209        let mut next_group = 0usize;
210
211        for mut step2_group in step2_groups {
212            step2_group.sort_unstable();
213            let pair_key = (step2_group[0], step2_group[1]);
214            let pair_index = *pair_to_index
215                .get(&pair_key)
216                .expect("chosen regular pair must exist in the pairing gadget");
217            pair_used[pair_index] = true;
218
219            let u_index = self.pairing_start() + 2 * pair_index;
220            let uprime_index = u_index + 1;
221
222            target_solution[step2_group[0]] = next_group;
223            target_solution[step2_group[1]] = next_group;
224            target_solution[u_index] = next_group;
225            next_group += 1;
226
227            target_solution[step2_group[2]] = next_group;
228            target_solution[step2_group[3]] = next_group;
229            target_solution[uprime_index] = next_group;
230            next_group += 1;
231        }
232
233        let mut filler_index = self.filler_start();
234        for (pair_index, used) in pair_used.into_iter().enumerate() {
235            if used {
236                continue;
237            }
238
239            let u_index = self.pairing_start() + 2 * pair_index;
240            let uprime_index = u_index + 1;
241            target_solution[u_index] = next_group;
242            target_solution[uprime_index] = next_group;
243            target_solution[filler_index] = next_group;
244            filler_index += 1;
245            next_group += 1;
246        }
247
248        assert_eq!(filler_index, self.target.num_elements());
249        assert_eq!(next_group, self.target.num_groups());
250
251        target_solution
252    }
253}
254
255impl ReductionResult for ReductionThreeDimensionalMatchingToThreePartition {
256    type Source = ThreeDimensionalMatching;
257    type Target = ThreePartition;
258
259    fn target_problem(&self) -> &Self::Target {
260        &self.target
261    }
262
263    /// Reverse the 4-Partition -> 3-Partition pairing gadget, then decode the
264    /// surviving real ABCD groups back into selected source triples.
265    fn extract_solution(
266        &self,
267        target_solution: &<Self::Target as crate::traits::Problem>::Solution,
268    ) -> crate::rules::ExtractionResult<<Self::Source as crate::traits::Problem>::Solution> {
269        crate::rules::traits::validate_target_witness(
270            self.target_problem(),
271            target_solution,
272            |value| value.0,
273            "target assignment is not a feasible 3-partition",
274        )?;
275
276        if self.num_source_triples == 0 {
277            return Ok(Vec::new());
278        }
279        let mut groups = vec![Vec::with_capacity(3); self.target.num_groups()];
280        let mut positions = Vec::with_capacity(target_solution.len());
281        for (element, &group) in target_solution.iter().enumerate() {
282            positions.push((group, groups[group].len()));
283            groups[group].push(element);
284        }
285
286        // Garey--Johnson's reverse construction first normalizes filler triples.
287        // Each has two pairing elements whose sum equals that of an original
288        // U/UPrime pair. Exchange the second element with the first's mate:
289        // they have equal sizes, so both affected triples remain valid. A mate
290        // cannot belong to an already normalized filler triple unless it is
291        // already here, so each iteration permanently normalizes one triple.
292        // Initial index order puts regulars first and fillers last; exchanges
293        // only move pairing elements, preserving those positions.
294        let pairing_start = self.pairing_start();
295        for filler in self.filler_start()..target_solution.len() {
296            let (group, _) = positions[filler];
297            let first = groups[group][0];
298            let second = groups[group][1];
299            let mate = pairing_start + ((first - pairing_start) ^ 1);
300            let (mate_group, mate_slot) = positions[mate];
301            groups[group][1] = mate;
302            groups[mate_group][mate_slot] = second;
303            positions[mate] = (group, 1);
304            positions[second] = (mate_group, mate_slot);
305        }
306
307        let mut source_solution = vec![false; self.num_source_triples];
308        for first in (pairing_start..self.filler_start()).step_by(2) {
309            let (left, _) = positions[first];
310            if groups[left][0] >= pairing_start {
311                continue; // This complete pair is used by a filler triple.
312            }
313            let (right, _) = positions[first + 1];
314            // Use the actual regular elements, not the pair's construction
315            // indices: equal-valued pairing elements are interchangeable.
316            let regulars = [
317                groups[left][0],
318                groups[left][1],
319                groups[right][0],
320                groups[right][1],
321            ];
322            if let Some(source_triple) = self.decode_real_group(regulars) {
323                source_solution[source_triple] = true;
324            }
325        }
326        Ok(source_solution)
327    }
328}
329
330fn enumerate_pair_keys(num_regulars: usize) -> Option<Vec<(usize, usize)>> {
331    let capacity = num_regulars
332        .checked_mul(num_regulars.saturating_sub(1))
333        .and_then(|value| value.checked_div(2))?;
334    let mut pairs = Vec::with_capacity(capacity);
335    for left in 0..num_regulars {
336        for right in left + 1..num_regulars {
337            pairs.push((left, right));
338        }
339    }
340    Some(pairs)
341}
342
343#[crate::aggregate_reduction(identity)]
344impl crate::rules::AggregateReductionResult for ReductionThreeDimensionalMatchingToThreePartition {}
345
346#[reduction(
347    transform = upper_bound {
348        // B=64*(16*40*(32q)^4+15)+4; fixed YES/NO bounds are 3/20.
349        bound = "40960 * (32 * universe_size)^4 + 964",
350        max_numeric_magnitude_bits = "4 * universe_size + 36",
351        num_elements = "24 * num_triples * num_triples - 3 * num_triples + 6",
352        num_groups = "8 * num_triples * num_triples - num_triples + 2",
353    })]
354impl ReduceTo<ThreePartition> for ThreeDimensionalMatching {
355    type Result = ReductionThreeDimensionalMatchingToThreePartition;
356
357    fn reduce_to(&self) -> Result<Self::Result, crate::rules::ReductionError> {
358        let q = self.universe_size();
359        let t = self.num_triples();
360
361        if q == 0 {
362            // The empty matching covers the empty universe.
363            return Ok(ReductionThreeDimensionalMatchingToThreePartition {
364                target: ThreePartition::new(vec![1, 1, 1], 3),
365                step2_items: Vec::new(),
366                pair_keys: Vec::new(),
367                num_source_triples: 0,
368            });
369        }
370
371        let mut covered_w = vec![false; q];
372        let mut covered_x = vec![false; q];
373        let mut covered_y = vec![false; q];
374        for &(w, x, y) in self.triples() {
375            covered_w[w] = true;
376            covered_x[x] = true;
377            covered_y[y] = true;
378        }
379        if covered_w.iter().any(|&covered| !covered)
380            || covered_x.iter().any(|&covered| !covered)
381            || covered_y.iter().any(|&covered| !covered)
382        {
383            return Ok(ReductionThreeDimensionalMatchingToThreePartition {
384                target: ThreePartition::new(vec![6, 6, 6, 6, 7, 9], 20),
385                step2_items: Vec::new(),
386                pair_keys: Vec::new(),
387                num_source_triples: t,
388            });
389        }
390
391        let arithmetic_overflow = |context| {
392            crate::rules::ReductionError::integer_overflow::<ThreeDimensionalMatching, ThreePartition>(
393                context,
394            )
395        };
396        let q = i64::try_from(q).map_err(|_| arithmetic_overflow("converting q to i64"))?;
397        let r = 32_i64
398            .checked_mul(q)
399            .ok_or_else(|| arithmetic_overflow("computing r = 32q"))?;
400        let r2 = r
401            .checked_mul(r)
402            .ok_or_else(|| arithmetic_overflow("computing r^2"))?;
403        let r3 = r2
404            .checked_mul(r)
405            .ok_or_else(|| arithmetic_overflow("computing r^3"))?;
406        let r4 = r3
407            .checked_mul(r)
408            .ok_or_else(|| arithmetic_overflow("computing r^4"))?;
409        let target1 = 40_i64
410            .checked_mul(r4)
411            .ok_or_else(|| arithmetic_overflow("computing the ABCD-Partition target"))?;
412
413        let mut step2_items = Vec::with_capacity(4 * t);
414        let mut step2_values = Vec::with_capacity(4 * t);
415
416        let mut seen_w = std::collections::HashSet::new();
417        let mut seen_x = std::collections::HashSet::new();
418        let mut seen_y = std::collections::HashSet::new();
419
420        for (source_triple, &(w, x, y)) in self.triples().iter().enumerate() {
421            let w_num = i64::try_from(w).map_err(|_| arithmetic_overflow("converting w to i64"))?;
422            let x_num = i64::try_from(x).map_err(|_| arithmetic_overflow("converting x to i64"))?;
423            let y_num = i64::try_from(y).map_err(|_| arithmetic_overflow("converting y to i64"))?;
424
425            let a_value = 10_i64
426                .checked_mul(r4)
427                .and_then(|value| value.checked_sub(y_num.checked_mul(r3)?))
428                .and_then(|value| value.checked_sub(x_num.checked_mul(r2)?))
429                .and_then(|value| value.checked_sub(w_num.checked_mul(r)?))
430                .ok_or_else(|| arithmetic_overflow("computing an A item"))?;
431            let step2_a = 16_i64
432                .checked_mul(a_value)
433                .and_then(|value| value.checked_add(1))
434                .ok_or_else(|| arithmetic_overflow("encoding an A item"))?;
435            step2_values.push(step2_a);
436            step2_items.push(Step2Item::A {
437                source_triple,
438                w,
439                x,
440                y,
441            });
442
443            let w_first = seen_w.insert(w);
444            let b_digit: i64 = if w_first { 10 } else { 11 };
445            let b_value = b_digit
446                .checked_mul(r4)
447                .and_then(|value| value.checked_add(w_num.checked_mul(r)?))
448                .ok_or_else(|| arithmetic_overflow("computing a B item"))?;
449            let step2_b = 16_i64
450                .checked_mul(b_value)
451                .and_then(|value| value.checked_add(2))
452                .ok_or_else(|| arithmetic_overflow("encoding a B item"))?;
453            step2_values.push(step2_b);
454            step2_items.push(Step2Item::B {
455                w,
456                first_occurrence: w_first,
457            });
458
459            let x_first = seen_x.insert(x);
460            let c_digit: i64 = if x_first { 10 } else { 11 };
461            let c_value = c_digit
462                .checked_mul(r4)
463                .and_then(|value| value.checked_add(x_num.checked_mul(r2)?))
464                .ok_or_else(|| arithmetic_overflow("computing a C item"))?;
465            let step2_c = 16_i64
466                .checked_mul(c_value)
467                .and_then(|value| value.checked_add(4))
468                .ok_or_else(|| arithmetic_overflow("encoding a C item"))?;
469            step2_values.push(step2_c);
470            step2_items.push(Step2Item::C {
471                x,
472                first_occurrence: x_first,
473            });
474
475            let y_first = seen_y.insert(y);
476            let d_digit: i64 = if y_first { 10 } else { 8 };
477            let d_value = d_digit
478                .checked_mul(r4)
479                .and_then(|value| value.checked_add(y_num.checked_mul(r3)?))
480                .ok_or_else(|| arithmetic_overflow("computing a D item"))?;
481            let step2_d = 16_i64
482                .checked_mul(d_value)
483                .and_then(|value| value.checked_add(8))
484                .ok_or_else(|| arithmetic_overflow("encoding a D item"))?;
485            step2_values.push(step2_d);
486            step2_items.push(Step2Item::D {
487                y,
488                first_occurrence: y_first,
489            });
490        }
491
492        let target2 = 16_i64
493            .checked_mul(target1)
494            .and_then(|value| value.checked_add(15))
495            .ok_or_else(|| arithmetic_overflow("computing the 4-Partition target"))?;
496        let pair_keys = enumerate_pair_keys(step2_values.len())
497            .ok_or_else(|| arithmetic_overflow("computing the 4-Partition pair count"))?;
498
499        let subtract_fillers =
500            3usize.checked_mul(t).ok_or_else(|| {
501                crate::rules::ReductionError::integer_overflow::<
502                    ThreeDimensionalMatching,
503                    ThreePartition,
504                >("computing the filler count")
505            })?;
506        let num_fillers = 8usize
507            .checked_mul(t)
508            .and_then(|value| value.checked_mul(t))
509            .and_then(|value| value.checked_sub(subtract_fillers))
510            .ok_or_else(|| {
511                crate::rules::ReductionError::integer_overflow::<
512                    ThreeDimensionalMatching,
513                    ThreePartition,
514                >("computing the filler count")
515            })?;
516
517        let pair_elements =
518            2usize.checked_mul(pair_keys.len()).ok_or_else(|| {
519                crate::rules::ReductionError::integer_overflow::<
520                    ThreeDimensionalMatching,
521                    ThreePartition,
522                >("computing the number of pair elements")
523            })?;
524        let total_elements = step2_values
525            .len()
526            .checked_add(pair_elements)
527            .and_then(|value| value.checked_add(num_fillers))
528            .ok_or_else(|| {
529                crate::rules::ReductionError::integer_overflow::<
530                    ThreeDimensionalMatching,
531                    ThreePartition,
532                >("computing the target element count")
533            })?;
534
535        let mut sizes = Vec::with_capacity(total_elements);
536
537        for &step2_value in &step2_values {
538            let regular = 5_i64
539                .checked_mul(target2)
540                .and_then(|value| value.checked_add(step2_value))
541                .and_then(|value| value.checked_mul(4))
542                .and_then(|value| value.checked_add(1))
543                .ok_or_else(|| arithmetic_overflow("computing a regular element"))?;
544            sizes.push(regular);
545        }
546
547        for &(left, right) in &pair_keys {
548            let pair_sum = step2_values[left]
549                .checked_add(step2_values[right])
550                .ok_or_else(|| arithmetic_overflow("summing paired 4-Partition elements"))?;
551            let u_value = 6_i64
552                .checked_mul(target2)
553                .and_then(|value| value.checked_sub(pair_sum))
554                .and_then(|value| value.checked_mul(4))
555                .and_then(|value| value.checked_add(2))
556                .ok_or_else(|| arithmetic_overflow("computing a pairing u element"))?;
557            sizes.push(u_value);
558
559            let uprime_value = 5_i64
560                .checked_mul(target2)
561                .and_then(|value| value.checked_add(pair_sum))
562                .and_then(|value| value.checked_mul(4))
563                .and_then(|value| value.checked_add(2))
564                .ok_or_else(|| arithmetic_overflow("computing a pairing u' element"))?;
565            sizes.push(uprime_value);
566        }
567
568        let filler_value = 20_i64
569            .checked_mul(target2)
570            .ok_or_else(|| arithmetic_overflow("computing a filler element"))?;
571        sizes.extend(std::iter::repeat_n(filler_value, num_fillers));
572
573        let bound = 64_i64
574            .checked_mul(target2)
575            .and_then(|value| value.checked_add(4))
576            .ok_or_else(|| arithmetic_overflow("computing the 3-Partition bound"))?;
577
578        Ok(ReductionThreeDimensionalMatchingToThreePartition {
579            target: ThreePartition::try_new(sizes, bound)
580                .map_err(<Self as ReduceTo<ThreePartition>>::target_construction)?,
581            step2_items,
582            pair_keys,
583            num_source_triples: t,
584        })
585    }
586}
587
588#[cfg(feature = "example-db")]
589pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::RuleExampleSpec> {
590    use crate::export::SolutionPair;
591
592    vec![crate::example_db::specs::RuleExampleSpec {
593        id: "threedimensionalmatching_to_threepartition",
594        build: || {
595            crate::example_db::specs::rule_example_with_witness::<_, ThreePartition>(
596                ThreeDimensionalMatching::new(1, vec![(0, 0, 0)]),
597                SolutionPair {
598                    source_config: serde_json::json!(vec![true]),
599                    target_config: serde_json::json!(vec![
600                        0, 0, 1, 1, 0, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 2, 3, 4, 5, 6,
601                    ]),
602                },
603            )
604        },
605    }]
606}
607
608#[cfg(test)]
609#[path = "../unit_tests/rules/threedimensionalmatching_threepartition.rs"]
610mod tests;