Skip to main content

problemreductions/models/misc/
knapsack.rs

1//! Knapsack problem implementation.
2//!
3//! The 0-1 Knapsack problem asks for a subset of items that maximizes
4//! total value while respecting a weight capacity constraint.
5
6use crate::registry::{CreateSpec, ProblemSchemaEntry};
7use crate::traits::Problem;
8use crate::types::Max;
9use serde::{Deserialize, Serialize};
10
11inventory::submit! {
12    ProblemSchemaEntry {
13        name: "Knapsack",
14        display_name: "Knapsack",
15        aliases: &[],
16        dimensions: &[],
17        category: crate::registry::ProblemCategory::Misc,
18        module_path: module_path!(),
19        description: "Select items to maximize total value subject to weight capacity constraint",
20        fields: KnapsackCreateSpec::FIELDS,
21    }
22}
23
24/// The 0-1 Knapsack problem.
25///
26/// Given `n` items, each with nonnegative weight `w_i` and nonnegative value `v_i`,
27/// and a nonnegative capacity `C`,
28/// find a subset `S ⊆ {0, ..., n-1}` such that `∑_{i∈S} w_i ≤ C`,
29/// maximizing `∑_{i∈S} v_i`.
30///
31/// # Representation
32///
33/// Each item has a binary variable: `x_i = 1` if item `i` is selected, `0` otherwise.
34///
35/// # Example
36///
37/// ```
38/// use problemreductions::models::misc::Knapsack;
39/// use problemreductions::{Problem, BruteForce};
40///
41/// let problem = Knapsack::new(vec![2, 3, 4, 5], vec![3, 4, 5, 7], 7);
42/// let solver = BruteForce::new();
43/// let solution = solver.solve(&problem).unwrap();
44/// assert!(solution.is_some());
45/// ```
46#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct Knapsack {
48    #[serde(deserialize_with = "nonnegative_i64_vec::deserialize")]
49    weights: Vec<i64>,
50    #[serde(deserialize_with = "nonnegative_i64_vec::deserialize")]
51    values: Vec<i64>,
52    #[serde(deserialize_with = "nonnegative_i64::deserialize")]
53    capacity: i64,
54}
55
56#[derive(Debug, Deserialize, crate::CreateSpec)]
57struct KnapsackCreateSpec {
58    /// Nonnegative item weights; defaults to one per value.
59    weights: Option<Vec<i64>>,
60    /// Nonnegative item values.
61    values: Vec<i64>,
62    /// Nonnegative knapsack capacity.
63    capacity: i64,
64}
65impl TryFrom<KnapsackCreateSpec> for Knapsack {
66    type Error = crate::registry::ConstructionError;
67    fn try_from(spec: KnapsackCreateSpec) -> Result<Self, Self::Error> {
68        let count = spec.values.len();
69        let weights = spec.weights.unwrap_or_else(|| vec![1; count]);
70        if weights.len() != count {
71            return Err("weights length must equal values length".to_string().into());
72        }
73        if weights.iter().any(|&value| value < 0)
74            || spec.values.iter().any(|&value| value < 0)
75            || spec.capacity < 0
76        {
77            return Err("weights, values, and capacity must be nonnegative"
78                .to_string()
79                .into());
80        }
81        Ok(Self::new(weights, spec.values, spec.capacity))
82    }
83}
84
85impl Knapsack {
86    /// Create a new Knapsack instance.
87    ///
88    /// # Panics
89    /// Panics if `weights` and `values` have different lengths, or if any
90    /// weight, value, or the capacity is negative.
91    pub fn new(weights: Vec<i64>, values: Vec<i64>, capacity: i64) -> Self {
92        assert_eq!(
93            weights.len(),
94            values.len(),
95            "weights and values must have the same length"
96        );
97        assert!(
98            weights.iter().all(|&weight| weight >= 0),
99            "Knapsack weights must be nonnegative"
100        );
101        assert!(
102            values.iter().all(|&value| value >= 0),
103            "Knapsack values must be nonnegative"
104        );
105        assert!(capacity >= 0, "Knapsack capacity must be nonnegative");
106        Self {
107            weights,
108            values,
109            capacity,
110        }
111    }
112
113    /// Returns the item weights.
114    pub fn weights(&self) -> &[i64] {
115        &self.weights
116    }
117
118    /// Returns the item values.
119    pub fn values(&self) -> &[i64] {
120        &self.values
121    }
122
123    /// Returns the knapsack capacity.
124    pub fn capacity(&self) -> i64 {
125        self.capacity
126    }
127
128    /// Returns the number of items.
129    pub fn num_items(&self) -> usize {
130        self.weights.len()
131    }
132
133    /// Returns the number of binary slack bits used by the QUBO encoding.
134    ///
135    /// For positive capacity this is `floor(log2(C)) + 1`; for zero capacity we
136    /// keep one slack bit so the encoding shape remains uniform.
137    pub fn num_slack_bits(&self) -> usize {
138        if self.capacity == 0 {
139            1
140        } else {
141            self.capacity.ilog2() as usize + 1
142        }
143    }
144}
145
146impl Problem for Knapsack {
147    const NAME: &'static str = "Knapsack";
148    type Solution = Vec<bool>;
149    type Value = Max<i64>;
150
151    crate::problem_parameters![("capacity", capacity), ("num_items", num_items),];
152
153    fn variant() -> Vec<(&'static str, &'static str)> {
154        crate::variant_params![]
155    }
156
157    fn evaluate(
158        &self,
159        config: &Self::Solution,
160    ) -> Result<Max<i64>, crate::traits::EvaluationError> {
161        Ok({
162            if config.len() != self.num_items() {
163                return Err(crate::traits::EvaluationError::InvalidConfiguration(
164                    "item-selection length does not match the instance".into(),
165                ));
166            }
167            let total_weight = config
168                .iter()
169                .enumerate()
170                .filter(|(_, &x)| x)
171                .map(|(i, _)| self.weights[i])
172                .try_fold(0_i64, |total, weight| {
173                    total.checked_add(weight).ok_or_else(|| {
174                        crate::traits::EvaluationError::IntegerOverflow(
175                            "summing selected knapsack weights".into(),
176                        )
177                    })
178                })?;
179            if total_weight > self.capacity {
180                return Ok(Max(None));
181            }
182            let total_value = config
183                .iter()
184                .enumerate()
185                .filter(|(_, &x)| x)
186                .map(|(i, _)| self.values[i])
187                .try_fold(0_i64, |total, value| {
188                    total.checked_add(value).ok_or_else(|| {
189                        crate::traits::EvaluationError::IntegerOverflow(
190                            "summing selected knapsack values".into(),
191                        )
192                    })
193                })?;
194            Max(Some(total_value))
195        })
196    }
197}
198
199impl crate::solvers::BruteForceProblem for Knapsack {
200    fn dimensions(&self) -> Vec<usize> {
201        vec![2; self.num_items()]
202    }
203}
204
205crate::declare_variants! {
206    default Knapsack => "2^(num_items / 2)" create KnapsackCreateSpec,
207}
208
209crate::register_brute_force! {
210    Knapsack decode |_, indices: Vec<usize>| crate::config::config_to_bits(&indices),
211}
212
213mod nonnegative_i64 {
214    use serde::de::Error;
215    use serde::{Deserialize, Deserializer};
216
217    pub fn deserialize<'de, D>(deserializer: D) -> Result<i64, D::Error>
218    where
219        D: Deserializer<'de>,
220    {
221        let value = i64::deserialize(deserializer)?;
222        if value < 0 {
223            return Err(D::Error::custom(format!(
224                "expected nonnegative integer, got {value}"
225            )));
226        }
227        Ok(value)
228    }
229}
230
231mod nonnegative_i64_vec {
232    use serde::de::Error;
233    use serde::{Deserialize, Deserializer};
234
235    pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<i64>, D::Error>
236    where
237        D: Deserializer<'de>,
238    {
239        let values = Vec::<i64>::deserialize(deserializer)?;
240        if let Some(value) = values.iter().copied().find(|value| *value < 0) {
241            return Err(D::Error::custom(format!(
242                "expected nonnegative integers, got {value}"
243            )));
244        }
245        Ok(values)
246    }
247}
248
249#[cfg(feature = "example-db")]
250pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
251    // 4 items: weights [2,3,4,5], values [3,4,5,7], capacity 7
252    // Optimal: items 0,3 → weight=7, value=10
253    vec![crate::example_db::specs::ModelExampleSpec {
254        id: "knapsack",
255        instance: Box::new(Knapsack::new(vec![2, 3, 4, 5], vec![3, 4, 5, 7], 7)),
256        optimal_config: serde_json::json!(vec![true, false, false, true]),
257        optimal_value: serde_json::json!(10),
258    }]
259}
260
261#[cfg(test)]
262#[path = "../../unit_tests/models/misc/knapsack.rs"]
263mod tests;