diff --git a/src/rules/mod.rs b/src/rules/mod.rs index fabfa8c2..125e39af 100644 --- a/src/rules/mod.rs +++ b/src/rules/mod.rs @@ -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; @@ -369,6 +370,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec 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 { + 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 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 = 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 = 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 { + 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::::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; diff --git a/src/unit_tests/rules/subsetsum_partition.rs b/src/unit_tests/rules/subsetsum_partition.rs new file mode 100644 index 00000000..7ea14f1a --- /dev/null +++ b/src/unit_tests/rules/subsetsum_partition.rs @@ -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::::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::::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::::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::::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::::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", + ); +}