Skip to content

Commit ca7b5b6

Browse files
authored
Fix #202: [Rule] Partition to Knapsack (#744)
* Add plan for #202: [Rule] Partition to Knapsack * Implement #202: [Rule] Partition to Knapsack * chore: remove plan file after implementation
1 parent 669f090 commit ca7b5b6

4 files changed

Lines changed: 171 additions & 0 deletions

File tree

docs/paper/reductions.typ

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5929,6 +5929,48 @@ where $P$ is a penalty weight large enough that any constraint violation costs m
59295929
_Solution extraction._ Discard slack variables: return $bold(x)' [0..n]$.
59305930
]
59315931

5932+
#let part_ks = load-example("Partition", "Knapsack")
5933+
#let part_ks_sol = part_ks.solutions.at(0)
5934+
#let part_ks_sizes = part_ks.source.instance.sizes
5935+
#let part_ks_n = part_ks_sizes.len()
5936+
#let part_ks_total = part_ks_sizes.fold(0, (a, b) => a + b)
5937+
#let part_ks_capacity = part_ks.target.instance.capacity
5938+
#let part_ks_selected = part_ks_sol.source_config.enumerate().filter(((i, x)) => x == 1).map(((i, x)) => i)
5939+
#let part_ks_selected_sizes = part_ks_selected.map(i => part_ks_sizes.at(i))
5940+
#let part_ks_selected_sum = part_ks_selected_sizes.fold(0, (a, b) => a + b)
5941+
#reduction-rule("Partition", "Knapsack",
5942+
example: true,
5943+
example-caption: [#part_ks_n elements, total sum $S = #part_ks_total$],
5944+
extra: [
5945+
#pred-commands(
5946+
"pred create --example " + problem-spec(part_ks.source) + " -o partition.json",
5947+
"pred reduce partition.json --to " + target-spec(part_ks) + " -o bundle.json",
5948+
"pred solve bundle.json",
5949+
"pred evaluate partition.json --config " + part_ks_sol.source_config.map(str).join(","),
5950+
)
5951+
5952+
*Step 1 -- Source instance.* The canonical Partition instance has sizes $(#part_ks_sizes.map(str).join(", "))$ with total sum $S = #part_ks_total$, so a balanced witness must hit exactly $S / 2 = #part_ks_capacity$.
5953+
5954+
*Step 2 -- Build the knapsack instance.* The reduction copies each size into both the weight and the value list, producing weights $(#part_ks.target.instance.weights.map(str).join(", "))$, values $(#part_ks.target.instance.values.map(str).join(", "))$, and capacity $C = #part_ks_capacity$. No auxiliary variables are introduced, so the target has the same $#part_ks_n$ binary coordinates as the source.
5955+
5956+
*Step 3 -- Verify the canonical witness.* The serialized witness uses the same binary vector on both sides, $bold(x) = (#part_ks_sol.source_config.map(str).join(", "))$. It selects elements at indices $\{#part_ks_selected.map(str).join(", ")\}$ with sizes $(#part_ks_selected_sizes.map(str).join(", "))$, so the chosen subset has total weight and value $#part_ks_selected_sum = #part_ks_capacity$. Hence the knapsack solution saturates the capacity and certifies a balanced partition.
5957+
5958+
*Witness semantics.* The example DB stores one canonical balanced subset. This instance has multiple balanced partitions because several different subsets sum to $#part_ks_capacity$, but one witness is enough to demonstrate the reduction.
5959+
],
5960+
)[
5961+
This $O(n)$ reduction#footnote[The linear-time bound follows from a single pass that copies the source sizes into item weights and values.] @garey1979[MP9] constructs a 0-1 Knapsack instance by copying each Partition size into both the item weight and item value and setting the capacity to half the total size sum. For $n$ source elements it produces $n$ knapsack items.
5962+
][
5963+
_Construction._ Given positive sizes $s_0, dots, s_(n-1)$ with total sum $S = sum_(i=0)^(n-1) s_i$, create one knapsack item per element and set
5964+
$ w_i = s_i, quad v_i = s_i $
5965+
for every $i in {0, dots, n-1}$. Set the knapsack capacity to
5966+
$ C = floor(S / 2). $
5967+
Every feasible knapsack solution is therefore a subset of the original elements, and because $w_i = v_i$, its objective value equals the same subset sum.
5968+
5969+
_Correctness._ ($arrow.r.double$) If the Partition instance is satisfiable, some subset $A'$ has sum $S / 2$. In particular $S$ is even, so $C = S / 2$, and selecting exactly the corresponding knapsack items is feasible with value $S / 2$. No feasible knapsack solution can have value larger than $C$, because value equals weight for every item and total weight is bounded by $C$. Thus the knapsack optimum is exactly $S / 2$. ($arrow.l.double$) If the knapsack optimum is $S / 2$, then the optimum is an integer and hence $S$ must be even. The selected items have total value $S / 2$, so they also have total weight $S / 2$ because $w_i = v_i$ itemwise. Those items therefore form a subset of the original multiset whose complement has the same sum, giving a valid balanced partition.
5970+
5971+
_Solution extraction._ Return the same binary selection vector on the original elements: item $i$ is selected in the knapsack witness if and only if element $i$ belongs to the extracted partition subset.
5972+
]
5973+
59325974
#let ks_qubo = load-example("Knapsack", "QUBO")
59335975
#let ks_qubo_sol = ks_qubo.solutions.at(0)
59345976
#let ks_qubo_num_items = ks_qubo.source.instance.weights.len()

src/rules/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ pub(crate) mod minimummultiwaycut_qubo;
3131
pub(crate) mod minimumvertexcover_maximumindependentset;
3232
pub(crate) mod minimumvertexcover_minimumfeedbackvertexset;
3333
pub(crate) mod minimumvertexcover_minimumsetcovering;
34+
pub(crate) mod partition_knapsack;
3435
pub(crate) mod sat_circuitsat;
3536
pub(crate) mod sat_coloring;
3637
pub(crate) mod sat_ksat;
@@ -114,6 +115,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::Ru
114115
specs.extend(maximummatching_maximumsetpacking::canonical_rule_example_specs());
115116
specs.extend(maximumsetpacking_qubo::canonical_rule_example_specs());
116117
specs.extend(minimummultiwaycut_qubo::canonical_rule_example_specs());
118+
specs.extend(partition_knapsack::canonical_rule_example_specs());
117119
specs.extend(minimumvertexcover_maximumindependentset::canonical_rule_example_specs());
118120
specs.extend(minimumvertexcover_minimumfeedbackvertexset::canonical_rule_example_specs());
119121
specs.extend(minimumvertexcover_minimumsetcovering::canonical_rule_example_specs());

src/rules/partition_knapsack.rs

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
//! Reduction from Partition to Knapsack.
2+
3+
use crate::models::misc::{Knapsack, Partition};
4+
use crate::reduction;
5+
use crate::rules::traits::{ReduceTo, ReductionResult};
6+
7+
/// Result of reducing Partition to Knapsack.
8+
#[derive(Debug, Clone)]
9+
pub struct ReductionPartitionToKnapsack {
10+
target: Knapsack,
11+
}
12+
13+
impl ReductionResult for ReductionPartitionToKnapsack {
14+
type Source = Partition;
15+
type Target = Knapsack;
16+
17+
fn target_problem(&self) -> &Self::Target {
18+
&self.target
19+
}
20+
21+
fn extract_solution(&self, target_solution: &[usize]) -> Vec<usize> {
22+
target_solution.to_vec()
23+
}
24+
}
25+
26+
fn partition_size_to_i64(value: u64) -> i64 {
27+
i64::try_from(value)
28+
.expect("Partition -> Knapsack requires all sizes and total_sum / 2 to fit in i64")
29+
}
30+
31+
#[reduction(overhead = {
32+
num_items = "num_elements",
33+
})]
34+
impl ReduceTo<Knapsack> for Partition {
35+
type Result = ReductionPartitionToKnapsack;
36+
37+
fn reduce_to(&self) -> Self::Result {
38+
let weights: Vec<i64> = self
39+
.sizes()
40+
.iter()
41+
.copied()
42+
.map(partition_size_to_i64)
43+
.collect();
44+
let values = weights.clone();
45+
let capacity = partition_size_to_i64(self.total_sum() / 2);
46+
47+
ReductionPartitionToKnapsack {
48+
target: Knapsack::new(weights, values, capacity),
49+
}
50+
}
51+
}
52+
53+
#[cfg(feature = "example-db")]
54+
pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::RuleExampleSpec> {
55+
use crate::export::SolutionPair;
56+
57+
vec![crate::example_db::specs::RuleExampleSpec {
58+
id: "partition_to_knapsack",
59+
build: || {
60+
crate::example_db::specs::rule_example_with_witness::<_, Knapsack>(
61+
Partition::new(vec![3, 1, 1, 2, 2, 1]),
62+
SolutionPair {
63+
source_config: vec![1, 0, 0, 1, 0, 0],
64+
target_config: vec![1, 0, 0, 1, 0, 0],
65+
},
66+
)
67+
},
68+
}]
69+
}
70+
71+
#[cfg(test)]
72+
#[path = "../unit_tests/rules/partition_knapsack.rs"]
73+
mod tests;
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
use super::*;
2+
use crate::models::misc::Partition;
3+
use crate::rules::test_helpers::assert_satisfaction_round_trip_from_optimization_target;
4+
use crate::solvers::{BruteForce, Solver};
5+
use crate::traits::Problem;
6+
use crate::types::SolutionSize;
7+
8+
#[test]
9+
fn test_partition_to_knapsack_closed_loop() {
10+
let source = Partition::new(vec![3, 1, 1, 2, 2, 1]);
11+
let reduction = ReduceTo::<Knapsack>::reduce_to(&source);
12+
13+
assert_satisfaction_round_trip_from_optimization_target(
14+
&source,
15+
&reduction,
16+
"Partition -> Knapsack closed loop",
17+
);
18+
}
19+
20+
#[test]
21+
fn test_partition_to_knapsack_structure() {
22+
let source = Partition::new(vec![3, 1, 1, 2, 2, 1]);
23+
let reduction = ReduceTo::<Knapsack>::reduce_to(&source);
24+
let target = reduction.target_problem();
25+
26+
assert_eq!(target.weights(), &[3, 1, 1, 2, 2, 1]);
27+
assert_eq!(target.values(), &[3, 1, 1, 2, 2, 1]);
28+
assert_eq!(target.capacity(), 5);
29+
assert_eq!(target.num_items(), source.num_elements());
30+
}
31+
32+
#[test]
33+
fn test_partition_to_knapsack_odd_total_is_not_satisfying() {
34+
let source = Partition::new(vec![2, 4, 5]);
35+
let reduction = ReduceTo::<Knapsack>::reduce_to(&source);
36+
let target = reduction.target_problem();
37+
let best = BruteForce::new()
38+
.find_best(target)
39+
.expect("Knapsack target should always have an optimal solution");
40+
41+
assert_eq!(target.evaluate(&best), SolutionSize::Valid(5));
42+
43+
let extracted = reduction.extract_solution(&best);
44+
assert!(!source.evaluate(&extracted));
45+
}
46+
47+
#[test]
48+
#[should_panic(
49+
expected = "Partition -> Knapsack requires all sizes and total_sum / 2 to fit in i64"
50+
)]
51+
fn test_partition_to_knapsack_panics_on_large_coefficients() {
52+
let source = Partition::new(vec![(i64::MAX as u64) + 1]);
53+
let _ = ReduceTo::<Knapsack>::reduce_to(&source);
54+
}

0 commit comments

Comments
 (0)