Skip to content
Merged
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
36 changes: 36 additions & 0 deletions docs/paper/reductions.typ
Original file line number Diff line number Diff line change
Expand Up @@ -4676,6 +4676,42 @@ The following reductions to Integer Linear Programming are straightforward formu
_Solution extraction._ $K = {v : x_v = 1}$.
]

#let ks_ilp = load-example("Knapsack", "ILP")
#let ks_ilp_sol = ks_ilp.solutions.at(0)
#let ks_ilp_selected = ks_ilp_sol.source_config.enumerate().filter(((i, x)) => x == 1).map(((i, x)) => i)
#let ks_ilp_sel_weight = ks_ilp_selected.fold(0, (a, i) => a + ks_ilp.source.instance.weights.at(i))
#let ks_ilp_sel_value = ks_ilp_selected.fold(0, (a, i) => a + ks_ilp.source.instance.values.at(i))
#reduction-rule("Knapsack", "ILP",
example: true,
example-caption: [$n = #ks_ilp.source.instance.weights.len()$ items, capacity $C = #ks_ilp.source.instance.capacity$],
extra: [
*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$.

*Step 2 -- Build the binary ILP.* Introduce one binary variable per item:
$#range(ks_ilp.source.instance.weights.len()).map(i => $x_#i$).join(", ") in {0,1}$.
The objective is
$ max #ks_ilp.source.instance.values.enumerate().map(((i, v)) => $#v x_#i$).join($+$) $
subject to the single capacity inequality
$ #ks_ilp.source.instance.weights.enumerate().map(((i, w)) => $#w x_#i$).join($+$) <= #ks_ilp.source.instance.capacity $.

*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.

*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.
],
)[
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.
][
_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:
$ max sum_(i=0)^(n-1) v_i x_i $
subject to
$ sum_(i=0)^(n-1) w_i x_i <= C $
and $x_i in {0,1}$ for all $i$. The target therefore has exactly $n$ variables and one linear constraint.

_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.

_Solution extraction._ Identity: return the binary variable vector $bold(x)$ as the knapsack selection.
]

#reduction-rule("MaximumClique", "MaximumIndependentSet",
example: true,
example-caption: [Path graph $P_4$: clique in $G$ maps to independent set in complement $overline(G)$.],
Expand Down
8 changes: 8 additions & 0 deletions docs/paper/references.bib
Original file line number Diff line number Diff line change
Expand Up @@ -934,6 +934,14 @@ @article{papadimitriou1982
doi = {10.1145/322307.322309}
}

@book{papadimitriou-steiglitz1982,
author = {Christos H. Papadimitriou and Kenneth Steiglitz},
title = {Combinatorial Optimization: Algorithms and Complexity},
publisher = {Prentice-Hall},
address = {Englewood Cliffs, NJ},
year = {1982}
}

@techreport{Heidari2022,
author = {Heidari, Shahrokh and Dinneen, Michael J. and Delmas, Patrice},
title = {An Equivalent {QUBO} Model to the Minimum Multi-Way Cut Problem},
Expand Down
83 changes: 83 additions & 0 deletions src/rules/knapsack_ilp.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
//! Reduction from Knapsack to ILP (Integer Linear Programming).
//!
//! The standard 0-1 knapsack formulation is already a binary ILP:
//! - Variables: one binary variable per item
//! - Constraint: the total selected weight must not exceed capacity
//! - Objective: maximize the total selected value

Comment on lines +1 to +7
use crate::models::algebraic::{ILP, LinearConstraint, ObjectiveSense};
use crate::models::misc::Knapsack;
use crate::reduction;
use crate::rules::traits::{ReduceTo, ReductionResult};

/// Result of reducing Knapsack to ILP.
#[derive(Debug, Clone)]
pub struct ReductionKnapsackToILP {
target: ILP<bool>,
}

impl ReductionResult for ReductionKnapsackToILP {
type Source = Knapsack;
type Target = ILP<bool>;

fn target_problem(&self) -> &ILP<bool> {
&self.target
}

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

#[reduction(
overhead = {
num_vars = "num_items",
num_constraints = "1",
}
)]
impl ReduceTo<ILP<bool>> for Knapsack {
type Result = ReductionKnapsackToILP;

fn reduce_to(&self) -> Self::Result {
let num_vars = self.num_items();
let constraints = vec![LinearConstraint::le(
self.weights()
.iter()
.enumerate()
.map(|(i, &weight)| (i, weight as f64))
.collect(),
self.capacity() as f64,
)];
let objective = self
.values()
.iter()
.enumerate()
.map(|(i, &value)| (i, value as f64))
.collect();
let target = ILP::new(num_vars, constraints, objective, ObjectiveSense::Maximize);

ReductionKnapsackToILP { target }
}
}

#[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: "knapsack_to_ilp",
build: || {
crate::example_db::specs::rule_example_with_witness::<_, ILP<bool>>(
Knapsack::new(vec![1, 3, 4, 5], vec![1, 4, 5, 7], 7),
SolutionPair {
source_config: vec![0, 1, 1, 0],
target_config: vec![0, 1, 1, 0],
},
)
},
}]
}

#[cfg(test)]
#[path = "../unit_tests/rules/knapsack_ilp.rs"]
mod tests;
3 changes: 3 additions & 0 deletions src/rules/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ mod ilp_bool_ilp_i32;
#[cfg(feature = "ilp-solver")]
pub(crate) mod ilp_qubo;
#[cfg(feature = "ilp-solver")]
pub(crate) mod knapsack_ilp;
#[cfg(feature = "ilp-solver")]
pub(crate) mod longestcommonsubsequence_ilp;
#[cfg(feature = "ilp-solver")]
pub(crate) mod maximumclique_ilp;
Expand Down Expand Up @@ -113,6 +115,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::Ru
specs.extend(coloring_ilp::canonical_rule_example_specs());
specs.extend(factoring_ilp::canonical_rule_example_specs());
specs.extend(ilp_qubo::canonical_rule_example_specs());
specs.extend(knapsack_ilp::canonical_rule_example_specs());
specs.extend(longestcommonsubsequence_ilp::canonical_rule_example_specs());
specs.extend(maximumclique_ilp::canonical_rule_example_specs());
specs.extend(maximummatching_ilp::canonical_rule_example_specs());
Expand Down
2 changes: 2 additions & 0 deletions src/unit_tests/rules/analysis.rs
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,8 @@ fn test_find_dominated_rules_returns_known_set() {
("Factoring", "ILP {variable: \"i32\"}"),
// K3-SAT → QUBO via SAT → CircuitSAT → SpinGlass chain
("KSatisfiability {k: \"K3\"}", "QUBO {weight: \"f64\"}"),
// Knapsack -> ILP -> QUBO is better than the direct penalty reduction
("Knapsack", "QUBO {weight: \"f64\"}"),
// MaxMatching → MaxSetPacking → ILP is better than direct MaxMatching → ILP
Comment on lines 245 to 250
(
"MaximumMatching {graph: \"SimpleGraph\", weight: \"i32\"}",
Expand Down
26 changes: 25 additions & 1 deletion src/unit_tests/rules/graph.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use super::*;
use crate::models::algebraic::QUBO;
use crate::models::algebraic::{ILP, QUBO};
use crate::models::graph::{MaximumIndependentSet, MinimumVertexCover};
use crate::models::misc::Knapsack;
use crate::models::set::MaximumSetPacking;
use crate::rules::cost::{Minimize, MinimizeSteps};
use crate::rules::graph::{classify_problem_category, ReductionStep};
Expand Down Expand Up @@ -50,6 +51,29 @@ fn test_find_shortest_path() {
assert_eq!(path.len(), 1); // Direct path exists
}

#[test]
fn test_knapsack_to_ilp_path_exists() {
let graph = ReductionGraph::new();
let src = ReductionGraph::variant_to_map(&Knapsack::variant());
let dst = ReductionGraph::variant_to_map(&ILP::<bool>::variant());
let path = graph.find_cheapest_path(
"Knapsack",
&src,
"ILP",
&dst,
&ProblemSize::new(vec![]),
&MinimizeSteps,
);

let path = path.expect("Knapsack should reduce to ILP");
assert_eq!(
path.type_names(),
vec!["Knapsack", "ILP"],
"Knapsack should have a direct ILP reduction"
);
assert_eq!(path.len(), 1, "Knapsack -> ILP should be one direct step");
}
Comment on lines +54 to +75

#[test]
fn test_has_direct_reduction() {
let graph = ReductionGraph::new();
Expand Down
97 changes: 97 additions & 0 deletions src/unit_tests/rules/knapsack_ilp.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
use super::*;
use crate::models::algebraic::{Comparison, ObjectiveSense, ILP};
use crate::rules::test_helpers::assert_optimization_round_trip_from_optimization_target;
use crate::solvers::ILPSolver;

#[test]
fn test_knapsack_to_ilp_closed_loop() {
let knapsack = Knapsack::new(vec![1, 3, 4, 5], vec![1, 4, 5, 7], 7);
let reduction = ReduceTo::<ILP<bool>>::reduce_to(&knapsack);

assert_optimization_round_trip_from_optimization_target(
&knapsack,
&reduction,
"Knapsack->ILP closed loop",
);

let ilp_solution = ILPSolver::new()
.solve(reduction.target_problem())
.expect("ILP should be solvable");
let extracted = reduction.extract_solution(&ilp_solution);
assert_eq!(extracted, vec![0, 1, 1, 0]);
}

#[test]
fn test_knapsack_to_ilp_structure() {
let knapsack = Knapsack::new(vec![1, 3, 4, 5], vec![1, 4, 5, 7], 7);
let reduction = ReduceTo::<ILP<bool>>::reduce_to(&knapsack);
let ilp = reduction.target_problem();

assert_eq!(ilp.num_vars(), 4);
assert_eq!(ilp.num_constraints(), 1);
assert_eq!(ilp.sense, ObjectiveSense::Maximize);
assert_eq!(ilp.objective, vec![(0, 1.0), (1, 4.0), (2, 5.0), (3, 7.0)]);

let constraint = &ilp.constraints[0];
assert_eq!(constraint.cmp, Comparison::Le);
assert_eq!(constraint.rhs, 7.0);
assert_eq!(
constraint.terms,
vec![(0, 1.0), (1, 3.0), (2, 4.0), (3, 5.0)]
);
}

#[test]
fn test_knapsack_to_ilp_zero_capacity() {
let knapsack = Knapsack::new(vec![2, 3], vec![5, 7], 0);
let reduction = ReduceTo::<ILP<bool>>::reduce_to(&knapsack);

let ilp_solution = ILPSolver::new()
.solve(reduction.target_problem())
.expect("zero-capacity ILP should still be solvable");
let extracted = reduction.extract_solution(&ilp_solution);
assert_eq!(extracted, vec![0, 0]);
}

#[test]
fn test_knapsack_to_ilp_empty_instance() {
let knapsack = Knapsack::new(vec![], vec![], 0);
let reduction = ReduceTo::<ILP<bool>>::reduce_to(&knapsack);
let ilp = reduction.target_problem();

assert_eq!(ilp.num_vars(), 0);
assert_eq!(ilp.num_constraints(), 1);
assert_eq!(ilp.constraints[0].cmp, Comparison::Le);
assert_eq!(ilp.constraints[0].rhs, 0.0);
assert!(ilp.constraints[0].terms.is_empty());
assert!(ilp.objective.is_empty());

let ilp_solution = ILPSolver::new()
.solve(ilp)
.expect("empty Knapsack ILP should still be solvable");
let extracted = reduction.extract_solution(&ilp_solution);
assert_eq!(extracted, Vec::<usize>::new());
}

#[cfg(feature = "example-db")]
#[test]
fn test_knapsack_to_ilp_canonical_example_spec() {
let spec = canonical_rule_example_specs()
.into_iter()
.find(|spec| spec.id == "knapsack_to_ilp")
.expect("missing canonical Knapsack -> ILP example spec");
let example = (spec.build)();

assert_eq!(example.source.problem, "Knapsack");
assert_eq!(example.target.problem, "ILP");
assert_eq!(example.source.instance["capacity"], 7);
assert_eq!(example.target.instance["num_vars"], 4);
assert_eq!(example.target.instance["constraints"].as_array().unwrap().len(), 1);
assert_eq!(
example.solutions,
vec![crate::export::SolutionPair {
source_config: vec![0, 1, 1, 0],
target_config: vec![0, 1, 1, 0],
}]
);
}
Loading