Skip to main content

problemreductions/models/misc/
partially_ordered_knapsack.rs

1//! Partially Ordered Knapsack problem implementation.
2//!
3//! A knapsack variant where items are subject to a partial order: including
4//! an item requires including all its predecessors (downward-closed set).
5//! NP-complete in the strong sense (Garey & Johnson, A6 MP12).
6
7use crate::registry::{CreateSpec, ProblemSchemaEntry};
8use crate::traits::Problem;
9use crate::types::Max;
10use serde::{Deserialize, Serialize};
11
12inventory::submit! {
13    ProblemSchemaEntry {
14        name: "PartiallyOrderedKnapsack",
15        display_name: "Partially Ordered Knapsack",
16        aliases: &["POK"],
17        dimensions: &[],
18        category: crate::registry::ProblemCategory::Misc,
19        module_path: module_path!(),
20        description: "Select items to maximize total value subject to precedence constraints and weight capacity",
21        fields: PartiallyOrderedKnapsackCreateSpec::FIELDS,
22    }
23}
24
25/// The Partially Ordered Knapsack problem.
26///
27/// Given `n` items, each with weight `w(u)` and value `v(u)`, a partial order
28/// on the items (given as precedence pairs), and a capacity `C`, find a subset
29/// `S ⊆ {0,…,n-1}` that is downward-closed (if `i ∈ S` and `j ≺ i`, then `j ∈ S`),
30/// satisfies `∑_{i∈S} w_i ≤ C`, and maximizes `∑_{i∈S} v_i`.
31///
32/// # Representation
33///
34/// Each item has a binary variable: `x_u = 1` if item `u` is selected, `0` otherwise.
35/// Precedences are stored as `(a, b)` pairs meaning item `a` must be included
36/// whenever item `b` is included.
37///
38/// # Example
39///
40/// ```
41/// use problemreductions::models::misc::PartiallyOrderedKnapsack;
42/// use problemreductions::{Problem, BruteForce};
43///
44/// let problem = PartiallyOrderedKnapsack::new(
45///     vec![2, 3, 4, 1, 2, 3],  // weights
46///     vec![3, 2, 5, 4, 3, 8],  // values
47///     vec![(0, 2), (0, 3), (1, 4), (3, 5), (4, 5)],  // precedences
48///     11,  // capacity
49/// );
50/// let solver = BruteForce::new();
51/// let solution = solver.solve(&problem).unwrap();
52/// assert!(solution.is_some());
53/// ```
54///
55// Raw serialization helper for [`PartiallyOrderedKnapsack`].
56#[derive(Serialize, Deserialize)]
57struct PartiallyOrderedKnapsackRaw {
58    weights: Vec<i64>,
59    values: Vec<i64>,
60    precedences: Vec<(usize, usize)>,
61    capacity: i64,
62}
63
64#[derive(Debug, Clone)]
65pub struct PartiallyOrderedKnapsack {
66    weights: Vec<i64>,
67    values: Vec<i64>,
68    precedences: Vec<(usize, usize)>,
69    capacity: i64,
70    /// Precomputed transitive predecessors for each item.
71    /// `predecessors[b]` contains all items that must be selected when `b` is selected.
72    predecessors: Vec<Vec<usize>>,
73}
74
75#[derive(Debug, Deserialize, crate::CreateSpec)]
76struct PartiallyOrderedKnapsackCreateSpec {
77    weights: Vec<i64>,
78    values: Vec<i64>,
79    precedences: Option<Vec<(usize, usize)>>,
80    capacity: i64,
81}
82
83impl TryFrom<PartiallyOrderedKnapsackCreateSpec> for PartiallyOrderedKnapsack {
84    type Error = crate::registry::ConstructionError;
85
86    fn try_from(spec: PartiallyOrderedKnapsackCreateSpec) -> Result<Self, Self::Error> {
87        Self::try_new(
88            spec.weights,
89            spec.values,
90            spec.precedences.unwrap_or_default(),
91            spec.capacity,
92        )
93    }
94}
95
96impl Serialize for PartiallyOrderedKnapsack {
97    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
98        PartiallyOrderedKnapsackRaw {
99            weights: self.weights.clone(),
100            values: self.values.clone(),
101            precedences: self.precedences.clone(),
102            capacity: self.capacity,
103        }
104        .serialize(serializer)
105    }
106}
107
108impl<'de> Deserialize<'de> for PartiallyOrderedKnapsack {
109    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
110        let raw = PartiallyOrderedKnapsackRaw::deserialize(deserializer)?;
111        Self::try_new(raw.weights, raw.values, raw.precedences, raw.capacity)
112            .map_err(serde::de::Error::custom)
113    }
114}
115
116impl PartiallyOrderedKnapsack {
117    /// Create a new PartiallyOrderedKnapsack instance.
118    ///
119    /// # Arguments
120    /// * `weights` - Weight w(u) for each item
121    /// * `values` - Value v(u) for each item
122    /// * `precedences` - Precedence pairs `(a, b)` meaning item `a` must be included before item `b`
123    /// * `capacity` - Knapsack capacity C
124    ///
125    /// # Panics
126    /// Panics if `weights` and `values` have different lengths, if any weight,
127    /// value, or capacity is negative, if any precedence index is out of bounds,
128    /// or if the precedences contain a cycle.
129    pub fn new(
130        weights: Vec<i64>,
131        values: Vec<i64>,
132        precedences: Vec<(usize, usize)>,
133        capacity: i64,
134    ) -> Self {
135        Self::try_new(weights, values, precedences, capacity)
136            .unwrap_or_else(|error| panic!("{error}"))
137    }
138
139    fn try_new(
140        weights: Vec<i64>,
141        values: Vec<i64>,
142        precedences: Vec<(usize, usize)>,
143        capacity: i64,
144    ) -> Result<Self, crate::registry::ConstructionError> {
145        if weights.len() != values.len() {
146            return Err("weights and values must have the same length".into());
147        }
148        if capacity < 0 {
149            return Err("capacity must be non-negative".into());
150        }
151        for (i, &w) in weights.iter().enumerate() {
152            if w < 0 {
153                return Err(format!("weight[{i}] must be non-negative, got {w}").into());
154            }
155        }
156        for (i, &v) in values.iter().enumerate() {
157            if v < 0 {
158                return Err(format!("value[{i}] must be non-negative, got {v}").into());
159            }
160        }
161        let n = weights.len();
162        for &(a, b) in &precedences {
163            if a >= n {
164                return Err(format!("precedence index {a} out of bounds (n={n})").into());
165            }
166            if b >= n {
167                return Err(format!("precedence index {b} out of bounds (n={n})").into());
168            }
169        }
170        let predecessors = Self::compute_predecessors(&precedences, n);
171        // Check for cycles: if any item is its own transitive predecessor, the DAG has a cycle
172        for (i, preds) in predecessors.iter().enumerate() {
173            if preds.contains(&i) {
174                return Err(format!("precedences contain a cycle involving item {i}").into());
175            }
176        }
177        Ok(Self {
178            weights,
179            values,
180            precedences,
181            capacity,
182            predecessors,
183        })
184    }
185
186    /// Compute transitive predecessors for each item via Floyd-Warshall.
187    fn compute_predecessors(precedences: &[(usize, usize)], n: usize) -> Vec<Vec<usize>> {
188        let mut reachable = vec![vec![false; n]; n];
189        for &(a, b) in precedences {
190            reachable[a][b] = true;
191        }
192        for k in 0..n {
193            for i in 0..n {
194                for j in 0..n {
195                    if reachable[i][k] && reachable[k][j] {
196                        reachable[i][j] = true;
197                    }
198                }
199            }
200        }
201        (0..n)
202            .map(|b| (0..n).filter(|&a| reachable[a][b]).collect())
203            .collect()
204    }
205
206    /// Returns the item weights.
207    pub fn weights(&self) -> &[i64] {
208        &self.weights
209    }
210
211    /// Returns the item values.
212    pub fn values(&self) -> &[i64] {
213        &self.values
214    }
215
216    /// Returns the precedence pairs.
217    pub fn precedences(&self) -> &[(usize, usize)] {
218        &self.precedences
219    }
220
221    /// Returns the knapsack capacity.
222    pub fn capacity(&self) -> i64 {
223        self.capacity
224    }
225
226    /// Smallest h >= 1 with every item weight and the capacity strictly below 2^h.
227    pub fn max_weight_bits(&self) -> u64 {
228        crate::types::max_numeric_magnitude_bits(
229            self.weights
230                .iter()
231                .copied()
232                .chain(std::iter::once(self.capacity)),
233        )
234    }
235
236    /// Returns the number of items.
237    pub fn num_items(&self) -> usize {
238        self.weights.len()
239    }
240
241    /// Returns the number of precedence relations.
242    pub fn num_precedences(&self) -> usize {
243        self.precedences.len()
244    }
245
246    /// Check if the selected items form a downward-closed set.
247    ///
248    /// Uses precomputed transitive predecessors: if item `b` is selected,
249    /// all its predecessors must also be selected.
250    fn is_downward_closed(&self, config: &[bool]) -> bool {
251        for (b, preds) in self.predecessors.iter().enumerate() {
252            if config[b] {
253                for &a in preds {
254                    if !config[a] {
255                        return false;
256                    }
257                }
258            }
259        }
260        true
261    }
262}
263
264impl Problem for PartiallyOrderedKnapsack {
265    const NAME: &'static str = "PartiallyOrderedKnapsack";
266    type Solution = Vec<bool>;
267    type Value = Max<i64>;
268
269    crate::problem_parameters![
270        ("max_weight_bits", max_weight_bits),
271        ("num_items", num_items),
272        ("num_precedences", num_precedences),
273    ];
274
275    fn variant() -> Vec<(&'static str, &'static str)> {
276        crate::variant_params![]
277    }
278
279    fn evaluate(
280        &self,
281        config: &Self::Solution,
282    ) -> Result<Max<i64>, crate::traits::EvaluationError> {
283        Ok({
284            if config.len() != self.num_items() {
285                return Err(crate::traits::EvaluationError::InvalidConfiguration(
286                    "item-selection length does not match the instance".into(),
287                ));
288            }
289            // Check downward-closure (precedence constraints)
290            if !self.is_downward_closed(config) {
291                return Ok(Max(None));
292            }
293            // Check capacity constraint
294            let total_weight = config
295                .iter()
296                .enumerate()
297                .filter(|(_, &x)| x)
298                .map(|(i, _)| self.weights[i])
299                .try_fold(0_i64, |total, weight| {
300                    total.checked_add(weight).ok_or_else(|| {
301                        crate::traits::EvaluationError::IntegerOverflow(
302                            "summing selected partially ordered knapsack weights".into(),
303                        )
304                    })
305                })?;
306            if total_weight > self.capacity {
307                return Ok(Max(None));
308            }
309            // Compute total value
310            let total_value = config
311                .iter()
312                .enumerate()
313                .filter(|(_, &x)| x)
314                .map(|(i, _)| self.values[i])
315                .try_fold(0_i64, |total, value| {
316                    total.checked_add(value).ok_or_else(|| {
317                        crate::traits::EvaluationError::IntegerOverflow(
318                            "summing selected partially ordered knapsack values".into(),
319                        )
320                    })
321                })?;
322            Max(Some(total_value))
323        })
324    }
325}
326
327impl crate::solvers::BruteForceProblem for PartiallyOrderedKnapsack {
328    fn dimensions(&self) -> Vec<usize> {
329        vec![2; self.num_items()]
330    }
331}
332
333crate::declare_variants! {
334    default PartiallyOrderedKnapsack => "2^num_items" create PartiallyOrderedKnapsackCreateSpec,
335}
336
337crate::register_brute_force! {
338    PartiallyOrderedKnapsack decode |_, indices: Vec<usize>| crate::config::config_to_bits(&indices),
339}
340
341#[cfg(feature = "example-db")]
342pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
343    vec![crate::example_db::specs::ModelExampleSpec {
344        id: "partially_ordered_knapsack",
345        instance: Box::new(PartiallyOrderedKnapsack::new(
346            vec![2, 3, 4, 1, 2, 3],
347            vec![3, 2, 5, 4, 3, 8],
348            vec![(0, 2), (0, 3), (1, 4), (3, 5), (4, 5)],
349            11,
350        )),
351        optimal_config: serde_json::json!(vec![true, true, false, true, true, true]),
352        optimal_value: serde_json::json!(20),
353    }]
354}
355
356#[cfg(test)]
357#[path = "../../unit_tests/models/misc/partially_ordered_knapsack.rs"]
358mod tests;