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
39 changes: 37 additions & 2 deletions docs/paper/reductions.typ
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -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()
Expand Down
2 changes: 2 additions & 0 deletions src/rules/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -107,6 +108,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::Ru
specs.extend(sat_minimumdominatingset::canonical_rule_example_specs());
specs.extend(spinglass_maxcut::canonical_rule_example_specs());
specs.extend(spinglass_qubo::canonical_rule_example_specs());
specs.extend(subsetsum_knapsack::canonical_rule_example_specs());
specs.extend(travelingsalesman_qubo::canonical_rule_example_specs());
#[cfg(feature = "ilp-solver")]
{
Expand Down
67 changes: 67 additions & 0 deletions src/rules/subsetsum_knapsack.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
//! Reduction from Subset Sum to Knapsack.

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

#[derive(Debug, Clone)]
pub struct ReductionSubsetSumToKnapsack {
target: Knapsack,
}

impl ReductionResult for ReductionSubsetSumToKnapsack {
type Source = SubsetSum;
type Target = Knapsack;

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

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

#[reduction(overhead = { num_items = "num_elements" })]
impl ReduceTo<Knapsack> for SubsetSum {
type Result = ReductionSubsetSumToKnapsack;

fn reduce_to(&self) -> Self::Result {
let weights = self.sizes().iter().map(biguint_to_i64).collect::<Vec<_>>();
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<crate::example_db::specs::RuleExampleSpec> {
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;
74 changes: 74 additions & 0 deletions src/unit_tests/rules/subsetsum_knapsack.rs
Original file line number Diff line number Diff line change
@@ -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::<Knapsack>::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::<Knapsack>::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::<Knapsack>::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::<Knapsack>::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],
}]
);
}
Loading