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        if spec.weights.len() != spec.values.len() {
88            return Err("weights and values must have the same length"
89                .to_string()
90                .into());
91        }
92        if spec.capacity < 0 {
93            return Err("capacity must be non-negative".to_string().into());
94        }
95        if let Some((index, weight)) = spec
96            .weights
97            .iter()
98            .enumerate()
99            .find(|(_, weight)| **weight < 0)
100        {
101            return Err(format!("weight[{index}] must be non-negative, got {weight}").into());
102        }
103        if let Some((index, value)) = spec
104            .values
105            .iter()
106            .enumerate()
107            .find(|(_, value)| **value < 0)
108        {
109            return Err(format!("value[{index}] must be non-negative, got {value}").into());
110        }
111        let precedences = spec.precedences.unwrap_or_default();
112        let num_items = spec.weights.len();
113        if let Some(&(pred, succ)) = precedences
114            .iter()
115            .find(|&&(pred, succ)| pred >= num_items || succ >= num_items)
116        {
117            return Err(format!(
118                "precedence ({pred}, {succ}) is out of range for {num_items} items"
119            )
120            .into());
121        }
122        let predecessors = Self::compute_predecessors(&precedences, num_items);
123        if let Some(item) = predecessors
124            .iter()
125            .enumerate()
126            .find_map(|(item, preds)| preds.contains(&item).then_some(item))
127        {
128            return Err(format!("precedences contain a cycle involving item {item}").into());
129        }
130        Ok(Self::new(
131            spec.weights,
132            spec.values,
133            precedences,
134            spec.capacity,
135        ))
136    }
137}
138
139impl Serialize for PartiallyOrderedKnapsack {
140    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
141        PartiallyOrderedKnapsackRaw {
142            weights: self.weights.clone(),
143            values: self.values.clone(),
144            precedences: self.precedences.clone(),
145            capacity: self.capacity,
146        }
147        .serialize(serializer)
148    }
149}
150
151impl<'de> Deserialize<'de> for PartiallyOrderedKnapsack {
152    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
153        let raw = PartiallyOrderedKnapsackRaw::deserialize(deserializer)?;
154        Ok(Self::new(
155            raw.weights,
156            raw.values,
157            raw.precedences,
158            raw.capacity,
159        ))
160    }
161}
162
163impl PartiallyOrderedKnapsack {
164    /// Create a new PartiallyOrderedKnapsack instance.
165    ///
166    /// # Arguments
167    /// * `weights` - Weight w(u) for each item
168    /// * `values` - Value v(u) for each item
169    /// * `precedences` - Precedence pairs `(a, b)` meaning item `a` must be included before item `b`
170    /// * `capacity` - Knapsack capacity C
171    ///
172    /// # Panics
173    /// Panics if `weights` and `values` have different lengths, if any weight,
174    /// value, or capacity is negative, if any precedence index is out of bounds,
175    /// or if the precedences contain a cycle.
176    pub fn new(
177        weights: Vec<i64>,
178        values: Vec<i64>,
179        precedences: Vec<(usize, usize)>,
180        capacity: i64,
181    ) -> Self {
182        assert_eq!(
183            weights.len(),
184            values.len(),
185            "weights and values must have the same length"
186        );
187        assert!(capacity >= 0, "capacity must be non-negative");
188        for (i, &w) in weights.iter().enumerate() {
189            assert!(w >= 0, "weight[{i}] must be non-negative, got {w}");
190        }
191        for (i, &v) in values.iter().enumerate() {
192            assert!(v >= 0, "value[{i}] must be non-negative, got {v}");
193        }
194        let n = weights.len();
195        for &(a, b) in &precedences {
196            assert!(a < n, "precedence index {a} out of bounds (n={n})");
197            assert!(b < n, "precedence index {b} out of bounds (n={n})");
198        }
199        let predecessors = Self::compute_predecessors(&precedences, n);
200        // Check for cycles: if any item is its own transitive predecessor, the DAG has a cycle
201        for (i, preds) in predecessors.iter().enumerate() {
202            assert!(
203                !preds.contains(&i),
204                "precedences contain a cycle involving item {i}"
205            );
206        }
207        Self {
208            weights,
209            values,
210            precedences,
211            capacity,
212            predecessors,
213        }
214    }
215
216    /// Compute transitive predecessors for each item via Floyd-Warshall.
217    fn compute_predecessors(precedences: &[(usize, usize)], n: usize) -> Vec<Vec<usize>> {
218        let mut reachable = vec![vec![false; n]; n];
219        for &(a, b) in precedences {
220            reachable[a][b] = true;
221        }
222        for k in 0..n {
223            for i in 0..n {
224                for j in 0..n {
225                    if reachable[i][k] && reachable[k][j] {
226                        reachable[i][j] = true;
227                    }
228                }
229            }
230        }
231        (0..n)
232            .map(|b| (0..n).filter(|&a| reachable[a][b]).collect())
233            .collect()
234    }
235
236    /// Returns the item weights.
237    pub fn weights(&self) -> &[i64] {
238        &self.weights
239    }
240
241    /// Returns the item values.
242    pub fn values(&self) -> &[i64] {
243        &self.values
244    }
245
246    /// Returns the precedence pairs.
247    pub fn precedences(&self) -> &[(usize, usize)] {
248        &self.precedences
249    }
250
251    /// Returns the knapsack capacity.
252    pub fn capacity(&self) -> i64 {
253        self.capacity
254    }
255
256    /// Returns the number of items.
257    pub fn num_items(&self) -> usize {
258        self.weights.len()
259    }
260
261    /// Returns the number of precedence relations.
262    pub fn num_precedences(&self) -> usize {
263        self.precedences.len()
264    }
265
266    /// Check if the selected items form a downward-closed set.
267    ///
268    /// Uses precomputed transitive predecessors: if item `b` is selected,
269    /// all its predecessors must also be selected.
270    fn is_downward_closed(&self, config: &[bool]) -> bool {
271        for (b, preds) in self.predecessors.iter().enumerate() {
272            if config[b] {
273                for &a in preds {
274                    if !config[a] {
275                        return false;
276                    }
277                }
278            }
279        }
280        true
281    }
282}
283
284impl Problem for PartiallyOrderedKnapsack {
285    const NAME: &'static str = "PartiallyOrderedKnapsack";
286    type Solution = Vec<bool>;
287    type Value = Max<i64>;
288
289    crate::problem_parameters![
290        ("num_items", num_items),
291        ("num_precedences", num_precedences),
292    ];
293
294    fn variant() -> Vec<(&'static str, &'static str)> {
295        crate::variant_params![]
296    }
297
298    fn evaluate(
299        &self,
300        config: &Self::Solution,
301    ) -> Result<Max<i64>, crate::traits::EvaluationError> {
302        Ok({
303            if config.len() != self.num_items() {
304                return Err(crate::traits::EvaluationError::InvalidConfiguration(
305                    "item-selection length does not match the instance".into(),
306                ));
307            }
308            // Check downward-closure (precedence constraints)
309            if !self.is_downward_closed(config) {
310                return Ok(Max(None));
311            }
312            // Check capacity constraint
313            let total_weight = config
314                .iter()
315                .enumerate()
316                .filter(|(_, &x)| x)
317                .map(|(i, _)| self.weights[i])
318                .try_fold(0_i64, |total, weight| {
319                    total.checked_add(weight).ok_or_else(|| {
320                        crate::traits::EvaluationError::IntegerOverflow(
321                            "summing selected partially ordered knapsack weights".into(),
322                        )
323                    })
324                })?;
325            if total_weight > self.capacity {
326                return Ok(Max(None));
327            }
328            // Compute total value
329            let total_value = config
330                .iter()
331                .enumerate()
332                .filter(|(_, &x)| x)
333                .map(|(i, _)| self.values[i])
334                .try_fold(0_i64, |total, value| {
335                    total.checked_add(value).ok_or_else(|| {
336                        crate::traits::EvaluationError::IntegerOverflow(
337                            "summing selected partially ordered knapsack values".into(),
338                        )
339                    })
340                })?;
341            Max(Some(total_value))
342        })
343    }
344}
345
346impl crate::solvers::BruteForceProblem for PartiallyOrderedKnapsack {
347    fn dimensions(&self) -> Vec<usize> {
348        vec![2; self.num_items()]
349    }
350}
351
352crate::declare_variants! {
353    default PartiallyOrderedKnapsack => "2^num_items" create PartiallyOrderedKnapsackCreateSpec,
354}
355
356crate::register_brute_force! {
357    PartiallyOrderedKnapsack decode |_, indices: Vec<usize>| crate::config::config_to_bits(&indices),
358}
359
360#[cfg(feature = "example-db")]
361pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
362    vec![crate::example_db::specs::ModelExampleSpec {
363        id: "partially_ordered_knapsack",
364        instance: Box::new(PartiallyOrderedKnapsack::new(
365            vec![2, 3, 4, 1, 2, 3],
366            vec![3, 2, 5, 4, 3, 8],
367            vec![(0, 2), (0, 3), (1, 4), (3, 5), (4, 5)],
368            11,
369        )),
370        optimal_config: serde_json::json!(vec![true, true, false, true, true, true]),
371        optimal_value: serde_json::json!(20),
372    }]
373}
374
375#[cfg(test)]
376#[path = "../../unit_tests/models/misc/partially_ordered_knapsack.rs"]
377mod tests;