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::{FieldInfo, 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        module_path: module_path!(),
19        description: "Select items to maximize total value subject to precedence constraints and weight capacity",
20        fields: &[
21            FieldInfo { name: "weights", type_name: "Vec<i64>", description: "Item weights w(u) for each item" },
22            FieldInfo { name: "values", type_name: "Vec<i64>", description: "Item values v(u) for each item" },
23            FieldInfo { name: "precedences", type_name: "Vec<(usize, usize)>", description: "Precedence pairs (a, b) meaning a must be included before b" },
24            FieldInfo { name: "capacity", type_name: "i64", description: "Knapsack capacity B" },
25        ],
26    }
27}
28
29/// The Partially Ordered Knapsack problem.
30///
31/// Given `n` items, each with weight `w(u)` and value `v(u)`, a partial order
32/// on the items (given as precedence pairs), and a capacity `C`, find a subset
33/// `S ⊆ {0,…,n-1}` that is downward-closed (if `i ∈ S` and `j ≺ i`, then `j ∈ S`),
34/// satisfies `∑_{i∈S} w_i ≤ C`, and maximizes `∑_{i∈S} v_i`.
35///
36/// # Representation
37///
38/// Each item has a binary variable: `x_u = 1` if item `u` is selected, `0` otherwise.
39/// Precedences are stored as `(a, b)` pairs meaning item `a` must be included
40/// whenever item `b` is included.
41///
42/// # Example
43///
44/// ```
45/// use problemreductions::models::misc::PartiallyOrderedKnapsack;
46/// use problemreductions::{Problem, Solver, BruteForce};
47///
48/// let problem = PartiallyOrderedKnapsack::new(
49///     vec![2, 3, 4, 1, 2, 3],  // weights
50///     vec![3, 2, 5, 4, 3, 8],  // values
51///     vec![(0, 2), (0, 3), (1, 4), (3, 5), (4, 5)],  // precedences
52///     11,  // capacity
53/// );
54/// let solver = BruteForce::new();
55/// let solution = solver.find_witness(&problem);
56/// assert!(solution.is_some());
57/// ```
58///
59// Raw serialization helper for [`PartiallyOrderedKnapsack`].
60#[derive(Serialize, Deserialize)]
61struct PartiallyOrderedKnapsackRaw {
62    weights: Vec<i64>,
63    values: Vec<i64>,
64    precedences: Vec<(usize, usize)>,
65    capacity: i64,
66}
67
68#[derive(Debug, Clone)]
69pub struct PartiallyOrderedKnapsack {
70    weights: Vec<i64>,
71    values: Vec<i64>,
72    precedences: Vec<(usize, usize)>,
73    capacity: i64,
74    /// Precomputed transitive predecessors for each item.
75    /// `predecessors[b]` contains all items that must be selected when `b` is selected.
76    predecessors: Vec<Vec<usize>>,
77}
78
79impl Serialize for PartiallyOrderedKnapsack {
80    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
81        PartiallyOrderedKnapsackRaw {
82            weights: self.weights.clone(),
83            values: self.values.clone(),
84            precedences: self.precedences.clone(),
85            capacity: self.capacity,
86        }
87        .serialize(serializer)
88    }
89}
90
91impl<'de> Deserialize<'de> for PartiallyOrderedKnapsack {
92    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
93        let raw = PartiallyOrderedKnapsackRaw::deserialize(deserializer)?;
94        Ok(Self::new(
95            raw.weights,
96            raw.values,
97            raw.precedences,
98            raw.capacity,
99        ))
100    }
101}
102
103impl PartiallyOrderedKnapsack {
104    /// Create a new PartiallyOrderedKnapsack instance.
105    ///
106    /// # Arguments
107    /// * `weights` - Weight w(u) for each item
108    /// * `values` - Value v(u) for each item
109    /// * `precedences` - Precedence pairs `(a, b)` meaning item `a` must be included before item `b`
110    /// * `capacity` - Knapsack capacity C
111    ///
112    /// # Panics
113    /// Panics if `weights` and `values` have different lengths, if any weight,
114    /// value, or capacity is negative, if any precedence index is out of bounds,
115    /// or if the precedences contain a cycle.
116    pub fn new(
117        weights: Vec<i64>,
118        values: Vec<i64>,
119        precedences: Vec<(usize, usize)>,
120        capacity: i64,
121    ) -> Self {
122        assert_eq!(
123            weights.len(),
124            values.len(),
125            "weights and values must have the same length"
126        );
127        assert!(capacity >= 0, "capacity must be non-negative");
128        for (i, &w) in weights.iter().enumerate() {
129            assert!(w >= 0, "weight[{i}] must be non-negative, got {w}");
130        }
131        for (i, &v) in values.iter().enumerate() {
132            assert!(v >= 0, "value[{i}] must be non-negative, got {v}");
133        }
134        let n = weights.len();
135        for &(a, b) in &precedences {
136            assert!(a < n, "precedence index {a} out of bounds (n={n})");
137            assert!(b < n, "precedence index {b} out of bounds (n={n})");
138        }
139        let predecessors = Self::compute_predecessors(&precedences, n);
140        // Check for cycles: if any item is its own transitive predecessor, the DAG has a cycle
141        for (i, preds) in predecessors.iter().enumerate() {
142            assert!(
143                !preds.contains(&i),
144                "precedences contain a cycle involving item {i}"
145            );
146        }
147        Self {
148            weights,
149            values,
150            precedences,
151            capacity,
152            predecessors,
153        }
154    }
155
156    /// Compute transitive predecessors for each item via Floyd-Warshall.
157    fn compute_predecessors(precedences: &[(usize, usize)], n: usize) -> Vec<Vec<usize>> {
158        let mut reachable = vec![vec![false; n]; n];
159        for &(a, b) in precedences {
160            reachable[a][b] = true;
161        }
162        for k in 0..n {
163            for i in 0..n {
164                for j in 0..n {
165                    if reachable[i][k] && reachable[k][j] {
166                        reachable[i][j] = true;
167                    }
168                }
169            }
170        }
171        (0..n)
172            .map(|b| (0..n).filter(|&a| reachable[a][b]).collect())
173            .collect()
174    }
175
176    /// Returns the item weights.
177    pub fn weights(&self) -> &[i64] {
178        &self.weights
179    }
180
181    /// Returns the item values.
182    pub fn values(&self) -> &[i64] {
183        &self.values
184    }
185
186    /// Returns the precedence pairs.
187    pub fn precedences(&self) -> &[(usize, usize)] {
188        &self.precedences
189    }
190
191    /// Returns the knapsack capacity.
192    pub fn capacity(&self) -> i64 {
193        self.capacity
194    }
195
196    /// Returns the number of items.
197    pub fn num_items(&self) -> usize {
198        self.weights.len()
199    }
200
201    /// Returns the number of precedence relations.
202    pub fn num_precedences(&self) -> usize {
203        self.precedences.len()
204    }
205
206    /// Check if the selected items form a downward-closed set.
207    ///
208    /// Uses precomputed transitive predecessors: if item `b` is selected,
209    /// all its predecessors must also be selected.
210    fn is_downward_closed(&self, config: &[usize]) -> bool {
211        for (b, preds) in self.predecessors.iter().enumerate() {
212            if config[b] == 1 {
213                for &a in preds {
214                    if config[a] != 1 {
215                        return false;
216                    }
217                }
218            }
219        }
220        true
221    }
222}
223
224impl Problem for PartiallyOrderedKnapsack {
225    const NAME: &'static str = "PartiallyOrderedKnapsack";
226    type Value = Max<i64>;
227
228    fn variant() -> Vec<(&'static str, &'static str)> {
229        crate::variant_params![]
230    }
231
232    fn dims(&self) -> Vec<usize> {
233        vec![2; self.num_items()]
234    }
235
236    fn evaluate(&self, config: &[usize]) -> Max<i64> {
237        if config.len() != self.num_items() {
238            return Max(None);
239        }
240        if config.iter().any(|&v| v >= 2) {
241            return Max(None);
242        }
243        // Check downward-closure (precedence constraints)
244        if !self.is_downward_closed(config) {
245            return Max(None);
246        }
247        // Check capacity constraint
248        let total_weight: i64 = config
249            .iter()
250            .enumerate()
251            .filter(|(_, &x)| x == 1)
252            .map(|(i, _)| self.weights[i])
253            .sum();
254        if total_weight > self.capacity {
255            return Max(None);
256        }
257        // Compute total value
258        let total_value: i64 = config
259            .iter()
260            .enumerate()
261            .filter(|(_, &x)| x == 1)
262            .map(|(i, _)| self.values[i])
263            .sum();
264        Max(Some(total_value))
265    }
266}
267
268crate::declare_variants! {
269    default PartiallyOrderedKnapsack => "2^num_items",
270}
271
272#[cfg(feature = "example-db")]
273pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
274    vec![crate::example_db::specs::ModelExampleSpec {
275        id: "partially_ordered_knapsack",
276        instance: Box::new(PartiallyOrderedKnapsack::new(
277            vec![2, 3, 4, 1, 2, 3],
278            vec![3, 2, 5, 4, 3, 8],
279            vec![(0, 2), (0, 3), (1, 4), (3, 5), (4, 5)],
280            11,
281        )),
282        optimal_config: vec![1, 1, 0, 1, 1, 1],
283        optimal_value: serde_json::json!(20),
284    }]
285}
286
287#[cfg(test)]
288#[path = "../../unit_tests/models/misc/partially_ordered_knapsack.rs"]
289mod tests;