converge_optimization/knapsack/
mod.rs1use crate::{Error, Result, SolverParams, SolverStats, SolverStatus, Value, Weight};
13use serde::{Deserialize, Serialize};
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct KnapsackProblem {
18 pub weights: Vec<Weight>,
20 pub values: Vec<Value>,
22 pub capacity: Weight,
24}
25
26impl KnapsackProblem {
27 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 pub fn num_items(&self) -> usize {
41 self.weights.len()
42 }
43
44 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#[derive(Debug, Clone, Serialize, Deserialize)]
66pub struct KnapsackSolution {
67 pub selected: Vec<usize>,
69 pub total_value: Value,
71 pub total_weight: Weight,
73 pub status: SolverStatus,
75 pub stats: SolverStats,
77}
78
79pub trait KnapsackSolver {
81 fn solve(&self, problem: &KnapsackProblem, params: &SolverParams) -> Result<KnapsackSolution>;
83
84 fn name(&self) -> &'static str;
86}
87
88pub 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
101pub 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 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 let mut dp = vec![0i64; capacity + 1];
131
132 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 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 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 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}