Skip to content
Closed
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
2 changes: 2 additions & 0 deletions src/rules/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ pub(crate) mod spinglass_maxcut;
pub(crate) mod spinglass_qubo;
pub(crate) mod subsetsum_capacityassignment;
pub(crate) mod subsetsum_closestvectorproblem;
pub(crate) mod subsetsum_partition;
#[cfg(test)]
pub(crate) mod test_helpers;
pub(crate) mod threepartition_flowshopscheduling;
Expand Down Expand Up @@ -369,6 +370,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::Ru
specs.extend(spinglass_qubo::canonical_rule_example_specs());
specs.extend(subsetsum_capacityassignment::canonical_rule_example_specs());
specs.extend(subsetsum_closestvectorproblem::canonical_rule_example_specs());
specs.extend(subsetsum_partition::canonical_rule_example_specs());
specs.extend(travelingsalesman_qubo::canonical_rule_example_specs());
#[cfg(feature = "ilp-solver")]
{
Expand Down
160 changes: 160 additions & 0 deletions src/rules/subsetsum_partition.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
//! Reduction from SubsetSum to Partition.
//!
//! Given a SubsetSum instance (sizes, target T) with total sum Sigma,
//! we construct a Partition instance by padding with d = |Sigma - 2T|
//! when d > 0. The Partition half-sum H = (Sigma + d) / 2 aligns so
//! that a balanced partition of the padded set encodes a subset summing
//! to T in the original instance.

use crate::models::misc::{Partition, SubsetSum};
use crate::reduction;
use crate::rules::traits::{ReduceTo, ReductionResult};
use num_bigint::BigUint;
use num_traits::{ToPrimitive, Zero};

/// Result of reducing SubsetSum to Partition.
#[derive(Debug, Clone)]
pub struct ReductionSubsetSumToPartition {
target: Partition,
/// Number of elements in the original SubsetSum instance.
source_n: usize,
/// The padding value d = |Sigma - 2*T|. Zero means no padding element was added.
d: BigUint,
/// Total sum of original sizes.
sigma: BigUint,
/// Original target T.
original_target: BigUint,
}

impl ReductionResult for ReductionSubsetSumToPartition {
type Source = SubsetSum;
type Target = Partition;

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

fn extract_solution(&self, target_solution: &[usize]) -> Vec<usize> {
let n = self.source_n;

if self.d.is_zero() {
// No padding: partition_sizes == original sizes.
// Check which side sums to target.
// side0 = indices where partition_config[i] == 0
let side0_sum: BigUint = (0..n)
.filter(|&i| target_solution[i] == 0)
.map(|i| BigUint::from(self.target.sizes()[i]))
.sum();

if side0_sum == self.original_target {
// side 0 sums to target, so "selected" (1 in SubsetSum) = side 0 = NOT partition bit
(0..n)
.map(|i| 1 - target_solution[i])
.collect()
} else {
// side 1 sums to target
(0..n)
.map(|i| target_solution[i])
.collect()
}
} else if self.sigma > self.original_target.clone() * 2u32 {
// Sigma > 2T: S-elements on SAME side as padding sum to T
let pad_side = target_solution[n];
(0..n)
.map(|i| if target_solution[i] == pad_side { 1 } else { 0 })
.collect()
} else {
// Sigma < 2T: S-elements on OPPOSITE side from padding sum to T
let pad_side = target_solution[n];
(0..n)
.map(|i| if target_solution[i] != pad_side { 1 } else { 0 })
.collect()
}
}
}

#[reduction(overhead = {
num_elements = "num_elements + 1",
})]
impl ReduceTo<Partition> for SubsetSum {
type Result = ReductionSubsetSumToPartition;

fn reduce_to(&self) -> Self::Result {
let sigma: BigUint = self.sizes().iter().sum();
let two_t = self.target() * 2u32;
let d = if sigma >= two_t {
sigma.clone() - two_t
} else {
two_t - sigma.clone()
};

let source_n = self.num_elements();

let partition_sizes: Vec<u64> = if d.is_zero() {
self.sizes()
.iter()
.map(|s| s.to_u64().expect("size must fit in u64"))
.collect()
} else {
let d_u64 = d.to_u64().expect("padding d must fit in u64");
let mut sizes: Vec<u64> = self
.sizes()
.iter()
.map(|s| s.to_u64().expect("size must fit in u64"))
.collect();
sizes.push(d_u64);
sizes
};

let target = Partition::new(partition_sizes);

ReductionSubsetSumToPartition {
target,
source_n,
d: d.clone(),
sigma: sigma.clone(),
original_target: self.target().clone(),
}
}
}

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

// YES instance: sizes=[3,5,7,1,4], target=8
// Sigma=20, 2T=16, d=4, partition_sizes=[3,5,7,1,4,4]
// SubsetSum solution: select {3,5} -> config=[1,1,0,0,0]
// Partition: half=12, side with pad (side 0): 3+5+4=12 -> config=[0,0,1,1,1,0]
// Sigma > 2T: selected = same side as pad
// pad is at index 5, pad_side = config[5] = 0
// selected = indices where config[i] == 0 = {0,1,5} -> source config [1,1,0,0,0]
vec![crate::example_db::specs::RuleExampleSpec {
id: "subsetsum_to_partition",
build: || {
let source = SubsetSum::new(vec![3u32, 5, 7, 1, 4], 8u32);
let reduction = ReduceTo::<Partition>::reduce_to(&source);
let target = reduction.target_problem();

// Find a valid partition witness via brute force
let witness = crate::solvers::BruteForce::new()
.find_witness(target)
.expect("YES instance must have a partition witness");

// Extract source solution
let source_config = reduction.extract_solution(&witness);

crate::example_db::specs::rule_example_with_witness::<_, Partition>(
source,
SolutionPair {
source_config,
target_config: witness,
},
)
},
}]
}

#[cfg(test)]
#[path = "../unit_tests/rules/subsetsum_partition.rs"]
mod tests;
90 changes: 90 additions & 0 deletions src/unit_tests/rules/subsetsum_partition.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
use super::*;
use crate::rules::test_helpers::assert_satisfaction_round_trip_from_satisfaction_target;
use crate::solvers::BruteForce;

#[test]
fn test_subsetsum_to_partition_closed_loop() {
// YES instance: sizes=[3,5,7,1,4], target=8
// Sigma=20, 2T=16, d=4 -> partition_sizes=[3,5,7,1,4,4]
let source = SubsetSum::new(vec![3u32, 5, 7, 1, 4], 8u32);
let reduction = ReduceTo::<Partition>::reduce_to(&source);

assert_satisfaction_round_trip_from_satisfaction_target(
&source,
&reduction,
"SubsetSum -> Partition closed loop",
);
}

#[test]
fn test_subsetsum_to_partition_infeasible() {
// NO instance: sizes=[3,7,11], target=5
// Sigma=21, 2T=10, d=11 -> partition_sizes=[3,7,11,11]
// Total=32, half=16 — no subset sums to 16 among {3,7,11,11}
let source = SubsetSum::new(vec![3u32, 7, 11], 5u32);
let reduction = ReduceTo::<Partition>::reduce_to(&source);
let target = reduction.target_problem();

// No witness should exist for the target partition
let witness = BruteForce::new().find_witness(target);
assert!(witness.is_none(), "NO instance should yield infeasible Partition");

// Source should also be infeasible
let source_witness = BruteForce::new().find_witness(&source);
assert!(source_witness.is_none(), "Source SubsetSum should be infeasible");
}

#[test]
fn test_subsetsum_to_partition_structure() {
// sizes=[3,5,7,1,4], target=8
// Sigma=20, d=|20-16|=4 -> partition_sizes=[3,5,7,1,4,4]
let source = SubsetSum::new(vec![3u32, 5, 7, 1, 4], 8u32);
let reduction = ReduceTo::<Partition>::reduce_to(&source);
let target = reduction.target_problem();

// One extra element (the padding d=4)
assert_eq!(target.num_elements(), source.num_elements() + 1);
assert_eq!(target.sizes(), &[3, 5, 7, 1, 4, 4]);
// Total sum = 20 + 4 = 24, half = 12
assert_eq!(target.total_sum(), 24);
}

#[test]
fn test_subsetsum_to_partition_d_zero() {
// When Sigma == 2T, no padding is added.
// sizes=[2,3,5], target=5 -> Sigma=10, 2T=10, d=0
// partition_sizes=[2,3,5], total=10, half=5
let source = SubsetSum::new(vec![2u32, 3, 5], 5u32);
let reduction = ReduceTo::<Partition>::reduce_to(&source);
let target = reduction.target_problem();

// Same number of elements (no padding)
assert_eq!(target.num_elements(), source.num_elements());
assert_eq!(target.sizes(), &[2, 3, 5]);

assert_satisfaction_round_trip_from_satisfaction_target(
&source,
&reduction,
"SubsetSum -> Partition d=0 case",
);
}

#[test]
fn test_subsetsum_to_partition_sigma_less_than_2t() {
// Sigma < 2T case: sizes=[1,2,3], target=5
// Sigma=6, 2T=10, d=4 -> partition_sizes=[1,2,3,4]
// Total=10, half=5. Subset {1,4} or {2,3} sums to 5.
// SubsetSum: need subset summing to 5 from {1,2,3} -> {2,3}
let source = SubsetSum::new(vec![1u32, 2, 3], 5u32);
let reduction = ReduceTo::<Partition>::reduce_to(&source);
let target = reduction.target_problem();

assert_eq!(target.num_elements(), 4);
assert_eq!(target.sizes(), &[1, 2, 3, 4]);

assert_satisfaction_round_trip_from_satisfaction_target(
&source,
&reduction,
"SubsetSum -> Partition sigma < 2T case",
);
}
Loading