diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index ac2135880..cd1a3525b 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -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)$.], diff --git a/docs/paper/references.bib b/docs/paper/references.bib index a4f203221..3d145bc7f 100644 --- a/docs/paper/references.bib +++ b/docs/paper/references.bib @@ -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}, diff --git a/src/rules/knapsack_ilp.rs b/src/rules/knapsack_ilp.rs new file mode 100644 index 000000000..3434d94f2 --- /dev/null +++ b/src/rules/knapsack_ilp.rs @@ -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 + +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, +} + +impl ReductionResult for ReductionKnapsackToILP { + type Source = Knapsack; + type Target = ILP; + + fn target_problem(&self) -> &ILP { + &self.target + } + + fn extract_solution(&self, target_solution: &[usize]) -> Vec { + target_solution.to_vec() + } +} + +#[reduction( + overhead = { + num_vars = "num_items", + num_constraints = "1", + } +)] +impl ReduceTo> 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 { + use crate::export::SolutionPair; + + vec![crate::example_db::specs::RuleExampleSpec { + id: "knapsack_to_ilp", + build: || { + crate::example_db::specs::rule_example_with_witness::<_, ILP>( + 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; diff --git a/src/rules/mod.rs b/src/rules/mod.rs index 4740bd9bd..77f3d8941 100644 --- a/src/rules/mod.rs +++ b/src/rules/mod.rs @@ -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; @@ -113,6 +115,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec ILP -> QUBO is better than the direct penalty reduction + ("Knapsack", "QUBO {weight: \"f64\"}"), // MaxMatching → MaxSetPacking → ILP is better than direct MaxMatching → ILP ( "MaximumMatching {graph: \"SimpleGraph\", weight: \"i32\"}", diff --git a/src/unit_tests/rules/graph.rs b/src/unit_tests/rules/graph.rs index 1c6a1a8be..a7c5d9326 100644 --- a/src/unit_tests/rules/graph.rs +++ b/src/unit_tests/rules/graph.rs @@ -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}; @@ -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::::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"); +} + #[test] fn test_has_direct_reduction() { let graph = ReductionGraph::new(); diff --git a/src/unit_tests/rules/knapsack_ilp.rs b/src/unit_tests/rules/knapsack_ilp.rs new file mode 100644 index 000000000..0b5cfe014 --- /dev/null +++ b/src/unit_tests/rules/knapsack_ilp.rs @@ -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::>::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::>::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::>::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::>::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::::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], + }] + ); +}