Skip to main content

converge_optimization/knapsack/
mod.rs

1//! Knapsack problem solvers
2//!
3//! The knapsack problem: select items to maximize value within capacity.
4//!
5//! ## Variants
6//!
7//! - 0-1 Knapsack: Take or leave each item
8//! - Bounded: Limited copies of each item
9//! - Unbounded: Unlimited copies
10//! - Multidimensional: Multiple capacity constraints
11
12use crate::{Error, Result, SolverParams, SolverStats, SolverStatus, Value, Weight};
13use serde::{Deserialize, Serialize};
14
15/// A knapsack problem instance
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct KnapsackProblem {
18    /// Weight of each item
19    pub weights: Vec<Weight>,
20    /// Value of each item
21    pub values: Vec<Value>,
22    /// Knapsack capacity
23    pub capacity: Weight,
24}
25
26impl KnapsackProblem {
27    /// Create a new knapsack problem
28    pub fn new(weights: Vec<Weight>, values: Vec<Value>, capacity: Weight) -> Result<Self> {
29        if weights.len() != values.len() {
30            return Err(Error::dimension_mismatch(weights.len(), values.len()));
31        }
32        Ok(Self {
33            weights,
34            values,
35            capacity,
36        })
37    }
38
39    /// Number of items
40    pub fn num_items(&self) -> usize {
41        self.weights.len()
42    }
43
44    /// Validate the problem
45    pub fn validate(&self) -> Result<()> {
46        if self.weights.len() != self.values.len() {
47            return Err(Error::dimension_mismatch(
48                self.weights.len(),
49                self.values.len(),
50            ));
51        }
52        if self.capacity < 0 {
53            return Err(Error::invalid_input("negative capacity"));
54        }
55        for &w in &self.weights {
56            if w < 0 {
57                return Err(Error::invalid_input("negative weight"));
58            }
59        }
60        Ok(())
61    }
62}
63
64/// Solution to a knapsack problem
65#[derive(Debug, Clone, Serialize, Deserialize)]
66pub struct KnapsackSolution {
67    /// Selected items (indices)
68    pub selected: Vec<usize>,
69    /// Total value of selected items
70    pub total_value: Value,
71    /// Total weight of selected items
72    pub total_weight: Weight,
73    /// Solver status
74    pub status: SolverStatus,
75    /// Solver statistics
76    pub stats: SolverStats,
77}
78
79/// Trait for knapsack solvers
80pub trait KnapsackSolver {
81    /// Solve the knapsack problem
82    fn solve(&self, problem: &KnapsackProblem, params: &SolverParams) -> Result<KnapsackSolution>;
83
84    /// Solver name
85    fn name(&self) -> &'static str;
86}
87
88/// Dynamic programming solver for 0-1 knapsack
89pub struct DynamicProgrammingSolver;
90
91impl KnapsackSolver for DynamicProgrammingSolver {
92    fn solve(&self, problem: &KnapsackProblem, _params: &SolverParams) -> Result<KnapsackSolution> {
93        solve_dp(problem)
94    }
95
96    fn name(&self) -> &'static str {
97        "dynamic_programming"
98    }
99}
100
101/// Solve 0-1 knapsack using dynamic programming
102pub fn solve(problem: &KnapsackProblem) -> Result<KnapsackSolution> {
103    problem.validate()?;
104    solve_dp(problem)
105}
106
107fn solve_dp(problem: &KnapsackProblem) -> Result<KnapsackSolution> {
108    let start = std::time::Instant::now();
109    let n = problem.num_items();
110    let capacity = problem.capacity as usize;
111
112    if n == 0 || capacity == 0 {
113        return Ok(KnapsackSolution {
114            selected: vec![],
115            total_value: 0,
116            total_weight: 0,
117            status: SolverStatus::Optimal,
118            stats: SolverStats::default(),
119        });
120    }
121
122    // Check for overflow potential
123    if capacity > 10_000_000 {
124        return Err(Error::invalid_input(
125            "capacity too large for DP (use branch-and-bound instead)",
126        ));
127    }
128
129    // dp[w] = max value achievable with capacity w
130    let mut dp = vec![0i64; capacity + 1];
131
132    // Track which items were used
133    let mut keep = vec![vec![false; capacity + 1]; n];
134
135    for i in 0..n {
136        let w = problem.weights[i] as usize;
137        let v = problem.values[i];
138
139        // Process in reverse to avoid using item multiple times
140        for c in (w..=capacity).rev() {
141            if dp[c - w] + v > dp[c] {
142                dp[c] = dp[c - w] + v;
143                keep[i][c] = true;
144            }
145        }
146    }
147
148    // Backtrack to find selected items
149    let mut selected = Vec::new();
150    let mut remaining = capacity;
151
152    for i in (0..n).rev() {
153        if keep[i][remaining] {
154            selected.push(i);
155            remaining -= problem.weights[i] as usize;
156        }
157    }
158
159    selected.reverse();
160
161    let total_value = dp[capacity];
162    let total_weight: Weight = selected.iter().map(|&i| problem.weights[i]).sum();
163
164    let elapsed = start.elapsed().as_secs_f64();
165
166    Ok(KnapsackSolution {
167        selected,
168        total_value,
169        total_weight,
170        status: SolverStatus::Optimal,
171        stats: SolverStats {
172            solve_time_seconds: elapsed,
173            iterations: n * capacity,
174            objective_value: Some(total_value as f64),
175            ..Default::default()
176        },
177    })
178}
179
180#[cfg(test)]
181mod tests {
182    use super::*;
183
184    #[test]
185    fn test_simple_knapsack() {
186        let problem = KnapsackProblem::new(vec![10, 20, 30], vec![60, 100, 120], 50).unwrap();
187
188        let solution = solve(&problem).unwrap();
189
190        // Optimal: items 1 and 2 (weights 20+30=50, values 100+120=220)
191        assert_eq!(solution.total_value, 220);
192        assert_eq!(solution.total_weight, 50);
193        assert!(solution.selected.contains(&1));
194        assert!(solution.selected.contains(&2));
195    }
196
197    #[test]
198    fn test_empty_knapsack() {
199        let problem = KnapsackProblem::new(vec![], vec![], 100).unwrap();
200        let solution = solve(&problem).unwrap();
201        assert_eq!(solution.total_value, 0);
202        assert!(solution.selected.is_empty());
203    }
204
205    #[test]
206    fn test_zero_capacity() {
207        let problem = KnapsackProblem::new(vec![10, 20], vec![100, 200], 0).unwrap();
208        let solution = solve(&problem).unwrap();
209        assert_eq!(solution.total_value, 0);
210    }
211
212    #[test]
213    fn test_single_item_fits() {
214        let problem = KnapsackProblem::new(vec![5], vec![10], 10).unwrap();
215        let solution = solve(&problem).unwrap();
216        assert_eq!(solution.total_value, 10);
217        assert_eq!(solution.selected, vec![0]);
218    }
219
220    #[test]
221    fn test_single_item_too_heavy() {
222        let problem = KnapsackProblem::new(vec![15], vec![10], 10).unwrap();
223        let solution = solve(&problem).unwrap();
224        assert_eq!(solution.total_value, 0);
225        assert!(solution.selected.is_empty());
226    }
227}