diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index d119290fd..975d74cb0 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -39,6 +39,7 @@ "SpinGlass": [Spin Glass], "QUBO": [QUBO], "ILP": [Integer Linear Programming], + "Knapsack": [Knapsack], "Satisfiability": [SAT], "KSatisfiability": [$k$-SAT], "CircuitSAT": [CircuitSAT], @@ -886,6 +887,14 @@ Biclique Cover is equivalent to factoring the biadjacency matrix $M$ of the bipa ) ] +#problem-def("Knapsack")[ + Given $n$ items with weights $w_0, dots, w_(n-1) in NN$ and values $v_0, dots, v_(n-1) in NN$, and a capacity $C in NN$, find $S subset.eq {0, dots, n - 1}$ maximizing $sum_(i in S) v_i$ subject to $sum_(i in S) w_i lt.eq C$. +][ + One of Karp's 21 NP-complete problems @karp1972. Knapsack is only _weakly_ NP-hard: a classical dynamic-programming algorithm runs in $O(n C)$ pseudo-polynomial time, and a fully polynomial-time approximation scheme (FPTAS) achieves $(1 - epsilon)$-optimal value in $O(n^2 slash epsilon)$ time @ibarra1975. The special case $v_i = w_i$ for all $i$ is the Subset Sum problem. Knapsack is also a special case of Integer Linear Programming with a single constraint. The best known exact algorithm is the $O^*(2^(n slash 2))$ meet-in-the-middle approach of Horowitz and Sahni @horowitz1974, which partitions items into two halves and combines sorted sublists. + + *Example.* Let $n = 4$ items with weights $(2, 3, 4, 5)$, values $(3, 4, 5, 7)$, and capacity $C = 7$. Selecting $S = {1, 2}$ (items with weights 3 and 4) gives total weight $3 + 4 = 7 lt.eq C$ and total value $4 + 5 = 9$. Selecting $S = {0, 3}$ (weights 2 and 5) gives weight $2 + 5 = 7 lt.eq C$ and value $3 + 7 = 10$, which is optimal. +] + // Completeness check: warn about problem types in JSON but missing from paper #{ let json-models = { diff --git a/docs/paper/references.bib b/docs/paper/references.bib index e75278765..c3129a691 100644 --- a/docs/paper/references.bib +++ b/docs/paper/references.bib @@ -366,3 +366,24 @@ @article{alber2004 doi = {10.1016/j.jalgor.2003.10.001} } +@article{horowitz1974, + author = {Ellis Horowitz and Sartaj Sahni}, + title = {Computing Partitions with Applications to the Knapsack Problem}, + journal = {Journal of the ACM}, + volume = {21}, + number = {2}, + pages = {277--292}, + year = {1974}, + doi = {10.1145/321812.321823} +} + +@article{ibarra1975, + author = {Oscar H. Ibarra and Chul E. Kim}, + title = {Fast Approximation Algorithms for the Knapsack and Sum of Subset Problems}, + journal = {Journal of the ACM}, + volume = {22}, + number = {4}, + pages = {463--468}, + year = {1975}, + doi = {10.1145/321906.321909} +} diff --git a/docs/src/reductions/problem_schemas.json b/docs/src/reductions/problem_schemas.json index 8cc8d2ae9..15eafb74c 100644 --- a/docs/src/reductions/problem_schemas.json +++ b/docs/src/reductions/problem_schemas.json @@ -8,16 +8,6 @@ "type_name": "Vec>", "description": "Target boolean matrix A" }, - { - "name": "m", - "type_name": "usize", - "description": "Number of rows" - }, - { - "name": "n", - "type_name": "usize", - "description": "Number of columns" - }, { "name": "k", "type_name": "usize", @@ -75,11 +65,6 @@ "name": "circuit", "type_name": "Circuit", "description": "The boolean circuit" - }, - { - "name": "variables", - "type_name": "Vec", - "description": "Circuit variable names" } ] }, @@ -183,6 +168,27 @@ } ] }, + { + "name": "Knapsack", + "description": "Select items to maximize total value subject to weight capacity constraint", + "fields": [ + { + "name": "weights", + "type_name": "Vec", + "description": "Item weights w_i" + }, + { + "name": "values", + "type_name": "Vec", + "description": "Item values v_i" + }, + { + "name": "capacity", + "type_name": "i64", + "description": "Knapsack capacity C" + } + ] + }, { "name": "MaxCut", "description": "Find maximum weight cut in a graph", @@ -337,24 +343,9 @@ "description": "Minimize color changes in paint shop sequence", "fields": [ { - "name": "sequence_indices", - "type_name": "Vec", - "description": "Car sequence as indices" - }, - { - "name": "car_labels", + "name": "sequence", "type_name": "Vec", - "description": "Unique car labels" - }, - { - "name": "is_first", - "type_name": "Vec", - "description": "First occurrence flags" - }, - { - "name": "num_cars", - "type_name": "usize", - "description": "Number of unique cars" + "description": "Car labels (each must appear exactly twice)" } ] }, diff --git a/docs/src/reductions/reduction_graph.json b/docs/src/reductions/reduction_graph.json index 0b0c68506..3e73b9c15 100644 --- a/docs/src/reductions/reduction_graph.json +++ b/docs/src/reductions/reduction_graph.json @@ -148,6 +148,13 @@ "doc_path": "models/formula/struct.KSatisfiability.html", "complexity": "2^num_variables" }, + { + "name": "Knapsack", + "variant": {}, + "category": "misc", + "doc_path": "models/misc/struct.Knapsack.html", + "complexity": "2^(num_items / 2)" + }, { "name": "MaxCut", "variant": { @@ -386,7 +393,7 @@ }, { "source": 4, - "target": 38, + "target": 39, "overhead": [ { "field": "num_spins", @@ -431,7 +438,7 @@ }, { "source": 8, - "target": 35, + "target": 36, "overhead": [ { "field": "num_vars", @@ -472,7 +479,7 @@ }, { "source": 13, - "target": 35, + "target": 36, "overhead": [ { "field": "num_vars", @@ -498,7 +505,7 @@ }, { "source": 14, - "target": 35, + "target": 36, "overhead": [ { "field": "num_vars", @@ -509,7 +516,7 @@ }, { "source": 14, - "target": 36, + "target": 37, "overhead": [ { "field": "num_clauses", @@ -543,7 +550,7 @@ }, { "source": 15, - "target": 35, + "target": 36, "overhead": [ { "field": "num_vars", @@ -554,7 +561,7 @@ }, { "source": 15, - "target": 36, + "target": 37, "overhead": [ { "field": "num_clauses", @@ -573,7 +580,7 @@ }, { "source": 16, - "target": 36, + "target": 37, "overhead": [ { "field": "num_clauses", @@ -591,8 +598,8 @@ "doc_path": "rules/sat_ksat/index.html" }, { - "source": 17, - "target": 38, + "source": 18, + "target": 39, "overhead": [ { "field": "num_spins", @@ -606,7 +613,7 @@ "doc_path": "rules/spinglass_maxcut/index.html" }, { - "source": 19, + "source": 20, "target": 8, "overhead": [ { @@ -621,8 +628,8 @@ "doc_path": "rules/maximumclique_ilp/index.html" }, { - "source": 20, - "target": 21, + "source": 21, + "target": 22, "overhead": [ { "field": "num_vertices", @@ -636,8 +643,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 20, - "target": 25, + "source": 21, + "target": 26, "overhead": [ { "field": "num_vertices", @@ -651,8 +658,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 21, - "target": 26, + "source": 22, + "target": 27, "overhead": [ { "field": "num_vertices", @@ -666,8 +673,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 22, - "target": 20, + "source": 23, + "target": 21, "overhead": [ { "field": "num_vertices", @@ -681,8 +688,8 @@ "doc_path": "rules/maximumindependentset_gridgraph/index.html" }, { - "source": 22, - "target": 21, + "source": 23, + "target": 22, "overhead": [ { "field": "num_vertices", @@ -696,8 +703,8 @@ "doc_path": "rules/maximumindependentset_gridgraph/index.html" }, { - "source": 22, - "target": 23, + "source": 23, + "target": 24, "overhead": [ { "field": "num_vertices", @@ -711,8 +718,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 22, - "target": 24, + "source": 23, + "target": 25, "overhead": [ { "field": "num_vertices", @@ -726,8 +733,8 @@ "doc_path": "rules/maximumindependentset_triangular/index.html" }, { - "source": 22, - "target": 28, + "source": 23, + "target": 29, "overhead": [ { "field": "num_sets", @@ -741,7 +748,7 @@ "doc_path": "rules/maximumindependentset_maximumsetpacking/index.html" }, { - "source": 23, + "source": 24, "target": 8, "overhead": [ { @@ -756,8 +763,8 @@ "doc_path": "rules/maximumindependentset_ilp/index.html" }, { - "source": 23, - "target": 30, + "source": 24, + "target": 31, "overhead": [ { "field": "num_sets", @@ -771,8 +778,8 @@ "doc_path": "rules/maximumindependentset_maximumsetpacking/index.html" }, { - "source": 23, - "target": 33, + "source": 24, + "target": 34, "overhead": [ { "field": "num_vertices", @@ -786,8 +793,8 @@ "doc_path": "rules/minimumvertexcover_maximumindependentset/index.html" }, { - "source": 23, - "target": 35, + "source": 24, + "target": 36, "overhead": [ { "field": "num_vars", @@ -797,8 +804,8 @@ "doc_path": "rules/maximumindependentset_qubo/index.html" }, { - "source": 24, - "target": 26, + "source": 25, + "target": 27, "overhead": [ { "field": "num_vertices", @@ -812,8 +819,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 25, - "target": 22, + "source": 26, + "target": 23, "overhead": [ { "field": "num_vertices", @@ -827,8 +834,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 25, - "target": 26, + "source": 26, + "target": 27, "overhead": [ { "field": "num_vertices", @@ -842,8 +849,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 26, - "target": 23, + "source": 27, + "target": 24, "overhead": [ { "field": "num_vertices", @@ -857,7 +864,7 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 27, + "source": 28, "target": 8, "overhead": [ { @@ -872,8 +879,8 @@ "doc_path": "rules/maximummatching_ilp/index.html" }, { - "source": 27, - "target": 30, + "source": 28, + "target": 31, "overhead": [ { "field": "num_sets", @@ -887,8 +894,8 @@ "doc_path": "rules/maximummatching_maximumsetpacking/index.html" }, { - "source": 28, - "target": 22, + "source": 29, + "target": 23, "overhead": [ { "field": "num_vertices", @@ -902,8 +909,8 @@ "doc_path": "rules/maximumindependentset_maximumsetpacking/index.html" }, { - "source": 28, - "target": 30, + "source": 29, + "target": 31, "overhead": [ { "field": "num_sets", @@ -917,8 +924,8 @@ "doc_path": "rules/maximumsetpacking_casts/index.html" }, { - "source": 29, - "target": 35, + "source": 30, + "target": 36, "overhead": [ { "field": "num_vars", @@ -928,7 +935,7 @@ "doc_path": "rules/maximumsetpacking_qubo/index.html" }, { - "source": 30, + "source": 31, "target": 8, "overhead": [ { @@ -943,8 +950,8 @@ "doc_path": "rules/maximumsetpacking_ilp/index.html" }, { - "source": 30, - "target": 23, + "source": 31, + "target": 24, "overhead": [ { "field": "num_vertices", @@ -958,8 +965,8 @@ "doc_path": "rules/maximumindependentset_maximumsetpacking/index.html" }, { - "source": 30, - "target": 29, + "source": 31, + "target": 30, "overhead": [ { "field": "num_sets", @@ -973,7 +980,7 @@ "doc_path": "rules/maximumsetpacking_casts/index.html" }, { - "source": 31, + "source": 32, "target": 8, "overhead": [ { @@ -988,7 +995,7 @@ "doc_path": "rules/minimumdominatingset_ilp/index.html" }, { - "source": 32, + "source": 33, "target": 8, "overhead": [ { @@ -1003,7 +1010,7 @@ "doc_path": "rules/minimumsetcovering_ilp/index.html" }, { - "source": 33, + "source": 34, "target": 8, "overhead": [ { @@ -1018,8 +1025,8 @@ "doc_path": "rules/minimumvertexcover_ilp/index.html" }, { - "source": 33, - "target": 23, + "source": 34, + "target": 24, "overhead": [ { "field": "num_vertices", @@ -1033,8 +1040,8 @@ "doc_path": "rules/minimumvertexcover_maximumindependentset/index.html" }, { - "source": 33, - "target": 32, + "source": 34, + "target": 33, "overhead": [ { "field": "num_sets", @@ -1048,8 +1055,8 @@ "doc_path": "rules/minimumvertexcover_minimumsetcovering/index.html" }, { - "source": 33, - "target": 35, + "source": 34, + "target": 36, "overhead": [ { "field": "num_vars", @@ -1059,7 +1066,7 @@ "doc_path": "rules/minimumvertexcover_qubo/index.html" }, { - "source": 35, + "source": 36, "target": 8, "overhead": [ { @@ -1074,8 +1081,8 @@ "doc_path": "rules/qubo_ilp/index.html" }, { - "source": 35, - "target": 37, + "source": 36, + "target": 38, "overhead": [ { "field": "num_spins", @@ -1085,7 +1092,7 @@ "doc_path": "rules/spinglass_qubo/index.html" }, { - "source": 36, + "source": 37, "target": 4, "overhead": [ { @@ -1100,7 +1107,7 @@ "doc_path": "rules/sat_circuitsat/index.html" }, { - "source": 36, + "source": 37, "target": 10, "overhead": [ { @@ -1115,7 +1122,7 @@ "doc_path": "rules/sat_coloring/index.html" }, { - "source": 36, + "source": 37, "target": 15, "overhead": [ { @@ -1130,8 +1137,8 @@ "doc_path": "rules/sat_ksat/index.html" }, { - "source": 36, - "target": 22, + "source": 37, + "target": 23, "overhead": [ { "field": "num_vertices", @@ -1145,8 +1152,8 @@ "doc_path": "rules/sat_maximumindependentset/index.html" }, { - "source": 36, - "target": 31, + "source": 37, + "target": 32, "overhead": [ { "field": "num_vertices", @@ -1160,8 +1167,8 @@ "doc_path": "rules/sat_minimumdominatingset/index.html" }, { - "source": 37, - "target": 35, + "source": 38, + "target": 36, "overhead": [ { "field": "num_vars", @@ -1171,8 +1178,8 @@ "doc_path": "rules/spinglass_qubo/index.html" }, { - "source": 38, - "target": 17, + "source": 39, + "target": 18, "overhead": [ { "field": "num_vertices", @@ -1186,8 +1193,8 @@ "doc_path": "rules/spinglass_maxcut/index.html" }, { - "source": 38, - "target": 37, + "source": 39, + "target": 38, "overhead": [ { "field": "num_spins", @@ -1201,7 +1208,7 @@ "doc_path": "rules/spinglass_casts/index.html" }, { - "source": 39, + "source": 40, "target": 8, "overhead": [ { diff --git a/problemreductions-cli/src/dispatch.rs b/problemreductions-cli/src/dispatch.rs index a6b0011ff..7a8498421 100644 --- a/problemreductions-cli/src/dispatch.rs +++ b/problemreductions-cli/src/dispatch.rs @@ -1,6 +1,6 @@ use anyhow::{bail, Context, Result}; use problemreductions::models::algebraic::{ClosestVectorProblem, ILP}; -use problemreductions::models::misc::BinPacking; +use problemreductions::models::misc::{BinPacking, Knapsack}; use problemreductions::prelude::*; use problemreductions::rules::{MinimizeSteps, ReductionGraph}; use problemreductions::solvers::{BruteForce, ILPSolver, Solver}; @@ -244,6 +244,7 @@ pub fn load_problem( Some("f64") => deser_opt::>(data), _ => deser_opt::>(data), }, + "Knapsack" => deser_opt::(data), _ => bail!("{}", crate::problem_name::unknown_problem_error(&canonical)), } } @@ -303,6 +304,7 @@ pub fn serialize_any_problem( Some("f64") => try_ser::>(any), _ => try_ser::>(any), }, + "Knapsack" => try_ser::(any), _ => bail!("{}", crate::problem_name::unknown_problem_error(&canonical)), } } diff --git a/problemreductions-cli/src/problem_name.rs b/problemreductions-cli/src/problem_name.rs index 2e715a1b1..acd9b4b59 100644 --- a/problemreductions-cli/src/problem_name.rs +++ b/problemreductions-cli/src/problem_name.rs @@ -19,7 +19,6 @@ pub const ALIASES: &[(&str, &str)] = &[ ("3SAT", "KSatisfiability"), ("KSAT", "KSatisfiability"), ("TSP", "TravelingSalesman"), - ("BP", "BinPacking"), ("CVP", "ClosestVectorProblem"), ("MaxMatching", "MaximumMatching"), ]; @@ -50,8 +49,9 @@ pub fn resolve_alias(input: &str) -> String { "paintshop" => "PaintShop".to_string(), "bmf" => "BMF".to_string(), "bicliquecover" => "BicliqueCover".to_string(), - "bp" | "binpacking" => "BinPacking".to_string(), + "binpacking" => "BinPacking".to_string(), "cvp" | "closestvectorproblem" => "ClosestVectorProblem".to_string(), + "knapsack" => "Knapsack".to_string(), _ => input.to_string(), // pass-through for exact names } } diff --git a/src/lib.rs b/src/lib.rs index ef67ab53c..c9ada7ef1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -43,7 +43,7 @@ pub mod prelude { KColoring, MaxCut, MaximalIS, MaximumClique, MaximumIndependentSet, MaximumMatching, MinimumDominatingSet, MinimumVertexCover, TravelingSalesman, }; - pub use crate::models::misc::{BinPacking, Factoring, PaintShop}; + pub use crate::models::misc::{BinPacking, Factoring, Knapsack, PaintShop}; pub use crate::models::set::{MaximumSetPacking, MinimumSetCovering}; // Core traits diff --git a/src/models/misc/knapsack.rs b/src/models/misc/knapsack.rs new file mode 100644 index 000000000..49e3e90dd --- /dev/null +++ b/src/models/misc/knapsack.rs @@ -0,0 +1,143 @@ +//! Knapsack problem implementation. +//! +//! The 0-1 Knapsack problem asks for a subset of items that maximizes +//! total value while respecting a weight capacity constraint. + +use crate::registry::{FieldInfo, ProblemSchemaEntry}; +use crate::traits::{OptimizationProblem, Problem}; +use crate::types::{Direction, SolutionSize}; +use serde::{Deserialize, Serialize}; + +inventory::submit! { + ProblemSchemaEntry { + name: "Knapsack", + module_path: module_path!(), + description: "Select items to maximize total value subject to weight capacity constraint", + fields: &[ + FieldInfo { name: "weights", type_name: "Vec", description: "Item weights w_i" }, + FieldInfo { name: "values", type_name: "Vec", description: "Item values v_i" }, + FieldInfo { name: "capacity", type_name: "i64", description: "Knapsack capacity C" }, + ], + } +} + +/// The 0-1 Knapsack problem. +/// +/// Given `n` items, each with weight `w_i` and value `v_i`, and a capacity `C`, +/// find a subset `S ⊆ {0, ..., n-1}` such that `∑_{i∈S} w_i ≤ C`, +/// maximizing `∑_{i∈S} v_i`. +/// +/// # Representation +/// +/// Each item has a binary variable: `x_i = 1` if item `i` is selected, `0` otherwise. +/// +/// # Example +/// +/// ``` +/// use problemreductions::models::misc::Knapsack; +/// use problemreductions::{Problem, Solver, BruteForce}; +/// +/// let problem = Knapsack::new(vec![2, 3, 4, 5], vec![3, 4, 5, 7], 7); +/// let solver = BruteForce::new(); +/// let solution = solver.find_best(&problem); +/// assert!(solution.is_some()); +/// ``` +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Knapsack { + weights: Vec, + values: Vec, + capacity: i64, +} + +impl Knapsack { + /// Create a new Knapsack instance. + /// + /// # Panics + /// Panics if `weights` and `values` have different lengths. + pub fn new(weights: Vec, values: Vec, capacity: i64) -> Self { + assert_eq!( + weights.len(), + values.len(), + "weights and values must have the same length" + ); + Self { + weights, + values, + capacity, + } + } + + /// Returns the item weights. + pub fn weights(&self) -> &[i64] { + &self.weights + } + + /// Returns the item values. + pub fn values(&self) -> &[i64] { + &self.values + } + + /// Returns the knapsack capacity. + pub fn capacity(&self) -> i64 { + self.capacity + } + + /// Returns the number of items. + pub fn num_items(&self) -> usize { + self.weights.len() + } +} + +impl Problem for Knapsack { + const NAME: &'static str = "Knapsack"; + type Metric = SolutionSize; + + fn variant() -> Vec<(&'static str, &'static str)> { + crate::variant_params![] + } + + fn dims(&self) -> Vec { + vec![2; self.num_items()] + } + + fn evaluate(&self, config: &[usize]) -> SolutionSize { + if config.len() != self.num_items() { + return SolutionSize::Invalid; + } + if config.iter().any(|&v| v >= 2) { + return SolutionSize::Invalid; + } + let total_weight: i64 = config + .iter() + .enumerate() + .filter(|(_, &x)| x == 1) + .map(|(i, _)| self.weights[i]) + .sum(); + if total_weight > self.capacity { + return SolutionSize::Invalid; + } + let total_value: i64 = config + .iter() + .enumerate() + .filter(|(_, &x)| x == 1) + .map(|(i, _)| self.values[i]) + .sum(); + SolutionSize::Valid(total_value) + } +} + +impl OptimizationProblem for Knapsack { + type Value = i64; + + fn direction(&self) -> Direction { + Direction::Maximize + } +} + +crate::declare_variants! { + Knapsack => "2^(num_items / 2)", +} + +#[cfg(test)] +#[path = "../../unit_tests/models/misc/knapsack.rs"] +mod tests; diff --git a/src/models/misc/mod.rs b/src/models/misc/mod.rs index 6e2fa084a..cdb66e969 100644 --- a/src/models/misc/mod.rs +++ b/src/models/misc/mod.rs @@ -3,12 +3,15 @@ //! Problems with unique input structures that don't fit other categories: //! - [`BinPacking`]: Bin Packing (minimize bins) //! - [`Factoring`]: Integer factorization +//! - [`Knapsack`]: 0-1 Knapsack (maximize value subject to weight capacity) //! - [`PaintShop`]: Minimize color switches in paint shop scheduling mod bin_packing; pub(crate) mod factoring; +mod knapsack; pub(crate) mod paintshop; pub use bin_packing::BinPacking; pub use factoring::Factoring; +pub use knapsack::Knapsack; pub use paintshop::PaintShop; diff --git a/src/models/mod.rs b/src/models/mod.rs index 15df5cfa3..96b4b79d1 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -15,5 +15,5 @@ pub use graph::{ BicliqueCover, KColoring, MaxCut, MaximalIS, MaximumClique, MaximumIndependentSet, MaximumMatching, MinimumDominatingSet, MinimumVertexCover, SpinGlass, TravelingSalesman, }; -pub use misc::{BinPacking, Factoring, PaintShop}; +pub use misc::{BinPacking, Factoring, Knapsack, PaintShop}; pub use set::{MaximumSetPacking, MinimumSetCovering}; diff --git a/src/unit_tests/models/misc/knapsack.rs b/src/unit_tests/models/misc/knapsack.rs new file mode 100644 index 000000000..b658a1a95 --- /dev/null +++ b/src/unit_tests/models/misc/knapsack.rs @@ -0,0 +1,128 @@ +use super::*; +use crate::solvers::{BruteForce, Solver}; +use crate::traits::{OptimizationProblem, Problem}; +use crate::types::Direction; + +#[test] +fn test_knapsack_basic() { + let problem = Knapsack::new(vec![2, 3, 4, 5], vec![3, 4, 5, 7], 7); + assert_eq!(problem.num_items(), 4); + assert_eq!(problem.weights(), &[2, 3, 4, 5]); + assert_eq!(problem.values(), &[3, 4, 5, 7]); + assert_eq!(problem.capacity(), 7); + assert_eq!(problem.dims(), vec![2; 4]); + assert_eq!(problem.direction(), Direction::Maximize); + assert_eq!(::NAME, "Knapsack"); + assert_eq!(::variant(), vec![]); +} + +#[test] +fn test_knapsack_evaluate_optimal() { + let problem = Knapsack::new(vec![2, 3, 4, 5], vec![3, 4, 5, 7], 7); + assert_eq!(problem.evaluate(&[1, 0, 0, 1]), SolutionSize::Valid(10)); +} + +#[test] +fn test_knapsack_evaluate_feasible() { + let problem = Knapsack::new(vec![2, 3, 4, 5], vec![3, 4, 5, 7], 7); + assert_eq!(problem.evaluate(&[1, 1, 0, 0]), SolutionSize::Valid(7)); +} + +#[test] +fn test_knapsack_evaluate_overweight() { + let problem = Knapsack::new(vec![2, 3, 4, 5], vec![3, 4, 5, 7], 7); + assert_eq!(problem.evaluate(&[0, 0, 1, 1]), SolutionSize::Invalid); +} + +#[test] +fn test_knapsack_evaluate_empty() { + let problem = Knapsack::new(vec![2, 3, 4, 5], vec![3, 4, 5, 7], 7); + assert_eq!(problem.evaluate(&[0, 0, 0, 0]), SolutionSize::Valid(0)); +} + +#[test] +fn test_knapsack_evaluate_all_selected() { + let problem = Knapsack::new(vec![1, 1, 1], vec![10, 20, 30], 5); + assert_eq!(problem.evaluate(&[1, 1, 1]), SolutionSize::Valid(60)); +} + +#[test] +fn test_knapsack_evaluate_wrong_config_length() { + let problem = Knapsack::new(vec![2, 3], vec![3, 4], 5); + assert_eq!(problem.evaluate(&[1]), SolutionSize::Invalid); + assert_eq!(problem.evaluate(&[1, 0, 0]), SolutionSize::Invalid); +} + +#[test] +fn test_knapsack_evaluate_invalid_variable_value() { + let problem = Knapsack::new(vec![2, 3], vec![3, 4], 5); + assert_eq!(problem.evaluate(&[2, 0]), SolutionSize::Invalid); +} + +#[test] +fn test_knapsack_empty_instance() { + let problem = Knapsack::new(vec![], vec![], 10); + assert_eq!(problem.num_items(), 0); + assert_eq!(problem.dims(), Vec::::new()); + assert_eq!(problem.evaluate(&[]), SolutionSize::Valid(0)); +} + +#[test] +fn test_knapsack_brute_force() { + let problem = Knapsack::new(vec![2, 3, 4, 5], vec![3, 4, 5, 7], 7); + let solver = BruteForce::new(); + let solution = solver.find_best(&problem).expect("should find a solution"); + let metric = problem.evaluate(&solution); + assert_eq!(metric, SolutionSize::Valid(10)); +} + +#[test] +fn test_knapsack_serialization() { + let problem = Knapsack::new(vec![2, 3, 4, 5], vec![3, 4, 5, 7], 7); + let json = serde_json::to_value(&problem).unwrap(); + let restored: Knapsack = serde_json::from_value(json).unwrap(); + assert_eq!(restored.weights(), problem.weights()); + assert_eq!(restored.values(), problem.values()); + assert_eq!(restored.capacity(), problem.capacity()); +} + +#[test] +fn test_knapsack_zero_capacity() { + // Capacity 0: only empty set is feasible + let problem = Knapsack::new(vec![1, 2], vec![10, 20], 0); + assert_eq!(problem.evaluate(&[0, 0]), SolutionSize::Valid(0)); + assert_eq!(problem.evaluate(&[1, 0]), SolutionSize::Invalid); + let solver = BruteForce::new(); + let solution = solver.find_best(&problem).unwrap(); + assert_eq!(problem.evaluate(&solution), SolutionSize::Valid(0)); +} + +#[test] +fn test_knapsack_single_item() { + // Single item that fits + let problem = Knapsack::new(vec![3], vec![5], 3); + assert_eq!(problem.evaluate(&[1]), SolutionSize::Valid(5)); + assert_eq!(problem.evaluate(&[0]), SolutionSize::Valid(0)); + let solver = BruteForce::new(); + let solution = solver.find_best(&problem).unwrap(); + assert_eq!(problem.evaluate(&solution), SolutionSize::Valid(5)); +} + +#[test] +fn test_knapsack_greedy_not_optimal() { + // Classic case where greedy by value/weight ratio is suboptimal: + // Item 0: w=6, v=7, ratio=1.17 (greedy picks this first, then nothing else fits) + // Item 1: w=5, v=5, ratio=1.00 + // Item 2: w=5, v=5, ratio=1.00 + // Capacity=10. Greedy: {0} value=7. Optimal: {1,2} value=10. + let problem = Knapsack::new(vec![6, 5, 5], vec![7, 5, 5], 10); + let solver = BruteForce::new(); + let solution = solver.find_best(&problem).unwrap(); + assert_eq!(problem.evaluate(&solution), SolutionSize::Valid(10)); +} + +#[test] +#[should_panic(expected = "weights and values must have the same length")] +fn test_knapsack_mismatched_lengths() { + Knapsack::new(vec![1, 2], vec![3], 5); +}