Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions docs/paper/reductions.typ
Original file line number Diff line number Diff line change
Expand Up @@ -19165,6 +19165,57 @@ The following table shows concrete variable overhead for example instances, take
#let tdm_tp_sol = tdm_tp.solutions.at(0)
#let tdm_tmi = load-example("ThreeDimensionalMatching", "ThreeMatroidIntersection")
#let tdm_tmi_sol = tdm_tmi.solutions.at(0)
#let tdm_x3c = load-example("ThreeDimensionalMatching", "ExactCoverBy3Sets")
#let tdm_x3c_sol = tdm_x3c.solutions.at(0)
#reduction-rule("ThreeDimensionalMatching", "ExactCoverBy3Sets",
example: true,
example-caption: [$q = #tdm_x3c.source.instance.universe_size$, #tdm_x3c.source.instance.triples.len() triples $arrow.r$ #tdm_x3c.target.instance.subsets.len() tagged 3-sets],
extra: [
#pred-commands(
"pred create --example " + problem-spec(tdm_x3c.source) + " -o three-dimensional-matching.json",
"pred reduce three-dimensional-matching.json --to " + target-spec(tdm_x3c) + " -o bundle.json",
"pred solve bundle.json",
"pred evaluate three-dimensional-matching.json --config " + tdm_x3c_sol.source_config.map(str).join(","),
)

#{
let q = tdm_x3c.source.instance.universe_size
let triples = tdm_x3c.source.instance.triples
let subsets = tdm_x3c.target.instance.subsets
let witness-indices = tdm_x3c_sol.source_config.enumerate().filter(((i, x)) => x == 1).map(((i, x)) => i)
let witness-elements = witness-indices.map(i => subsets.at(i)).flatten().sorted()
let cross-indices = (triples.len() - 2, triples.len() - 1)
let cross-elements = cross-indices.map(i => subsets.at(i)).flatten().sorted()
let uncovered = range(3 * q).filter(element => not cross-elements.contains(element))
[
*Step 1 -- Read the indexed 3DM instance.* The three coordinate domains each have size $q = #q$. The fixture's indexed triple list is #triples.enumerate().map(((i, triple)) => "$t_" + str(i) + " = (" + triple.map(str).join(", ") + ")$").join([; ]).

*Step 2 -- Tag the coordinate domains.* Put the first, second, and third coordinates in disjoint numeric blocks $[0, q)$, $[q, 2q)$, and $[2q, 3q)$. Thus triple $t_j = (a_j, b_j, c_j)$ becomes $S_j = {a_j, q + b_j, 2q + c_j}$. The exact five target subsets are #subsets.enumerate().map(((i, subset)) => "$S_" + str(i) + " = {" + subset.map(str).join(", ") + "}$").join([; ]). The target therefore has universe size $#tdm_x3c.target.instance.universe_size = 3 q$ and one subset per source triple.

*Step 3 -- Verify the diagonal witness end-to-end.* The canonical source configuration is $(#tdm_x3c_sol.source_config.map(str).join(", "))$, selecting triple indices $#witness-indices.map(str).join(", ")$. Their target subsets have sorted union ${#witness-elements.map(str).join(", ")}$, which is every element of the tagged universe exactly once. The target configuration is the identical vector $(#tdm_x3c_sol.target_config.map(str).join(", "))$, so exact-cover feasibility maps back to the diagonal perfect matching #sym.checkmark.

*Step 4 -- Understand the cross triples.* The final two triples map to $S_#cross-indices.at(0) = {#subsets.at(cross-indices.at(0)).map(str).join(", ")}$ and $S_#cross-indices.at(1) = {#subsets.at(cross-indices.at(1)).map(str).join(", ")}$. These two sets are mutually disjoint, but together they leave ${#uncovered.map(str).join(", ")}$ uncovered. None of the five available subsets is contained in that remaining three-element set, so the pair cannot be extended by any available third triple to an exact cover.

*Multiplicity:* The fixture stores one canonical witness.
]
}
],
)[
This $O(q + t)$ reduction @karp1972 @garey1979[SP1--SP2] embeds the three coordinate domains of a Three-Dimensional Matching instance into three disjoint tagged blocks. For $t$ indexed source triples it constructs an Exact Cover by 3-Sets instance with exactly $3q$ universe elements and $t$ subsets (hence $t$ subset variables).
][
_Construction._ Let $q in NN$ and let the source contain the indexed list $T = (t_0, dots, t_(t - 1))$, where $t_j = (a_j, b_j, c_j) in W times X times Y$ and $W = X = Y = {0, dots, q - 1}$. Form the disjoint tagged blocks
$ W' = {0, dots, q - 1}, quad X' = {q, dots, 2q - 1}, quad Y' = {2q, dots, 3q - 1}, $
and target universe $U' = W' union X' union Y'$. For every indexed triple $t_j$, create the three-element target subset
$ S_j = {a_j, q + b_j, 2q + c_j}. $
The target family is the indexed list $(S_0, dots, S_(t - 1))$; in particular, duplicate source triples remain distinct subset variables. The exact target sizes are $|U'| = 3q$ and $|cal(S)| = t$.

_Correctness._ ($arrow.r.double$) Suppose source indices $J subset.eq {0, dots, t - 1}$ form a perfect three-dimensional matching. Every coordinate value occurs in exactly one selected triple. Therefore every element of $W'$, $X'$, and $Y'$ occurs in exactly one set $S_j$ with $j in J$. The selected sets are pairwise disjoint and their union is $U'$, so they form an exact cover.

($arrow.l.double$) Conversely, suppose target indices $J$ select an exact cover of $U'$. Each $S_j$ contains exactly one element from each of the three tagged blocks. Exact coverage of $W'$ implies that the first coordinates of the selected triples contain every value in $W$ exactly once; exact coverage of $X'$ and $Y'$ gives the same conclusion for the second and third coordinates. Hence the source triples indexed by $J$ are pairwise coordinate-disjoint and cover all three domains, so they form a perfect three-dimensional matching.

_Solution extraction._ Return the target's binary subset-indicator vector unchanged: target variable $j$ and source variable $j$ both refer to the same indexed triple.
]

#reduction-rule("ThreeDimensionalMatching", "ThreeMatroidIntersection",
example: true,
example-caption: [$q = 3$, $t = 5$ triples],
Expand Down
2 changes: 2 additions & 0 deletions src/rules/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ pub(crate) mod subsetsum_integerknapsack;
pub(crate) mod subsetsum_partition;
#[cfg(test)]
pub(crate) mod test_helpers;
pub(crate) mod threedimensionalmatching_exactcoverby3sets;
pub(crate) mod threedimensionalmatching_minimumweightdecoding;
pub(crate) mod threedimensionalmatching_threematroidintersection;
pub(crate) mod threedimensionalmatching_threepartition;
Expand Down Expand Up @@ -522,6 +523,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::Ru
specs.extend(maxcut_minimumcutintoboundedsets::canonical_rule_example_specs());
specs.extend(maxcut_minimummatrixcover::canonical_rule_example_specs());
specs.extend(partition_binpacking::canonical_rule_example_specs());
specs.extend(threedimensionalmatching_exactcoverby3sets::canonical_rule_example_specs());
specs.extend(threedimensionalmatching_minimumweightdecoding::canonical_rule_example_specs());
specs.extend(threedimensionalmatching_threematroidintersection::canonical_rule_example_specs());
specs.extend(threedimensionalmatching_threepartition::canonical_rule_example_specs());
Expand Down
75 changes: 75 additions & 0 deletions src/rules/threedimensionalmatching_exactcoverby3sets.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
//! Reduction from ThreeDimensionalMatching to ExactCoverBy3Sets.
//!
//! Each coordinate domain is embedded into its own tagged block of a
//! `3q`-element universe. Thus `(w, x, y)` becomes
//! `{w, q + x, 2q + y}`, preserving the source triple index.

use crate::models::set::{ExactCoverBy3Sets, ThreeDimensionalMatching};
use crate::reduction;
use crate::rules::traits::{ReduceTo, ReductionResult};

/// Result of reducing ThreeDimensionalMatching to ExactCoverBy3Sets.
#[derive(Debug, Clone)]
pub struct ReductionThreeDimensionalMatchingToExactCoverBy3Sets {
target: ExactCoverBy3Sets,
}

impl ReductionResult for ReductionThreeDimensionalMatchingToExactCoverBy3Sets {
type Source = ThreeDimensionalMatching;
type Target = ExactCoverBy3Sets;

fn target_problem(&self) -> &Self::Target {
&self.target
}

fn extract_solution(&self, target_solution: &[usize]) -> Vec<usize> {
target_solution.to_vec()
}
}

#[reduction(overhead = {
universe_size = "3 * universe_size",
num_subsets = "num_triples",
num_sets = "num_triples",
})]
impl ReduceTo<ExactCoverBy3Sets> for ThreeDimensionalMatching {
type Result = ReductionThreeDimensionalMatchingToExactCoverBy3Sets;

fn reduce_to(&self) -> Self::Result {
let q = self.universe_size();
let tagged_subsets = self
.triples()
.iter()
.map(|&(w, x, y)| [w, q + x, 2 * q + y])
.collect();

ReductionThreeDimensionalMatchingToExactCoverBy3Sets {
target: ExactCoverBy3Sets::new(3 * q, tagged_subsets),
}
}
}

#[cfg(feature = "example-db")]
pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::RuleExampleSpec> {
use crate::export::SolutionPair;

vec![crate::example_db::specs::RuleExampleSpec {
id: "threedimensionalmatching_to_exactcoverby3sets",
build: || {
crate::example_db::specs::rule_example_with_witness::<_, ExactCoverBy3Sets>(
ThreeDimensionalMatching::new(
3,
vec![(0, 0, 0), (1, 1, 1), (2, 2, 2), (0, 1, 2), (1, 2, 0)],
),
SolutionPair {
source_config: vec![1, 1, 1, 0, 0],
target_config: vec![1, 1, 1, 0, 0],
},
)
},
}]
}

#[cfg(test)]
#[path = "../unit_tests/rules/threedimensionalmatching_exactcoverby3sets.rs"]
mod tests;
2 changes: 2 additions & 0 deletions src/unit_tests/rules/analysis.rs
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,8 @@ fn test_find_dominated_rules_returns_known_set() {
),
// ExactCoverBy3Sets → MaxSetPacking → ILP is better than direct ExactCoverBy3Sets → ILP
("ExactCoverBy3Sets", "ILP {variable: \"bool\"}"),
// ThreeDimensionalMatching → ExactCoverBy3Sets → ILP is better than direct ThreeDimensionalMatching → ILP
("ThreeDimensionalMatching", "ILP {variable: \"bool\"}"),
// GraphPartitioning → MaxCut → SpinGlass → QUBO is better than direct GraphPartitioning → QUBO
(
"GraphPartitioning {graph: \"SimpleGraph\"}",
Expand Down
152 changes: 152 additions & 0 deletions src/unit_tests/rules/threedimensionalmatching_exactcoverby3sets.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
use super::*;
use crate::rules::test_helpers::assert_satisfaction_round_trip_from_satisfaction_target;
use crate::rules::traits::{ReduceTo, ReductionResult};
use crate::solvers::BruteForce;
use crate::traits::Problem;
use std::collections::HashSet;

#[test]
fn test_threedimensionalmatching_to_exactcoverby3sets_closed_loop() {
let source = ThreeDimensionalMatching::new(
3,
vec![(0, 0, 0), (1, 1, 1), (2, 2, 2), (0, 1, 2), (1, 2, 0)],
);
let reduction = ReduceTo::<ExactCoverBy3Sets>::reduce_to(&source);
assert_satisfaction_round_trip_from_satisfaction_target(
&source,
&reduction,
"ThreeDimensionalMatching -> ExactCoverBy3Sets",
);
}

#[test]
fn test_all_q2_triple_families_preserve_exact_witnesses() {
let triples: Vec<_> = (0..2)
.flat_map(|w| (0..2).flat_map(move |x| (0..2).map(move |y| (w, x, y))))
.collect();
let solver = BruteForce::new();

for family_mask in 0..(1usize << triples.len()) {
let family: Vec<_> = triples
.iter()
.enumerate()
.filter_map(|(index, &triple)| ((family_mask >> index) & 1 == 1).then_some(triple))
.collect();
let source = ThreeDimensionalMatching::new(2, family);
let reduction = ReduceTo::<ExactCoverBy3Sets>::reduce_to(&source);
let source_witnesses: HashSet<_> = solver.find_all_witnesses(&source).into_iter().collect();
let extracted_witnesses: HashSet<_> = solver
.find_all_witnesses(reduction.target_problem())
.into_iter()
.map(|witness| reduction.extract_solution(&witness))
.collect();

assert_eq!(
extracted_witnesses, source_witnesses,
"witness mismatch for family mask {family_mask:#010b}"
);
}
}

#[test]
fn test_target_tagging_and_overhead() {
let source = ThreeDimensionalMatching::new(3, vec![(0, 2, 1), (2, 0, 2)]);
let reduction = ReduceTo::<ExactCoverBy3Sets>::reduce_to(&source);
let target = reduction.target_problem();

assert_eq!(target.universe_size(), 9);
assert_eq!(target.subsets(), &[[0, 5, 7], [2, 3, 8]]);
assert_eq!(target.num_subsets(), 2);
assert_eq!(target.num_sets(), 2);

let entries: Vec<_> = inventory::iter::<crate::rules::ReductionEntry>()
.filter(|entry| {
entry.source_name == "ThreeDimensionalMatching"
&& entry.target_name == "ExactCoverBy3Sets"
})
.collect();
assert_eq!(entries.len(), 1);
let overhead = (entries[0].overhead_eval_fn)(&source as &dyn std::any::Any);
assert_eq!(overhead.get("universe_size"), Some(9));
assert_eq!(overhead.get("num_subsets"), Some(2));
assert_eq!(overhead.get("num_sets"), Some(2));
}

#[test]
fn test_infeasible_instance_has_no_target_witness() {
let source = ThreeDimensionalMatching::new(3, vec![(0, 0, 0), (0, 1, 1), (1, 2, 2)]);
let reduction = ReduceTo::<ExactCoverBy3Sets>::reduce_to(&source);

assert!(BruteForce::new().find_witness(&source).is_none());
assert!(BruteForce::new()
.find_witness(reduction.target_problem())
.is_none());
}

#[test]
fn test_empty_universe_preserves_empty_witness() {
let source = ThreeDimensionalMatching::new(0, vec![]);
let reduction = ReduceTo::<ExactCoverBy3Sets>::reduce_to(&source);
let target = reduction.target_problem();

assert_eq!(target.universe_size(), 0);
assert!(target.subsets().is_empty());
let target_witness = BruteForce::new().find_witness(target).unwrap();
assert!(target_witness.is_empty());
assert!(
source
.evaluate(&reduction.extract_solution(&target_witness))
.0
);
}

#[test]
fn test_duplicate_triples_preserve_indices() {
let source = ThreeDimensionalMatching::new(2, vec![(0, 0, 0), (0, 0, 0), (1, 1, 1)]);
let reduction = ReduceTo::<ExactCoverBy3Sets>::reduce_to(&source);

assert_eq!(
reduction.target_problem().subsets(),
&[[0, 2, 4], [0, 2, 4], [1, 3, 5]]
);
let witnesses: HashSet<_> = BruteForce::new()
.find_all_witnesses(reduction.target_problem())
.into_iter()
.collect();
assert_eq!(witnesses, HashSet::from([vec![0, 1, 1], vec![1, 0, 1]]));
for witness in witnesses {
assert!(source.evaluate(&reduction.extract_solution(&witness)).0);
}
}

#[test]
fn test_unused_coordinate_makes_both_instances_infeasible() {
let source = ThreeDimensionalMatching::new(2, vec![(0, 0, 0), (1, 0, 1)]);
let reduction = ReduceTo::<ExactCoverBy3Sets>::reduce_to(&source);

assert!(BruteForce::new().find_witness(&source).is_none());
assert!(BruteForce::new()
.find_witness(reduction.target_problem())
.is_none());
}

#[test]
fn test_equal_numeric_coordinates_are_distinct_across_domains() {
let source = ThreeDimensionalMatching::new(2, vec![(0, 0, 0), (1, 1, 1)]);
let reduction = ReduceTo::<ExactCoverBy3Sets>::reduce_to(&source);

assert_eq!(
reduction.target_problem().subsets(),
&[[0, 2, 4], [1, 3, 5]]
);
assert!(reduction.target_problem().evaluate(&[1, 1]).0);
assert!(source.evaluate(&reduction.extract_solution(&[1, 1])).0);
}

#[test]
fn test_solution_extraction_is_identity() {
let source = ThreeDimensionalMatching::new(2, vec![(0, 0, 0), (1, 1, 1)]);
let reduction = ReduceTo::<ExactCoverBy3Sets>::reduce_to(&source);

assert_eq!(reduction.extract_solution(&[1, 0]), vec![1, 0]);
}