Skip to main content

problemreductions/models/set/
integer_knapsack.rs

1//! Integer Knapsack problem implementation.
2//!
3//! The Integer Knapsack problem generalizes the 0-1 Knapsack by allowing
4//! each item to be selected with a non-negative integer multiplicity.
5
6use crate::registry::{FieldInfo, ProblemSchemaEntry};
7use crate::traits::Problem;
8use crate::types::Max;
9use serde::{Deserialize, Serialize};
10
11inventory::submit! {
12    ProblemSchemaEntry {
13        name: "IntegerKnapsack",
14        display_name: "Integer Knapsack",
15        aliases: &[],
16        dimensions: &[],
17        module_path: module_path!(),
18        description: "Select items with integer multiplicities to maximize total value subject to capacity constraint",
19        fields: &[
20            FieldInfo { name: "sizes", type_name: "Vec<i64>", description: "Positive item sizes s(u)" },
21            FieldInfo { name: "values", type_name: "Vec<i64>", description: "Positive item values v(u)" },
22            FieldInfo { name: "capacity", type_name: "i64", description: "Nonnegative knapsack capacity B" },
23        ],
24    }
25}
26
27/// The Integer Knapsack problem.
28///
29/// Given `n` items, each with positive size `s_i` and positive value `v_i`,
30/// and a nonnegative capacity `B`,
31/// find non-negative integer multiplicities `c_0, ..., c_{n-1}` such that
32/// `sum c_i * s_i <= B`, maximizing `sum c_i * v_i`.
33///
34/// # Representation
35///
36/// Variable `i` has domain `{0, ..., floor(B / s_i)}` representing the
37/// multiplicity of item `i`.
38///
39/// # Example
40///
41/// ```
42/// use problemreductions::models::set::IntegerKnapsack;
43/// use problemreductions::{Problem, Solver, BruteForce};
44///
45/// let problem = IntegerKnapsack::new(vec![3, 4, 5, 2, 7], vec![4, 5, 7, 3, 9], 15);
46/// let solver = BruteForce::new();
47/// let solution = solver.find_witness(&problem);
48/// assert!(solution.is_some());
49/// ```
50#[derive(Debug, Clone, Serialize)]
51#[serde(into = "RawIntegerKnapsack")]
52pub struct IntegerKnapsack {
53    sizes: Vec<i64>,
54    values: Vec<i64>,
55    capacity: i64,
56}
57
58impl IntegerKnapsack {
59    /// Create a new IntegerKnapsack instance.
60    ///
61    /// # Panics
62    /// Panics if `sizes` and `values` have different lengths, or if any
63    /// size or value is not positive, or if capacity is negative.
64    pub fn new(sizes: Vec<i64>, values: Vec<i64>, capacity: i64) -> Self {
65        assert_eq!(
66            sizes.len(),
67            values.len(),
68            "sizes and values must have the same length"
69        );
70        assert!(
71            sizes.iter().all(|&s| s > 0),
72            "IntegerKnapsack sizes must be positive"
73        );
74        assert!(
75            values.iter().all(|&v| v > 0),
76            "IntegerKnapsack values must be positive"
77        );
78        assert!(
79            capacity >= 0,
80            "IntegerKnapsack capacity must be nonnegative"
81        );
82        Self {
83            sizes,
84            values,
85            capacity,
86        }
87    }
88
89    /// Returns the item sizes.
90    pub fn sizes(&self) -> &[i64] {
91        &self.sizes
92    }
93
94    /// Returns the item values.
95    pub fn values(&self) -> &[i64] {
96        &self.values
97    }
98
99    /// Returns the knapsack capacity.
100    pub fn capacity(&self) -> i64 {
101        self.capacity
102    }
103
104    /// Returns the number of items.
105    pub fn num_items(&self) -> usize {
106        self.sizes.len()
107    }
108}
109
110impl Problem for IntegerKnapsack {
111    const NAME: &'static str = "IntegerKnapsack";
112    type Value = Max<i64>;
113
114    fn variant() -> Vec<(&'static str, &'static str)> {
115        crate::variant_params![]
116    }
117
118    fn dims(&self) -> Vec<usize> {
119        self.sizes
120            .iter()
121            .map(|&s| (self.capacity / s + 1) as usize)
122            .collect()
123    }
124
125    fn evaluate(&self, config: &[usize]) -> Max<i64> {
126        if config.len() != self.num_items() {
127            return Max(None);
128        }
129        let dims = self.dims();
130        if config.iter().zip(dims.iter()).any(|(&c, &d)| c >= d) {
131            return Max(None);
132        }
133        let total_size: i64 = config
134            .iter()
135            .enumerate()
136            .map(|(i, &c)| c as i64 * self.sizes[i])
137            .sum();
138        if total_size > self.capacity {
139            return Max(None);
140        }
141        let total_value: i64 = config
142            .iter()
143            .enumerate()
144            .map(|(i, &c)| c as i64 * self.values[i])
145            .sum();
146        Max(Some(total_value))
147    }
148}
149
150crate::declare_variants! {
151    default IntegerKnapsack => "(capacity + 1)^num_items",
152}
153
154/// Raw representation for serde deserialization with full validation.
155#[derive(Deserialize, Serialize)]
156struct RawIntegerKnapsack {
157    sizes: Vec<i64>,
158    values: Vec<i64>,
159    capacity: i64,
160}
161
162impl From<IntegerKnapsack> for RawIntegerKnapsack {
163    fn from(ik: IntegerKnapsack) -> Self {
164        RawIntegerKnapsack {
165            sizes: ik.sizes,
166            values: ik.values,
167            capacity: ik.capacity,
168        }
169    }
170}
171
172impl TryFrom<RawIntegerKnapsack> for IntegerKnapsack {
173    type Error = String;
174
175    fn try_from(raw: RawIntegerKnapsack) -> Result<Self, String> {
176        if raw.sizes.len() != raw.values.len() {
177            return Err(format!(
178                "sizes and values must have the same length, got {} and {}",
179                raw.sizes.len(),
180                raw.values.len()
181            ));
182        }
183        if let Some(&s) = raw.sizes.iter().find(|&&s| s <= 0) {
184            return Err(format!("expected positive sizes, got {s}"));
185        }
186        if let Some(&v) = raw.values.iter().find(|&&v| v <= 0) {
187            return Err(format!("expected positive values, got {v}"));
188        }
189        if raw.capacity < 0 {
190            return Err(format!(
191                "expected nonnegative capacity, got {}",
192                raw.capacity
193            ));
194        }
195        Ok(IntegerKnapsack {
196            sizes: raw.sizes,
197            values: raw.values,
198            capacity: raw.capacity,
199        })
200    }
201}
202
203impl<'de> Deserialize<'de> for IntegerKnapsack {
204    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
205    where
206        D: serde::Deserializer<'de>,
207    {
208        let raw = RawIntegerKnapsack::deserialize(deserializer)?;
209        IntegerKnapsack::try_from(raw).map_err(serde::de::Error::custom)
210    }
211}
212
213#[cfg(feature = "example-db")]
214pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
215    // 5 items: sizes [3,4,5,2,7], values [4,5,7,3,9], capacity 15
216    // Optimal: c=(0,0,1,5,0) → total_size=5+10=15, total_value=7+15=22
217    vec![crate::example_db::specs::ModelExampleSpec {
218        id: "integer-knapsack",
219        instance: Box::new(IntegerKnapsack::new(
220            vec![3, 4, 5, 2, 7],
221            vec![4, 5, 7, 3, 9],
222            15,
223        )),
224        optimal_config: vec![0, 0, 1, 5, 0],
225        optimal_value: serde_json::json!(22),
226    }]
227}
228
229#[cfg(test)]
230#[path = "../../unit_tests/models/set/integer_knapsack.rs"]
231mod tests;