Skip to content

Commit 418222f

Browse files
GiggleLiuisPANN
andauthored
Fix #639: [Rule] Knapsack to ILP (#688)
* Add plan for #639: [Rule] Knapsack to ILP * Implement #639: [Rule] Knapsack to ILP * Add extra tests for #639 Knapsack -> ILP * chore: remove plan file after implementation * Fix canonical example to use rule_example_with_witness API The direct_ilp_example helper was removed on main. Update to use the current rule_example_with_witness API with an explicit SolutionPair. --------- Co-authored-by: Xiwei Pan <xiwei.pan@connect.hkust-gz.edu.cn>
1 parent 7906394 commit 418222f

7 files changed

Lines changed: 254 additions & 1 deletion

File tree

docs/paper/reductions.typ

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4676,6 +4676,42 @@ The following reductions to Integer Linear Programming are straightforward formu
46764676
_Solution extraction._ $K = {v : x_v = 1}$.
46774677
]
46784678

4679+
#let ks_ilp = load-example("Knapsack", "ILP")
4680+
#let ks_ilp_sol = ks_ilp.solutions.at(0)
4681+
#let ks_ilp_selected = ks_ilp_sol.source_config.enumerate().filter(((i, x)) => x == 1).map(((i, x)) => i)
4682+
#let ks_ilp_sel_weight = ks_ilp_selected.fold(0, (a, i) => a + ks_ilp.source.instance.weights.at(i))
4683+
#let ks_ilp_sel_value = ks_ilp_selected.fold(0, (a, i) => a + ks_ilp.source.instance.values.at(i))
4684+
#reduction-rule("Knapsack", "ILP",
4685+
example: true,
4686+
example-caption: [$n = #ks_ilp.source.instance.weights.len()$ items, capacity $C = #ks_ilp.source.instance.capacity$],
4687+
extra: [
4688+
*Step 1 -- Source instance.* The canonical knapsack instance has weights $(#ks_ilp.source.instance.weights.map(str).join(", "))$, values $(#ks_ilp.source.instance.values.map(str).join(", "))$, and capacity $C = #ks_ilp.source.instance.capacity$.
4689+
4690+
*Step 2 -- Build the binary ILP.* Introduce one binary variable per item:
4691+
$#range(ks_ilp.source.instance.weights.len()).map(i => $x_#i$).join(", ") in {0,1}$.
4692+
The objective is
4693+
$ max #ks_ilp.source.instance.values.enumerate().map(((i, v)) => $#v x_#i$).join($+$) $
4694+
subject to the single capacity inequality
4695+
$ #ks_ilp.source.instance.weights.enumerate().map(((i, w)) => $#w x_#i$).join($+$) <= #ks_ilp.source.instance.capacity $.
4696+
4697+
*Step 3 -- Verify a solution.* The ILP optimum $bold(x)^* = (#ks_ilp_sol.target_config.map(str).join(", "))$ extracts directly to the knapsack selection $bold(x)^* = (#ks_ilp_sol.source_config.map(str).join(", "))$, choosing items $\{#ks_ilp_selected.map(str).join(", ")\}$. Their total weight is $#ks_ilp_selected.map(i => str(ks_ilp.source.instance.weights.at(i))).join(" + ") = #ks_ilp_sel_weight$ and their total value is $#ks_ilp_selected.map(i => str(ks_ilp.source.instance.values.at(i))).join(" + ") = #ks_ilp_sel_value$ #sym.checkmark.
4698+
4699+
*Uniqueness:* The fixture stores one canonical optimal witness. For this instance the optimum is unique: items $\{#ks_ilp_selected.map(str).join(", ")\}$ are the only feasible choice achieving value #ks_ilp_sel_value.
4700+
],
4701+
)[
4702+
A 0-1 Knapsack instance is already a binary Integer Linear Program @papadimitriou-steiglitz1982: each item-selection bit becomes a binary variable, the capacity condition is a single linear inequality, and the value objective is linear. The reduction preserves the number of decision variables exactly, producing an ILP with $n$ variables and one constraint.
4703+
][
4704+
_Construction._ Given nonnegative weights $w_0, dots, w_(n-1)$, nonnegative values $v_0, dots, v_(n-1)$, and capacity $C$, introduce binary variables $x_0, dots, x_(n-1) in {0,1}$ where $x_i = 1$ iff item $i$ is selected. Construct the binary ILP:
4705+
$ max sum_(i=0)^(n-1) v_i x_i $
4706+
subject to
4707+
$ sum_(i=0)^(n-1) w_i x_i <= C $
4708+
and $x_i in {0,1}$ for all $i$. The target therefore has exactly $n$ variables and one linear constraint.
4709+
4710+
_Correctness._ ($arrow.r.double$) Any feasible knapsack solution $bold(x)$ satisfies $sum_i w_i x_i <= C$, so the same binary vector is feasible for the ILP and attains identical objective value $sum_i v_i x_i$. ($arrow.l.double$) Any feasible binary ILP solution selects exactly the items with $x_i = 1$; the single inequality guarantees the chosen set fits in the knapsack, and the ILP objective equals the knapsack value. Therefore optimal solutions correspond one-to-one and preserve the optimum value.
4711+
4712+
_Solution extraction._ Identity: return the binary variable vector $bold(x)$ as the knapsack selection.
4713+
]
4714+
46794715
#reduction-rule("MaximumClique", "MaximumIndependentSet",
46804716
example: true,
46814717
example-caption: [Path graph $P_4$: clique in $G$ maps to independent set in complement $overline(G)$.],

docs/paper/references.bib

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -934,6 +934,14 @@ @article{papadimitriou1982
934934
doi = {10.1145/322307.322309}
935935
}
936936

937+
@book{papadimitriou-steiglitz1982,
938+
author = {Christos H. Papadimitriou and Kenneth Steiglitz},
939+
title = {Combinatorial Optimization: Algorithms and Complexity},
940+
publisher = {Prentice-Hall},
941+
address = {Englewood Cliffs, NJ},
942+
year = {1982}
943+
}
944+
937945
@techreport{Heidari2022,
938946
author = {Heidari, Shahrokh and Dinneen, Michael J. and Delmas, Patrice},
939947
title = {An Equivalent {QUBO} Model to the Minimum Multi-Way Cut Problem},

src/rules/knapsack_ilp.rs

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
//! Reduction from Knapsack to ILP (Integer Linear Programming).
2+
//!
3+
//! The standard 0-1 knapsack formulation is already a binary ILP:
4+
//! - Variables: one binary variable per item
5+
//! - Constraint: the total selected weight must not exceed capacity
6+
//! - Objective: maximize the total selected value
7+
8+
use crate::models::algebraic::{ILP, LinearConstraint, ObjectiveSense};
9+
use crate::models::misc::Knapsack;
10+
use crate::reduction;
11+
use crate::rules::traits::{ReduceTo, ReductionResult};
12+
13+
/// Result of reducing Knapsack to ILP.
14+
#[derive(Debug, Clone)]
15+
pub struct ReductionKnapsackToILP {
16+
target: ILP<bool>,
17+
}
18+
19+
impl ReductionResult for ReductionKnapsackToILP {
20+
type Source = Knapsack;
21+
type Target = ILP<bool>;
22+
23+
fn target_problem(&self) -> &ILP<bool> {
24+
&self.target
25+
}
26+
27+
fn extract_solution(&self, target_solution: &[usize]) -> Vec<usize> {
28+
target_solution.to_vec()
29+
}
30+
}
31+
32+
#[reduction(
33+
overhead = {
34+
num_vars = "num_items",
35+
num_constraints = "1",
36+
}
37+
)]
38+
impl ReduceTo<ILP<bool>> for Knapsack {
39+
type Result = ReductionKnapsackToILP;
40+
41+
fn reduce_to(&self) -> Self::Result {
42+
let num_vars = self.num_items();
43+
let constraints = vec![LinearConstraint::le(
44+
self.weights()
45+
.iter()
46+
.enumerate()
47+
.map(|(i, &weight)| (i, weight as f64))
48+
.collect(),
49+
self.capacity() as f64,
50+
)];
51+
let objective = self
52+
.values()
53+
.iter()
54+
.enumerate()
55+
.map(|(i, &value)| (i, value as f64))
56+
.collect();
57+
let target = ILP::new(num_vars, constraints, objective, ObjectiveSense::Maximize);
58+
59+
ReductionKnapsackToILP { target }
60+
}
61+
}
62+
63+
#[cfg(feature = "example-db")]
64+
pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::RuleExampleSpec> {
65+
use crate::export::SolutionPair;
66+
67+
vec![crate::example_db::specs::RuleExampleSpec {
68+
id: "knapsack_to_ilp",
69+
build: || {
70+
crate::example_db::specs::rule_example_with_witness::<_, ILP<bool>>(
71+
Knapsack::new(vec![1, 3, 4, 5], vec![1, 4, 5, 7], 7),
72+
SolutionPair {
73+
source_config: vec![0, 1, 1, 0],
74+
target_config: vec![0, 1, 1, 0],
75+
},
76+
)
77+
},
78+
}]
79+
}
80+
81+
#[cfg(test)]
82+
#[path = "../unit_tests/rules/knapsack_ilp.rs"]
83+
mod tests;

src/rules/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ mod ilp_bool_ilp_i32;
5555
#[cfg(feature = "ilp-solver")]
5656
pub(crate) mod ilp_qubo;
5757
#[cfg(feature = "ilp-solver")]
58+
pub(crate) mod knapsack_ilp;
59+
#[cfg(feature = "ilp-solver")]
5860
pub(crate) mod longestcommonsubsequence_ilp;
5961
#[cfg(feature = "ilp-solver")]
6062
pub(crate) mod maximumclique_ilp;
@@ -113,6 +115,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::Ru
113115
specs.extend(coloring_ilp::canonical_rule_example_specs());
114116
specs.extend(factoring_ilp::canonical_rule_example_specs());
115117
specs.extend(ilp_qubo::canonical_rule_example_specs());
118+
specs.extend(knapsack_ilp::canonical_rule_example_specs());
116119
specs.extend(longestcommonsubsequence_ilp::canonical_rule_example_specs());
117120
specs.extend(maximumclique_ilp::canonical_rule_example_specs());
118121
specs.extend(maximummatching_ilp::canonical_rule_example_specs());

src/unit_tests/rules/analysis.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,8 @@ fn test_find_dominated_rules_returns_known_set() {
245245
("Factoring", "ILP {variable: \"i32\"}"),
246246
// K3-SAT → QUBO via SAT → CircuitSAT → SpinGlass chain
247247
("KSatisfiability {k: \"K3\"}", "QUBO {weight: \"f64\"}"),
248+
// Knapsack -> ILP -> QUBO is better than the direct penalty reduction
249+
("Knapsack", "QUBO {weight: \"f64\"}"),
248250
// MaxMatching → MaxSetPacking → ILP is better than direct MaxMatching → ILP
249251
(
250252
"MaximumMatching {graph: \"SimpleGraph\", weight: \"i32\"}",

src/unit_tests/rules/graph.rs

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use super::*;
2-
use crate::models::algebraic::QUBO;
2+
use crate::models::algebraic::{ILP, QUBO};
33
use crate::models::graph::{MaximumIndependentSet, MinimumVertexCover};
4+
use crate::models::misc::Knapsack;
45
use crate::models::set::MaximumSetPacking;
56
use crate::rules::cost::{Minimize, MinimizeSteps};
67
use crate::rules::graph::{classify_problem_category, ReductionStep};
@@ -50,6 +51,29 @@ fn test_find_shortest_path() {
5051
assert_eq!(path.len(), 1); // Direct path exists
5152
}
5253

54+
#[test]
55+
fn test_knapsack_to_ilp_path_exists() {
56+
let graph = ReductionGraph::new();
57+
let src = ReductionGraph::variant_to_map(&Knapsack::variant());
58+
let dst = ReductionGraph::variant_to_map(&ILP::<bool>::variant());
59+
let path = graph.find_cheapest_path(
60+
"Knapsack",
61+
&src,
62+
"ILP",
63+
&dst,
64+
&ProblemSize::new(vec![]),
65+
&MinimizeSteps,
66+
);
67+
68+
let path = path.expect("Knapsack should reduce to ILP");
69+
assert_eq!(
70+
path.type_names(),
71+
vec!["Knapsack", "ILP"],
72+
"Knapsack should have a direct ILP reduction"
73+
);
74+
assert_eq!(path.len(), 1, "Knapsack -> ILP should be one direct step");
75+
}
76+
5377
#[test]
5478
fn test_has_direct_reduction() {
5579
let graph = ReductionGraph::new();
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
use super::*;
2+
use crate::models::algebraic::{Comparison, ObjectiveSense, ILP};
3+
use crate::rules::test_helpers::assert_optimization_round_trip_from_optimization_target;
4+
use crate::solvers::ILPSolver;
5+
6+
#[test]
7+
fn test_knapsack_to_ilp_closed_loop() {
8+
let knapsack = Knapsack::new(vec![1, 3, 4, 5], vec![1, 4, 5, 7], 7);
9+
let reduction = ReduceTo::<ILP<bool>>::reduce_to(&knapsack);
10+
11+
assert_optimization_round_trip_from_optimization_target(
12+
&knapsack,
13+
&reduction,
14+
"Knapsack->ILP closed loop",
15+
);
16+
17+
let ilp_solution = ILPSolver::new()
18+
.solve(reduction.target_problem())
19+
.expect("ILP should be solvable");
20+
let extracted = reduction.extract_solution(&ilp_solution);
21+
assert_eq!(extracted, vec![0, 1, 1, 0]);
22+
}
23+
24+
#[test]
25+
fn test_knapsack_to_ilp_structure() {
26+
let knapsack = Knapsack::new(vec![1, 3, 4, 5], vec![1, 4, 5, 7], 7);
27+
let reduction = ReduceTo::<ILP<bool>>::reduce_to(&knapsack);
28+
let ilp = reduction.target_problem();
29+
30+
assert_eq!(ilp.num_vars(), 4);
31+
assert_eq!(ilp.num_constraints(), 1);
32+
assert_eq!(ilp.sense, ObjectiveSense::Maximize);
33+
assert_eq!(ilp.objective, vec![(0, 1.0), (1, 4.0), (2, 5.0), (3, 7.0)]);
34+
35+
let constraint = &ilp.constraints[0];
36+
assert_eq!(constraint.cmp, Comparison::Le);
37+
assert_eq!(constraint.rhs, 7.0);
38+
assert_eq!(
39+
constraint.terms,
40+
vec![(0, 1.0), (1, 3.0), (2, 4.0), (3, 5.0)]
41+
);
42+
}
43+
44+
#[test]
45+
fn test_knapsack_to_ilp_zero_capacity() {
46+
let knapsack = Knapsack::new(vec![2, 3], vec![5, 7], 0);
47+
let reduction = ReduceTo::<ILP<bool>>::reduce_to(&knapsack);
48+
49+
let ilp_solution = ILPSolver::new()
50+
.solve(reduction.target_problem())
51+
.expect("zero-capacity ILP should still be solvable");
52+
let extracted = reduction.extract_solution(&ilp_solution);
53+
assert_eq!(extracted, vec![0, 0]);
54+
}
55+
56+
#[test]
57+
fn test_knapsack_to_ilp_empty_instance() {
58+
let knapsack = Knapsack::new(vec![], vec![], 0);
59+
let reduction = ReduceTo::<ILP<bool>>::reduce_to(&knapsack);
60+
let ilp = reduction.target_problem();
61+
62+
assert_eq!(ilp.num_vars(), 0);
63+
assert_eq!(ilp.num_constraints(), 1);
64+
assert_eq!(ilp.constraints[0].cmp, Comparison::Le);
65+
assert_eq!(ilp.constraints[0].rhs, 0.0);
66+
assert!(ilp.constraints[0].terms.is_empty());
67+
assert!(ilp.objective.is_empty());
68+
69+
let ilp_solution = ILPSolver::new()
70+
.solve(ilp)
71+
.expect("empty Knapsack ILP should still be solvable");
72+
let extracted = reduction.extract_solution(&ilp_solution);
73+
assert_eq!(extracted, Vec::<usize>::new());
74+
}
75+
76+
#[cfg(feature = "example-db")]
77+
#[test]
78+
fn test_knapsack_to_ilp_canonical_example_spec() {
79+
let spec = canonical_rule_example_specs()
80+
.into_iter()
81+
.find(|spec| spec.id == "knapsack_to_ilp")
82+
.expect("missing canonical Knapsack -> ILP example spec");
83+
let example = (spec.build)();
84+
85+
assert_eq!(example.source.problem, "Knapsack");
86+
assert_eq!(example.target.problem, "ILP");
87+
assert_eq!(example.source.instance["capacity"], 7);
88+
assert_eq!(example.target.instance["num_vars"], 4);
89+
assert_eq!(example.target.instance["constraints"].as_array().unwrap().len(), 1);
90+
assert_eq!(
91+
example.solutions,
92+
vec![crate::export::SolutionPair {
93+
source_config: vec![0, 1, 1, 0],
94+
target_config: vec![0, 1, 1, 0],
95+
}]
96+
);
97+
}

0 commit comments

Comments
 (0)