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)]
47#[serde(try_from = "KnapsackData")]
48pub struct Knapsack {
49    weights: Vec<i64>,
50    values: Vec<i64>,
51    capacity: i64,
52}
53
54#[derive(Deserialize)]
55struct KnapsackData {
56    weights: Vec<i64>,
57    values: Vec<i64>,
58    capacity: i64,
59}
60
61impl TryFrom<KnapsackData> for Knapsack {
62    type Error = crate::registry::ConstructionError;
63
64    fn try_from(data: KnapsackData) -> Result<Self, Self::Error> {
65        Self::try_new(data.weights, data.values, data.capacity)
66    }
67}
68
69#[derive(Debug, Deserialize, crate::CreateSpec)]
70struct KnapsackCreateSpec {
71    /// Nonnegative item weights; defaults to one per value.
72    weights: Option<Vec<i64>>,
73    /// Nonnegative item values.
74    values: Vec<i64>,
75    /// Nonnegative knapsack capacity.
76    capacity: i64,
77}
78impl TryFrom<KnapsackCreateSpec> for Knapsack {
79    type Error = crate::registry::ConstructionError;
80    fn try_from(spec: KnapsackCreateSpec) -> Result<Self, Self::Error> {
81        let weights = spec.weights.unwrap_or_else(|| vec![1; spec.values.len()]);
82        Self::try_new(weights, spec.values, spec.capacity)
83    }
84}
85
86impl Knapsack {
87    /// Create a new Knapsack instance.
88    ///
89    /// # Panics
90    /// Panics if `weights` and `values` have different lengths, or if any
91    /// weight, value, or the capacity is negative.
92    pub fn new(weights: Vec<i64>, values: Vec<i64>, capacity: i64) -> Self {
93        Self::try_new(weights, values, capacity).unwrap_or_else(|error| panic!("{error}"))
94    }
95
96    fn try_new(
97        weights: Vec<i64>,
98        values: Vec<i64>,
99        capacity: i64,
100    ) -> Result<Self, crate::registry::ConstructionError> {
101        if weights.len() != values.len() {
102            return Err("weights and values must have the same length".into());
103        }
104        if weights.iter().any(|&weight| weight < 0) {
105            return Err("Knapsack weights must be nonnegative".into());
106        }
107        if values.iter().any(|&value| value < 0) {
108            return Err("Knapsack values must be nonnegative".into());
109        }
110        if capacity < 0 {
111            return Err("Knapsack capacity must be nonnegative".into());
112        }
113        Ok(Self {
114            weights,
115            values,
116            capacity,
117        })
118    }
119
120    /// Returns the item weights.
121    pub fn weights(&self) -> &[i64] {
122        &self.weights
123    }
124
125    /// Returns the item values.
126    pub fn values(&self) -> &[i64] {
127        &self.values
128    }
129
130    /// Returns the knapsack capacity.
131    pub fn capacity(&self) -> i64 {
132        self.capacity
133    }
134
135    /// Binary digit count of the capacity, with a minimum of one for zero.
136    pub fn capacity_bits(&self) -> u64 {
137        crate::types::max_numeric_magnitude_bits([self.capacity])
138    }
139
140    /// Returns the number of items.
141    pub fn num_items(&self) -> usize {
142        self.weights.len()
143    }
144
145    /// Returns the number of binary slack bits used by the QUBO encoding.
146    ///
147    /// For positive capacity this is `floor(log2(C)) + 1`; for zero capacity we
148    /// keep one slack bit so the encoding shape remains uniform.
149    pub fn num_slack_bits(&self) -> usize {
150        usize::try_from(self.capacity_bits()).expect("capacity bit length fits usize")
151    }
152}
153
154impl Problem for Knapsack {
155    const NAME: &'static str = "Knapsack";
156    type Solution = Vec<bool>;
157    type Value = Max<i64>;
158
159    crate::problem_parameters![
160        ("capacity", capacity),
161        ("capacity_bits", capacity_bits),
162        ("num_items", num_items),
163    ];
164
165    fn variant() -> Vec<(&'static str, &'static str)> {
166        crate::variant_params![]
167    }
168
169    fn evaluate(
170        &self,
171        config: &Self::Solution,
172    ) -> Result<Max<i64>, crate::traits::EvaluationError> {
173        Ok({
174            if config.len() != self.num_items() {
175                return Err(crate::traits::EvaluationError::InvalidConfiguration(
176                    "item-selection length does not match the instance".into(),
177                ));
178            }
179            let total_weight = config
180                .iter()
181                .enumerate()
182                .filter(|(_, &x)| x)
183                .map(|(i, _)| self.weights[i])
184                .try_fold(0_i64, |total, weight| {
185                    total.checked_add(weight).ok_or_else(|| {
186                        crate::traits::EvaluationError::IntegerOverflow(
187                            "summing selected knapsack weights".into(),
188                        )
189                    })
190                })?;
191            if total_weight > self.capacity {
192                return Ok(Max(None));
193            }
194            let total_value = config
195                .iter()
196                .enumerate()
197                .filter(|(_, &x)| x)
198                .map(|(i, _)| self.values[i])
199                .try_fold(0_i64, |total, value| {
200                    total.checked_add(value).ok_or_else(|| {
201                        crate::traits::EvaluationError::IntegerOverflow(
202                            "summing selected knapsack values".into(),
203                        )
204                    })
205                })?;
206            Max(Some(total_value))
207        })
208    }
209}
210
211impl crate::solvers::BruteForceProblem for Knapsack {
212    fn dimensions(&self) -> Vec<usize> {
213        vec![2; self.num_items()]
214    }
215}
216
217crate::declare_variants! {
218    default Knapsack => "2^(num_items / 2)" create KnapsackCreateSpec,
219}
220
221crate::register_brute_force! {
222    Knapsack decode |_, indices: Vec<usize>| crate::config::config_to_bits(&indices),
223}
224
225#[cfg(feature = "example-db")]
226pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
227    // 4 items: weights [2,3,4,5], values [3,4,5,7], capacity 7
228    // Optimal: items 0,3 → weight=7, value=10
229    vec![crate::example_db::specs::ModelExampleSpec {
230        id: "knapsack",
231        instance: Box::new(Knapsack::new(vec![2, 3, 4, 5], vec![3, 4, 5, 7], 7)),
232        optimal_config: serde_json::json!(vec![true, false, false, true]),
233        optimal_value: serde_json::json!(10),
234    }]
235}
236
237#[cfg(test)]
238#[path = "../../unit_tests/models/misc/knapsack.rs"]
239mod tests;