diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index 57127ed8..e3fc21ad 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -3415,8 +3415,7 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76], let nproc = x.instance.num_processors let deadlines = x.instance.deadlines let precs = x.instance.precedences - let sample = x.samples.at(0) - let start = sample.config + let start = x.optimal_config let horizon = deadlines.fold(0, (acc, d) => if d > acc { d } else { acc }) let slot-groups = range(horizon).map(slot => range(ntasks).filter(t => start.at(t) == slot)) let tight-tasks = range(ntasks).filter(t => start.at(t) + 1 == deadlines.at(t)) @@ -4278,6 +4277,42 @@ where $P$ is a penalty weight large enough that any constraint violation costs m _Solution extraction._ Discard slack variables: return $bold(x)' [0..n]$. ] +#let ss_kn = load-example("SubsetSum", "Knapsack") +#let ss_kn_sol = ss_kn.solutions.at(0) +#let ss_kn_sizes = ss_kn.source.instance.sizes +#let ss_kn_selected = range(ss_kn_sizes.len()).filter(i => ss_kn_sol.source_config.at(i) == 1) +#let ss_kn_sel_sizes = ss_kn_selected.map(i => ss_kn_sizes.at(i)) +#reduction-rule("SubsetSum", "Knapsack", + example: true, + example-caption: [$n = #ss_kn_sizes.len()$ elements, target $B = #ss_kn.source.instance.target$], + extra: [ + *Step 1 -- Source instance.* The canonical Subset Sum instance has sizes $(#ss_kn_sizes.map(str).join(", "))$ and target $B = #ss_kn.source.instance.target$. + + *Step 2 -- Build Knapsack.* Reuse the same numbers as both weights and values: + $bold(w) = (#ss_kn.target.instance.weights.map(str).join(", "))$, $bold(v) = (#ss_kn.target.instance.values.map(str).join(", "))$, and capacity $C = #ss_kn.target.instance.capacity$. + + *Step 3 -- Verify a witness.* The canonical witness $bold(x) = (#ss_kn_sol.source_config.map(str).join(", "))$ selects $\{#ss_kn_sel_sizes.map(str).join(", ")\}$, so the total selected weight and value are $#ss_kn_sel_sizes.map(str).join(" + ") = #ss_kn.source.instance.target$. + + *Witness semantics.* The fixture stores one canonical witness. Other subsets can also sum to $B$ for this instance, and every such subset maps to an optimal Knapsack solution by the same identity extraction. + ], +)[ + This is the direct special-case embedding between the classical formulations of Subset Sum and Knapsack @karp1972 @garey1979. Given positive integers $a_0, dots, a_(n-1)$ and target $B$, create one knapsack item per element with weight $w_i = a_i$ and value $v_i = a_i$, and set the capacity to $C = B$. The reduction preserves the number of variables/items exactly. In the present library implementation, this embedding applies when all source integers fit in signed 64-bit storage, because `Knapsack` uses `i64` weights, values, and capacity. +][ + _Construction._ For a Subset Sum instance with binary choice variables $x_0, dots, x_(n-1)$, create a Knapsack instance with the same binary variables and define + $ w_i = a_i, quad v_i = a_i quad (0 <= i < n), quad C = B. $ + No auxiliary items or constraints are added: the target has exactly $n$ items. + + _Correctness._ ($arrow.r.double$) If a subset $A' subset.eq A$ satisfies $sum_(a_i in A') a_i = B$, select exactly those corresponding knapsack items. The total weight is $B$, so the choice is feasible, and because $v_i = w_i$ for every item, its total value is also $B$. Any feasible knapsack solution satisfies + $ sum_i v_i x_i = sum_i w_i x_i <= C = B, $ + so this feasible solution already attains the maximum possible value $B$. ($arrow.l.double$) If the Knapsack optimum has value $B$, then the optimal selection satisfies + $ B = sum_i v_i x_i = sum_i w_i x_i <= C = B, $ + hence the selected items have total weight exactly $B$, giving a valid Subset Sum witness. If no subset sums to $B$, then every feasible knapsack solution has value strictly less than $B$, so extracting any optimum does _not_ satisfy the source instance. + + _Variable mapping._ The binary variables are reused unchanged: $x_i = 1$ means “choose element $a_i$” in Subset Sum and “take item $i$” in Knapsack. + + _Solution extraction._ Return the target selection vector unchanged. +] + #let ks_qubo = load-example("Knapsack", "QUBO") #let ks_qubo_sol = ks_qubo.solutions.at(0) #let ks_qubo_num_items = ks_qubo.source.instance.weights.len() diff --git a/src/rules/mod.rs b/src/rules/mod.rs index 77f3d894..2243d5bc 100644 --- a/src/rules/mod.rs +++ b/src/rules/mod.rs @@ -35,6 +35,7 @@ pub(crate) mod sat_minimumdominatingset; mod spinglass_casts; pub(crate) mod spinglass_maxcut; pub(crate) mod spinglass_qubo; +pub(crate) mod subsetsum_knapsack; #[cfg(test)] pub(crate) mod test_helpers; mod traits; @@ -107,6 +108,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec &Self::Target { + &self.target + } + + fn extract_solution(&self, target_solution: &[usize]) -> Vec { + target_solution.to_vec() + } +} + +#[reduction(overhead = { num_items = "num_elements" })] +impl ReduceTo for SubsetSum { + type Result = ReductionSubsetSumToKnapsack; + + fn reduce_to(&self) -> Self::Result { + let weights = self.sizes().iter().map(biguint_to_i64).collect::>(); + let capacity = biguint_to_i64(self.target()); + + ReductionSubsetSumToKnapsack { + target: Knapsack::new(weights.clone(), weights, capacity), + } + } +} + +fn biguint_to_i64(value: &BigUint) -> i64 { + value.to_i64().unwrap_or_else(|| { + panic!("SubsetSum -> Knapsack reduction requires all sizes and target to fit in i64") + }) +} + +#[cfg(feature = "example-db")] +pub(crate) fn canonical_rule_example_specs() -> Vec { + use crate::export::SolutionPair; + + vec![crate::example_db::specs::RuleExampleSpec { + id: "subsetsum_to_knapsack", + build: || { + crate::example_db::specs::rule_example_with_witness::<_, Knapsack>( + SubsetSum::new(vec![3u32, 7, 1, 8, 4, 12, 5], 15u32), + SolutionPair { + source_config: vec![1, 0, 0, 0, 0, 1, 0], + target_config: vec![1, 0, 0, 0, 0, 1, 0], + }, + ) + }, + }] +} + +#[cfg(test)] +#[path = "../unit_tests/rules/subsetsum_knapsack.rs"] +mod tests; diff --git a/src/unit_tests/rules/subsetsum_knapsack.rs b/src/unit_tests/rules/subsetsum_knapsack.rs new file mode 100644 index 00000000..15ae2046 --- /dev/null +++ b/src/unit_tests/rules/subsetsum_knapsack.rs @@ -0,0 +1,74 @@ +use super::*; +use crate::models::misc::{Knapsack, SubsetSum}; +use crate::rules::test_helpers::{ + assert_satisfaction_round_trip_from_optimization_target, solve_optimization_problem, +}; +use crate::traits::Problem; +use num_bigint::BigUint; + +#[test] +fn test_subsetsum_to_knapsack_closed_loop() { + let source = SubsetSum::new(vec![3u32, 7, 1, 8, 4, 12, 5], 15u32); + let reduction = ReduceTo::::reduce_to(&source); + + assert_satisfaction_round_trip_from_optimization_target( + &source, + &reduction, + "SubsetSum->Knapsack closed loop", + ); +} + +#[test] +fn test_subsetsum_to_knapsack_target_structure() { + let source = SubsetSum::new(vec![3u32, 7, 1, 8, 4, 12, 5], 15u32); + let reduction = ReduceTo::::reduce_to(&source); + let target = reduction.target_problem(); + + assert_eq!(target.weights(), &[3, 7, 1, 8, 4, 12, 5]); + assert_eq!(target.values(), &[3, 7, 1, 8, 4, 12, 5]); + assert_eq!(target.capacity(), 15); + assert_eq!(target.num_items(), source.num_elements()); +} + +#[test] +fn test_subsetsum_to_knapsack_unsat_extracts_non_solution() { + let source = SubsetSum::new(vec![2u32, 4, 6], 5u32); + let reduction = ReduceTo::::reduce_to(&source); + + let target_solution = solve_optimization_problem(reduction.target_problem()) + .expect("SubsetSum->Knapsack: optimization target should always have an optimum"); + let extracted = reduction.extract_solution(&target_solution); + + assert!(!source.evaluate(&extracted)); +} + +#[test] +#[should_panic( + expected = "SubsetSum -> Knapsack reduction requires all sizes and target to fit in i64" +)] +fn test_subsetsum_to_knapsack_panics_on_i64_overflow() { + let too_large = BigUint::from(i64::MAX as u64) + BigUint::from(1u32); + let source = SubsetSum::new_unchecked(vec![too_large.clone()], too_large); + + let _ = ReduceTo::::reduce_to(&source); +} + +#[cfg(feature = "example-db")] +#[test] +fn test_subsetsum_to_knapsack_canonical_example_spec() { + let spec = canonical_rule_example_specs() + .into_iter() + .find(|spec| spec.id == "subsetsum_to_knapsack") + .expect("missing canonical SubsetSum -> Knapsack example spec"); + let example = (spec.build)(); + + assert_eq!(example.source.problem, "SubsetSum"); + assert_eq!(example.target.problem, "Knapsack"); + assert_eq!( + example.solutions, + vec![crate::export::SolutionPair { + source_config: vec![1, 0, 0, 0, 0, 1, 0], + target_config: vec![1, 0, 0, 0, 0, 1, 0], + }] + ); +}