From 8bad353ee946dbdc9b2f233d1c78fcffd831d85a Mon Sep 17 00:00:00 2001 From: zazabap Date: Fri, 13 Mar 2026 09:03:40 +0000 Subject: [PATCH 01/10] Add plan for #534: PartiallyOrderedKnapsack model Co-Authored-By: Claude Opus 4.6 --- .../2026-03-13-partially-ordered-knapsack.md | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 docs/plans/2026-03-13-partially-ordered-knapsack.md diff --git a/docs/plans/2026-03-13-partially-ordered-knapsack.md b/docs/plans/2026-03-13-partially-ordered-knapsack.md new file mode 100644 index 000000000..7be158546 --- /dev/null +++ b/docs/plans/2026-03-13-partially-ordered-knapsack.md @@ -0,0 +1,66 @@ +# Plan: Add PartiallyOrderedKnapsack Model + +Fixes #534 + +## Overview + +Add the PartiallyOrderedKnapsack problem model — a knapsack variant where items are subject to a partial order (precedence constraints). Including an item requires including all its predecessors. Modeled as an optimization problem (maximize total value subject to precedence + capacity constraints), consistent with the existing Knapsack model. + +## Design Decisions + +- **Optimization, not satisfaction**: Following the check-issue recommendation and consistency with existing `Knapsack`, model as `OptimizationProblem` with `Direction::Maximize`. The capacity constraint is part of feasibility; the objective is total value. No `target_value` field needed. +- **Precedences as edge list**: Store `Vec<(usize, usize)>` where `(a, b)` means item `a` must precede item `b` (a < b in the partial order). The full precedence requirement is the transitive closure. +- **Complexity**: Use `"2^num_items"` as a baseline (naive enumeration). The problem is strongly NP-hard, so no pseudo-polynomial algorithm exists for general partial orders. +- **Category**: `misc/` — unique input structure (items + partial order + capacity), doesn't fit graph/formula/set/algebraic. + +## Steps + +### Step 1: Implement the model (`src/models/misc/partially_ordered_knapsack.rs`) + +- `ProblemSchemaEntry` with fields: `sizes`, `values`, `precedences`, `capacity` +- Struct: `PartiallyOrderedKnapsack { sizes: Vec, values: Vec, precedences: Vec<(usize, usize)>, capacity: i64 }` +- Constructor: `new(sizes, values, precedences, capacity)` — assert sizes.len() == values.len(), validate precedence indices +- Getters: `sizes()`, `values()`, `capacity()`, `precedences()`, `num_items()`, `num_precedences()` +- `Problem` impl: `NAME = "PartiallyOrderedKnapsack"`, `Metric = SolutionSize`, `dims() = vec![2; n]`, `variant() = variant_params![]` +- `evaluate()`: check config length/values, check downward-closure (compute transitive closure, verify for each selected item all predecessors are selected), check capacity, return total value +- `OptimizationProblem` impl: `Value = i64`, `direction() = Maximize` +- `declare_variants!`: `PartiallyOrderedKnapsack => "2^num_items"` +- Test link: `#[cfg(test)] #[path = "../../unit_tests/models/misc/partially_ordered_knapsack.rs"] mod tests;` + +### Step 2: Register the model + +- `src/models/misc/mod.rs`: add `mod partially_ordered_knapsack;` and `pub use` +- `src/models/mod.rs`: add to `misc` re-export line + +### Step 3: Register in CLI + +- `problemreductions-cli/src/dispatch.rs`: add `load_problem` and `serialize_any_problem` arms +- `problemreductions-cli/src/problem_name.rs`: add `"partiallyorderedknapsack" | "pok"` alias (POK is not well-established, so only add lowercase identity mapping, no short alias) + +### Step 4: Add CLI creation support + +- `problemreductions-cli/src/commands/create.rs`: add `"PartiallyOrderedKnapsack"` arm parsing `--sizes`, `--values`, `--capacity`, `--precedences` +- `problemreductions-cli/src/cli.rs`: add `--values` and `--precedences` flags to `CreateArgs`, update `all_data_flags_empty()`, update help table +- Note: `--sizes` and `--capacity` already exist from BinPacking + +### Step 5: Write unit tests (`src/unit_tests/models/misc/partially_ordered_knapsack.rs`) + +- `test_partially_ordered_knapsack_basic`: construct instance, verify dims, getters +- `test_partially_ordered_knapsack_evaluate_valid`: valid downward-closed set within capacity +- `test_partially_ordered_knapsack_evaluate_precedence_violation`: select item without predecessor +- `test_partially_ordered_knapsack_evaluate_overweight`: valid precedence but over capacity +- `test_partially_ordered_knapsack_evaluate_empty`: empty selection +- `test_partially_ordered_knapsack_brute_force`: solver finds optimal +- `test_partially_ordered_knapsack_serialization`: round-trip serde +- `test_partially_ordered_knapsack_direction`: verify Maximize +- Use example from issue: items a-f with precedences a Date: Fri, 13 Mar 2026 09:11:34 +0000 Subject: [PATCH 02/10] Implement #534: Add PartiallyOrderedKnapsack model - New optimization problem model in src/models/misc/ - Precedence-constrained knapsack with downward-closure check - Full CLI support (dispatch, alias, create) - 17 unit tests covering evaluation, solver, serialization - Paper documentation with problem definition and references Co-Authored-By: Claude Opus 4.6 --- docs/paper/reductions.typ | 9 + docs/paper/references.bib | 22 ++ problemreductions-cli/src/cli.rs | 7 + problemreductions-cli/src/commands/create.rs | 50 +++- problemreductions-cli/src/dispatch.rs | 4 +- problemreductions-cli/src/problem_name.rs | 1 + src/models/misc/mod.rs | 3 + src/models/misc/partially_ordered_knapsack.rs | 223 ++++++++++++++++++ src/models/mod.rs | 2 +- .../models/misc/partially_ordered_knapsack.rs | 186 +++++++++++++++ 10 files changed, 504 insertions(+), 3 deletions(-) create mode 100644 src/models/misc/partially_ordered_knapsack.rs create mode 100644 src/unit_tests/models/misc/partially_ordered_knapsack.rs diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index 565abdb14..96da21005 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -41,6 +41,7 @@ "QUBO": [QUBO], "ILP": [Integer Linear Programming], "Knapsack": [Knapsack], + "PartiallyOrderedKnapsack": [Partially Ordered Knapsack], "Satisfiability": [SAT], "KSatisfiability": [$k$-SAT], "CircuitSAT": [CircuitSAT], @@ -980,6 +981,14 @@ Biclique Cover is equivalent to factoring the biadjacency matrix $M$ of the bipa *Example.* Let $n = 4$ items with weights $(2, 3, 4, 5)$, values $(3, 4, 5, 7)$, and capacity $C = 7$. Selecting $S = {1, 2}$ (items with weights 3 and 4) gives total weight $3 + 4 = 7 lt.eq C$ and total value $4 + 5 = 9$. Selecting $S = {0, 3}$ (weights 2 and 5) gives weight $2 + 5 = 7 lt.eq C$ and value $3 + 7 = 10$, which is optimal. ] +#problem-def("PartiallyOrderedKnapsack")[ + Given a finite set $U$ with $|U| = n$ items, a partial order $<$ on $U$ (given by its cover relations), for each $u in U$ a size $s(u) in ZZ^+$ and a value $v(u) in ZZ^+$, and a capacity $B in ZZ^+$, find a downward-closed subset $U' subset.eq U$ (i.e., if $u in U'$ and $u' < u$ then $u' in U'$) maximizing $sum_(u in U') v(u)$ subject to $sum_(u in U') s(u) lt.eq B$. +][ + Garey and Johnson's problem A6 MP12 @garey1979. Unlike standard Knapsack, the partial order constraint makes the problem _strongly_ NP-complete --- it remains NP-complete even when $s(u) = v(u)$ for all $u$, so no pseudo-polynomial algorithm exists unless $P = "NP"$. The problem arises in manufacturing scheduling, project selection, and mining operations. For tree partial orders, Johnson and Niemi @johnson1983 gave pseudo-polynomial $O(n dot B)$ tree DP and an FPTAS. Kolliopoulos and Steiner @kolliopoulos2007 extended the FPTAS to 2-dimensional partial orders with $O(n^4 slash epsilon)$ running time. + + *Example.* Consider $n = 6$ items with partial order given by cover relations $a < c$, $a < d$, $b < e$, $d < f$, $e < f$. Sizes $(2, 3, 4, 1, 2, 3)$, values $(3, 2, 5, 4, 3, 8)$, and capacity $B = 11$. Selecting $U' = {a, b, d, e, f}$ is downward-closed (all predecessors included), has total size $2 + 3 + 1 + 2 + 3 = 11 lt.eq B$, and total value $3 + 2 + 4 + 3 + 8 = 20$. Adding $c$ would exceed capacity ($15 > 11$). +] + #problem-def("SubsetSum")[ Given a finite set $A = {a_0, dots, a_(n-1)}$ with sizes $s(a_i) in ZZ^+$ and a target $B in ZZ^+$, determine whether there exists a subset $A' subset.eq A$ such that $sum_(a in A') s(a) = B$. ][ diff --git a/docs/paper/references.bib b/docs/paper/references.bib index 0ec874f61..2aff1fd0d 100644 --- a/docs/paper/references.bib +++ b/docs/paper/references.bib @@ -456,3 +456,25 @@ @article{cygan2014 note = {Conference version: STOC 2014}, doi = {10.1137/140990255} } + +@article{johnson1983, + author = {David S. Johnson and Kenneth A. Niemi}, + title = {On Knapsacks, Partitions, and a New Dynamic Programming Technique for Trees}, + journal = {Mathematics of Operations Research}, + volume = {8}, + number = {1}, + pages = {1--14}, + year = {1983}, + doi = {10.1287/moor.8.1.1} +} + +@article{kolliopoulos2007, + author = {Stavros G. Kolliopoulos and George Steiner}, + title = {Partially Ordered Knapsack and Applications to Scheduling}, + journal = {Discrete Applied Mathematics}, + volume = {155}, + number = {8}, + pages = {889--897}, + year = {2007}, + doi = {10.1016/j.dam.2006.09.003} +} diff --git a/problemreductions-cli/src/cli.rs b/problemreductions-cli/src/cli.rs index 91792e20b..5cc80fb34 100644 --- a/problemreductions-cli/src/cli.rs +++ b/problemreductions-cli/src/cli.rs @@ -218,6 +218,7 @@ Flags by problem type: BMF --matrix (0/1), --rank CVP --basis, --target-vec [--bounds] FVS --arcs [--weights] [--num-vertices] + PartiallyOrderedKnapsack --sizes, --values, --capacity, --item-precedences ILP, CircuitSAT (via reduction only) Geometry graph variants (use slash notation, e.g., MIS/KingsSubgraph): @@ -332,6 +333,12 @@ pub struct CreateArgs { /// Directed arcs for directed graph problems (e.g., 0>1,1>2,2>0) #[arg(long)] pub arcs: Option, + /// Item values (e.g., 3,4,5,7) for PartiallyOrderedKnapsack + #[arg(long)] + pub values: Option, + /// Precedence pairs (e.g., "0>2,0>3,1>4") for PartiallyOrderedKnapsack + #[arg(long)] + pub item_precedences: Option, } #[derive(clap::Args)] diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index 2df4f0994..4059b1031 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -6,7 +6,7 @@ use crate::util; use anyhow::{bail, Context, Result}; use problemreductions::models::algebraic::{ClosestVectorProblem, BMF}; use problemreductions::models::graph::GraphPartitioning; -use problemreductions::models::misc::{BinPacking, PaintShop}; +use problemreductions::models::misc::{BinPacking, PaintShop, PartiallyOrderedKnapsack}; use problemreductions::prelude::*; use problemreductions::registry::collect_schemas; use problemreductions::topology::{ @@ -48,6 +48,8 @@ fn all_data_flags_empty(args: &CreateArgs) -> bool { && args.target_vec.is_none() && args.bounds.is_none() && args.arcs.is_none() + && args.values.is_none() + && args.item_precedences.is_none() } fn type_format_hint(type_name: &str, graph_type: Option<&str>) -> &'static str { @@ -509,6 +511,52 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { ) } + // PartiallyOrderedKnapsack + "PartiallyOrderedKnapsack" => { + let sizes_str = args.sizes.as_deref().ok_or_else(|| { + anyhow::anyhow!( + "PartiallyOrderedKnapsack requires --sizes, --values, --capacity, and --item-precedences\n\n\ + Usage: pred create PartiallyOrderedKnapsack --sizes 2,3,4,1,2,3 --values 3,2,5,4,3,8 --item-precedences \"0>2,0>3,1>4,3>5,4>5\" --capacity 11" + ) + })?; + let values_str = args.values.as_deref().ok_or_else(|| { + anyhow::anyhow!("PartiallyOrderedKnapsack requires --values (e.g., 3,2,5,4,3,8)") + })?; + let cap_str = args.capacity.as_deref().ok_or_else(|| { + anyhow::anyhow!("PartiallyOrderedKnapsack requires --capacity (e.g., 11)") + })?; + let sizes: Vec = util::parse_comma_list(sizes_str)?; + let values: Vec = util::parse_comma_list(values_str)?; + let capacity: i64 = cap_str.parse()?; + let precedences = match args.item_precedences.as_deref() { + Some(s) if !s.trim().is_empty() => s + .split(',') + .map(|pair| { + let parts: Vec<&str> = pair.trim().split('>').collect(); + anyhow::ensure!( + parts.len() == 2, + "Invalid precedence format '{}', expected 'a>b'", + pair.trim() + ); + Ok(( + parts[0].trim().parse::()?, + parts[1].trim().parse::()?, + )) + }) + .collect::>>()?, + _ => vec![], + }; + ( + ser(PartiallyOrderedKnapsack::new( + sizes, + values, + precedences, + capacity, + ))?, + resolved_variant.clone(), + ) + } + _ => bail!("{}", crate::problem_name::unknown_problem_error(canonical)), }; diff --git a/problemreductions-cli/src/dispatch.rs b/problemreductions-cli/src/dispatch.rs index 49dd523cb..9de768fa0 100644 --- a/problemreductions-cli/src/dispatch.rs +++ b/problemreductions-cli/src/dispatch.rs @@ -1,6 +1,6 @@ use anyhow::{bail, Context, Result}; use problemreductions::models::algebraic::{ClosestVectorProblem, ILP}; -use problemreductions::models::misc::{BinPacking, Knapsack, SubsetSum}; +use problemreductions::models::misc::{BinPacking, Knapsack, PartiallyOrderedKnapsack, SubsetSum}; use problemreductions::prelude::*; use problemreductions::rules::{MinimizeSteps, ReductionGraph}; use problemreductions::solvers::{BruteForce, ILPSolver, Solver}; @@ -246,6 +246,7 @@ pub fn load_problem( _ => deser_opt::>(data), }, "Knapsack" => deser_opt::(data), + "PartiallyOrderedKnapsack" => deser_opt::(data), "MinimumFeedbackVertexSet" => deser_opt::>(data), "SubsetSum" => deser_sat::(data), _ => bail!("{}", crate::problem_name::unknown_problem_error(&canonical)), @@ -309,6 +310,7 @@ pub fn serialize_any_problem( _ => try_ser::>(any), }, "Knapsack" => try_ser::(any), + "PartiallyOrderedKnapsack" => try_ser::(any), "MinimumFeedbackVertexSet" => try_ser::>(any), "SubsetSum" => try_ser::(any), _ => bail!("{}", crate::problem_name::unknown_problem_error(&canonical)), diff --git a/problemreductions-cli/src/problem_name.rs b/problemreductions-cli/src/problem_name.rs index 2b6c8c737..7990c2b2d 100644 --- a/problemreductions-cli/src/problem_name.rs +++ b/problemreductions-cli/src/problem_name.rs @@ -54,6 +54,7 @@ pub fn resolve_alias(input: &str) -> String { "binpacking" => "BinPacking".to_string(), "cvp" | "closestvectorproblem" => "ClosestVectorProblem".to_string(), "knapsack" => "Knapsack".to_string(), + "partiallyorderedknapsack" => "PartiallyOrderedKnapsack".to_string(), "fvs" | "minimumfeedbackvertexset" => "MinimumFeedbackVertexSet".to_string(), "subsetsum" => "SubsetSum".to_string(), _ => input.to_string(), // pass-through for exact names diff --git a/src/models/misc/mod.rs b/src/models/misc/mod.rs index 36ebe905b..de6c9f70e 100644 --- a/src/models/misc/mod.rs +++ b/src/models/misc/mod.rs @@ -5,16 +5,19 @@ //! - [`Factoring`]: Integer factorization //! - [`Knapsack`]: 0-1 Knapsack (maximize value subject to weight capacity) //! - [`PaintShop`]: Minimize color switches in paint shop scheduling +//! - [`PartiallyOrderedKnapsack`]: Knapsack with precedence constraints //! - [`SubsetSum`]: Find a subset summing to exactly a target value mod bin_packing; pub(crate) mod factoring; mod knapsack; pub(crate) mod paintshop; +mod partially_ordered_knapsack; mod subset_sum; pub use bin_packing::BinPacking; pub use factoring::Factoring; pub use knapsack::Knapsack; pub use paintshop::PaintShop; +pub use partially_ordered_knapsack::PartiallyOrderedKnapsack; pub use subset_sum::SubsetSum; diff --git a/src/models/misc/partially_ordered_knapsack.rs b/src/models/misc/partially_ordered_knapsack.rs new file mode 100644 index 000000000..052158085 --- /dev/null +++ b/src/models/misc/partially_ordered_knapsack.rs @@ -0,0 +1,223 @@ +//! Partially Ordered Knapsack problem implementation. +//! +//! A knapsack variant where items are subject to a partial order: including +//! an item requires including all its predecessors (downward-closed set). +//! NP-complete in the strong sense (Garey & Johnson, A6 MP12). + +use crate::registry::{FieldInfo, ProblemSchemaEntry}; +use crate::traits::{OptimizationProblem, Problem}; +use crate::types::{Direction, SolutionSize}; +use serde::{Deserialize, Serialize}; + +inventory::submit! { + ProblemSchemaEntry { + name: "PartiallyOrderedKnapsack", + module_path: module_path!(), + description: "Select items to maximize total value subject to precedence constraints and weight capacity", + fields: &[ + FieldInfo { name: "sizes", type_name: "Vec", description: "Item sizes s(u) for each item" }, + FieldInfo { name: "values", type_name: "Vec", description: "Item values v(u) for each item" }, + FieldInfo { name: "precedences", type_name: "Vec<(usize, usize)>", description: "Precedence pairs (a, b) meaning a must be included before b" }, + FieldInfo { name: "capacity", type_name: "i64", description: "Knapsack capacity B" }, + ], + } +} + +/// The Partially Ordered Knapsack problem. +/// +/// Given `n` items, each with size `s(u)` and value `v(u)`, a partial order +/// on the items (given as precedence pairs), and a capacity `B`, find a subset +/// `U' ⊆ U` that is downward-closed (if `u ∈ U'` and `u' < u`, then `u' ∈ U'`), +/// satisfies `∑_{u∈U'} s(u) ≤ B`, and maximizes `∑_{u∈U'} v(u)`. +/// +/// # Representation +/// +/// Each item has a binary variable: `x_u = 1` if item `u` is selected, `0` otherwise. +/// Precedences are stored as `(a, b)` pairs meaning item `a` must be included +/// whenever item `b` is included. +/// +/// # Example +/// +/// ``` +/// use problemreductions::models::misc::PartiallyOrderedKnapsack; +/// use problemreductions::{Problem, Solver, BruteForce}; +/// +/// let problem = PartiallyOrderedKnapsack::new( +/// vec![2, 3, 4, 1, 2, 3], // sizes +/// vec![3, 2, 5, 4, 3, 8], // values +/// vec![(0, 2), (0, 3), (1, 4), (3, 5), (4, 5)], // precedences +/// 11, // capacity +/// ); +/// let solver = BruteForce::new(); +/// let solution = solver.find_best(&problem); +/// assert!(solution.is_some()); +/// ``` +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PartiallyOrderedKnapsack { + sizes: Vec, + values: Vec, + precedences: Vec<(usize, usize)>, + capacity: i64, +} + +impl PartiallyOrderedKnapsack { + /// Create a new PartiallyOrderedKnapsack instance. + /// + /// # Arguments + /// * `sizes` - Size s(u) for each item + /// * `values` - Value v(u) for each item + /// * `precedences` - Precedence pairs `(a, b)` meaning item `a` must be included before item `b` + /// * `capacity` - Knapsack capacity B + /// + /// # Panics + /// Panics if `sizes` and `values` have different lengths, or if any precedence + /// index is out of bounds. + pub fn new( + sizes: Vec, + values: Vec, + precedences: Vec<(usize, usize)>, + capacity: i64, + ) -> Self { + assert_eq!( + sizes.len(), + values.len(), + "sizes and values must have the same length" + ); + let n = sizes.len(); + for &(a, b) in &precedences { + assert!(a < n, "precedence index {a} out of bounds (n={n})"); + assert!(b < n, "precedence index {b} out of bounds (n={n})"); + } + Self { + sizes, + values, + precedences, + capacity, + } + } + + /// Returns the item sizes. + pub fn sizes(&self) -> &[i64] { + &self.sizes + } + + /// Returns the item values. + pub fn values(&self) -> &[i64] { + &self.values + } + + /// Returns the precedence pairs. + pub fn precedences(&self) -> &[(usize, usize)] { + &self.precedences + } + + /// Returns the knapsack capacity. + pub fn capacity(&self) -> i64 { + self.capacity + } + + /// Returns the number of items. + pub fn num_items(&self) -> usize { + self.sizes.len() + } + + /// Returns the number of precedence relations. + pub fn num_precedences(&self) -> usize { + self.precedences.len() + } + + /// Check if the selected items form a downward-closed set. + /// + /// Uses the transitive closure of the precedence relation: if item `b` is + /// selected and `a` is a (transitive) predecessor of `b`, then `a` must + /// also be selected. + fn is_downward_closed(&self, config: &[usize]) -> bool { + let n = self.num_items(); + // Build adjacency matrix for transitive closure + let mut reachable = vec![vec![false; n]; n]; + for &(a, b) in &self.precedences { + reachable[a][b] = true; + } + // Floyd-Warshall for transitive closure + for k in 0..n { + for i in 0..n { + for j in 0..n { + if reachable[i][k] && reachable[k][j] { + reachable[i][j] = true; + } + } + } + } + // Check: for every selected item b, all predecessors must be selected + for b in 0..n { + if config[b] == 1 { + for a in 0..n { + if reachable[a][b] && config[a] != 1 { + return false; + } + } + } + } + true + } +} + +impl Problem for PartiallyOrderedKnapsack { + const NAME: &'static str = "PartiallyOrderedKnapsack"; + type Metric = SolutionSize; + + fn variant() -> Vec<(&'static str, &'static str)> { + crate::variant_params![] + } + + fn dims(&self) -> Vec { + vec![2; self.num_items()] + } + + fn evaluate(&self, config: &[usize]) -> SolutionSize { + if config.len() != self.num_items() { + return SolutionSize::Invalid; + } + if config.iter().any(|&v| v >= 2) { + return SolutionSize::Invalid; + } + // Check downward-closure (precedence constraints) + if !self.is_downward_closed(config) { + return SolutionSize::Invalid; + } + // Check capacity constraint + let total_size: i64 = config + .iter() + .enumerate() + .filter(|(_, &x)| x == 1) + .map(|(i, _)| self.sizes[i]) + .sum(); + if total_size > self.capacity { + return SolutionSize::Invalid; + } + // Compute total value + let total_value: i64 = config + .iter() + .enumerate() + .filter(|(_, &x)| x == 1) + .map(|(i, _)| self.values[i]) + .sum(); + SolutionSize::Valid(total_value) + } +} + +impl OptimizationProblem for PartiallyOrderedKnapsack { + type Value = i64; + + fn direction(&self) -> Direction { + Direction::Maximize + } +} + +crate::declare_variants! { + PartiallyOrderedKnapsack => "2^num_items", +} + +#[cfg(test)] +#[path = "../../unit_tests/models/misc/partially_ordered_knapsack.rs"] +mod tests; diff --git a/src/models/mod.rs b/src/models/mod.rs index ceb584ce2..c5617485c 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -16,5 +16,5 @@ pub use graph::{ MaximumIndependentSet, MaximumMatching, MinimumDominatingSet, MinimumFeedbackVertexSet, MinimumVertexCover, SpinGlass, TravelingSalesman, }; -pub use misc::{BinPacking, Factoring, Knapsack, PaintShop, SubsetSum}; +pub use misc::{BinPacking, Factoring, Knapsack, PaintShop, PartiallyOrderedKnapsack, SubsetSum}; pub use set::{MaximumSetPacking, MinimumSetCovering}; diff --git a/src/unit_tests/models/misc/partially_ordered_knapsack.rs b/src/unit_tests/models/misc/partially_ordered_knapsack.rs new file mode 100644 index 000000000..2b16feae4 --- /dev/null +++ b/src/unit_tests/models/misc/partially_ordered_knapsack.rs @@ -0,0 +1,186 @@ +use super::*; +use crate::solvers::{BruteForce, Solver}; +use crate::traits::{OptimizationProblem, Problem}; +use crate::types::Direction; + +/// Helper: create the example instance from the issue. +/// Items: a=0, b=1, c=2, d=3, e=4, f=5 +/// Precedences: a PartiallyOrderedKnapsack { + PartiallyOrderedKnapsack::new( + vec![2, 3, 4, 1, 2, 3], + vec![3, 2, 5, 4, 3, 8], + vec![(0, 2), (0, 3), (1, 4), (3, 5), (4, 5)], + 11, + ) +} + +#[test] +fn test_partially_ordered_knapsack_basic() { + let problem = example_instance(); + assert_eq!(problem.num_items(), 6); + assert_eq!(problem.sizes(), &[2, 3, 4, 1, 2, 3]); + assert_eq!(problem.values(), &[3, 2, 5, 4, 3, 8]); + assert_eq!( + problem.precedences(), + &[(0, 2), (0, 3), (1, 4), (3, 5), (4, 5)] + ); + assert_eq!(problem.capacity(), 11); + assert_eq!(problem.dims(), vec![2; 6]); + assert_eq!(problem.direction(), Direction::Maximize); + assert_eq!( + ::NAME, + "PartiallyOrderedKnapsack" + ); + assert_eq!(::variant(), vec![]); +} + +#[test] +fn test_partially_ordered_knapsack_evaluate_valid() { + let problem = example_instance(); + // U' = {a, b, d, e, f} = indices {0, 1, 3, 4, 5} + // Total size: 2+3+1+2+3 = 11 <= 11 + // Total value: 3+2+4+3+8 = 20 + assert_eq!( + problem.evaluate(&[1, 1, 0, 1, 1, 1]), + SolutionSize::Valid(20) + ); +} + +#[test] +fn test_partially_ordered_knapsack_evaluate_precedence_violation() { + let problem = example_instance(); + // U' = {d, f} = indices {3, 5} — f requires e and b (transitively), d requires a + // Not downward-closed: d selected but a (predecessor of d) not selected + assert_eq!(problem.evaluate(&[0, 0, 0, 1, 0, 1]), SolutionSize::Invalid); +} + +#[test] +fn test_partially_ordered_knapsack_evaluate_transitive_precedence_violation() { + let problem = example_instance(); + // U' = {d, e, f} = indices {3, 4, 5} + // f requires d (ok) and e (ok), but d requires a (0) which is not selected + // Also e requires b (1) which is not selected + assert_eq!(problem.evaluate(&[0, 0, 0, 1, 1, 1]), SolutionSize::Invalid); +} + +#[test] +fn test_partially_ordered_knapsack_evaluate_overweight() { + let problem = example_instance(); + // U' = {a, b, c, d, e, f} = all items + // Total size: 2+3+4+1+2+3 = 15 > 11 + assert_eq!(problem.evaluate(&[1, 1, 1, 1, 1, 1]), SolutionSize::Invalid); +} + +#[test] +fn test_partially_ordered_knapsack_evaluate_empty() { + let problem = example_instance(); + assert_eq!( + problem.evaluate(&[0, 0, 0, 0, 0, 0]), + SolutionSize::Valid(0) + ); +} + +#[test] +fn test_partially_ordered_knapsack_evaluate_single_root() { + let problem = example_instance(); + // Just item a (no predecessors) + assert_eq!( + problem.evaluate(&[1, 0, 0, 0, 0, 0]), + SolutionSize::Valid(3) + ); +} + +#[test] +fn test_partially_ordered_knapsack_evaluate_valid_chain() { + let problem = example_instance(); + // U' = {a, d} = indices {0, 3} + // a has no predecessors, d's predecessor a is selected: downward-closed + // Total size: 2+1 = 3 <= 11, Total value: 3+4 = 7 + assert_eq!( + problem.evaluate(&[1, 0, 0, 1, 0, 0]), + SolutionSize::Valid(7) + ); +} + +#[test] +fn test_partially_ordered_knapsack_evaluate_wrong_config_length() { + let problem = example_instance(); + assert_eq!(problem.evaluate(&[1, 0]), SolutionSize::Invalid); + assert_eq!( + problem.evaluate(&[1, 0, 0, 0, 0, 0, 0]), + SolutionSize::Invalid + ); +} + +#[test] +fn test_partially_ordered_knapsack_evaluate_invalid_variable_value() { + let problem = example_instance(); + assert_eq!(problem.evaluate(&[2, 0, 0, 0, 0, 0]), SolutionSize::Invalid); +} + +#[test] +fn test_partially_ordered_knapsack_brute_force() { + let problem = example_instance(); + let solver = BruteForce::new(); + let solution = solver.find_best(&problem).expect("should find a solution"); + let metric = problem.evaluate(&solution); + // The optimal should be {a, b, d, e, f} with value 20 + assert_eq!(metric, SolutionSize::Valid(20)); +} + +#[test] +fn test_partially_ordered_knapsack_empty_instance() { + let problem = PartiallyOrderedKnapsack::new(vec![], vec![], vec![], 10); + assert_eq!(problem.num_items(), 0); + assert_eq!(problem.num_precedences(), 0); + assert_eq!(problem.dims(), Vec::::new()); + assert_eq!(problem.evaluate(&[]), SolutionSize::Valid(0)); +} + +#[test] +fn test_partially_ordered_knapsack_no_precedences() { + // Without precedences, behaves like standard knapsack + let problem = PartiallyOrderedKnapsack::new(vec![2, 3, 4, 5], vec![3, 4, 5, 7], vec![], 7); + let solver = BruteForce::new(); + let solution = solver.find_best(&problem).expect("should find a solution"); + let metric = problem.evaluate(&solution); + // Same as standard knapsack: items 0 and 3 give weight 7, value 10 + assert_eq!(metric, SolutionSize::Valid(10)); +} + +#[test] +fn test_partially_ordered_knapsack_zero_capacity() { + let problem = PartiallyOrderedKnapsack::new(vec![1, 2], vec![10, 20], vec![(0, 1)], 0); + assert_eq!(problem.evaluate(&[0, 0]), SolutionSize::Valid(0)); + assert_eq!(problem.evaluate(&[1, 0]), SolutionSize::Invalid); + let solver = BruteForce::new(); + let solution = solver.find_best(&problem).unwrap(); + assert_eq!(problem.evaluate(&solution), SolutionSize::Valid(0)); +} + +#[test] +fn test_partially_ordered_knapsack_serialization() { + let problem = example_instance(); + let json = serde_json::to_value(&problem).unwrap(); + let restored: PartiallyOrderedKnapsack = serde_json::from_value(json).unwrap(); + assert_eq!(restored.sizes(), problem.sizes()); + assert_eq!(restored.values(), problem.values()); + assert_eq!(restored.precedences(), problem.precedences()); + assert_eq!(restored.capacity(), problem.capacity()); +} + +#[test] +#[should_panic(expected = "sizes and values must have the same length")] +fn test_partially_ordered_knapsack_mismatched_lengths() { + PartiallyOrderedKnapsack::new(vec![1, 2], vec![3], vec![], 5); +} + +#[test] +#[should_panic(expected = "precedence index 5 out of bounds")] +fn test_partially_ordered_knapsack_invalid_precedence() { + PartiallyOrderedKnapsack::new(vec![1, 2], vec![3, 4], vec![(0, 5)], 5); +} From 2c5ca10d77781a2ef71c11e0fb07ca16393bbe3f Mon Sep 17 00:00:00 2001 From: zazabap Date: Fri, 13 Mar 2026 09:11:38 +0000 Subject: [PATCH 03/10] chore: remove plan file after implementation Co-Authored-By: Claude Opus 4.6 --- .../2026-03-13-partially-ordered-knapsack.md | 66 ------------------- 1 file changed, 66 deletions(-) delete mode 100644 docs/plans/2026-03-13-partially-ordered-knapsack.md diff --git a/docs/plans/2026-03-13-partially-ordered-knapsack.md b/docs/plans/2026-03-13-partially-ordered-knapsack.md deleted file mode 100644 index 7be158546..000000000 --- a/docs/plans/2026-03-13-partially-ordered-knapsack.md +++ /dev/null @@ -1,66 +0,0 @@ -# Plan: Add PartiallyOrderedKnapsack Model - -Fixes #534 - -## Overview - -Add the PartiallyOrderedKnapsack problem model — a knapsack variant where items are subject to a partial order (precedence constraints). Including an item requires including all its predecessors. Modeled as an optimization problem (maximize total value subject to precedence + capacity constraints), consistent with the existing Knapsack model. - -## Design Decisions - -- **Optimization, not satisfaction**: Following the check-issue recommendation and consistency with existing `Knapsack`, model as `OptimizationProblem` with `Direction::Maximize`. The capacity constraint is part of feasibility; the objective is total value. No `target_value` field needed. -- **Precedences as edge list**: Store `Vec<(usize, usize)>` where `(a, b)` means item `a` must precede item `b` (a < b in the partial order). The full precedence requirement is the transitive closure. -- **Complexity**: Use `"2^num_items"` as a baseline (naive enumeration). The problem is strongly NP-hard, so no pseudo-polynomial algorithm exists for general partial orders. -- **Category**: `misc/` — unique input structure (items + partial order + capacity), doesn't fit graph/formula/set/algebraic. - -## Steps - -### Step 1: Implement the model (`src/models/misc/partially_ordered_knapsack.rs`) - -- `ProblemSchemaEntry` with fields: `sizes`, `values`, `precedences`, `capacity` -- Struct: `PartiallyOrderedKnapsack { sizes: Vec, values: Vec, precedences: Vec<(usize, usize)>, capacity: i64 }` -- Constructor: `new(sizes, values, precedences, capacity)` — assert sizes.len() == values.len(), validate precedence indices -- Getters: `sizes()`, `values()`, `capacity()`, `precedences()`, `num_items()`, `num_precedences()` -- `Problem` impl: `NAME = "PartiallyOrderedKnapsack"`, `Metric = SolutionSize`, `dims() = vec![2; n]`, `variant() = variant_params![]` -- `evaluate()`: check config length/values, check downward-closure (compute transitive closure, verify for each selected item all predecessors are selected), check capacity, return total value -- `OptimizationProblem` impl: `Value = i64`, `direction() = Maximize` -- `declare_variants!`: `PartiallyOrderedKnapsack => "2^num_items"` -- Test link: `#[cfg(test)] #[path = "../../unit_tests/models/misc/partially_ordered_knapsack.rs"] mod tests;` - -### Step 2: Register the model - -- `src/models/misc/mod.rs`: add `mod partially_ordered_knapsack;` and `pub use` -- `src/models/mod.rs`: add to `misc` re-export line - -### Step 3: Register in CLI - -- `problemreductions-cli/src/dispatch.rs`: add `load_problem` and `serialize_any_problem` arms -- `problemreductions-cli/src/problem_name.rs`: add `"partiallyorderedknapsack" | "pok"` alias (POK is not well-established, so only add lowercase identity mapping, no short alias) - -### Step 4: Add CLI creation support - -- `problemreductions-cli/src/commands/create.rs`: add `"PartiallyOrderedKnapsack"` arm parsing `--sizes`, `--values`, `--capacity`, `--precedences` -- `problemreductions-cli/src/cli.rs`: add `--values` and `--precedences` flags to `CreateArgs`, update `all_data_flags_empty()`, update help table -- Note: `--sizes` and `--capacity` already exist from BinPacking - -### Step 5: Write unit tests (`src/unit_tests/models/misc/partially_ordered_knapsack.rs`) - -- `test_partially_ordered_knapsack_basic`: construct instance, verify dims, getters -- `test_partially_ordered_knapsack_evaluate_valid`: valid downward-closed set within capacity -- `test_partially_ordered_knapsack_evaluate_precedence_violation`: select item without predecessor -- `test_partially_ordered_knapsack_evaluate_overweight`: valid precedence but over capacity -- `test_partially_ordered_knapsack_evaluate_empty`: empty selection -- `test_partially_ordered_knapsack_brute_force`: solver finds optimal -- `test_partially_ordered_knapsack_serialization`: round-trip serde -- `test_partially_ordered_knapsack_direction`: verify Maximize -- Use example from issue: items a-f with precedences a Date: Sun, 15 Mar 2026 08:02:00 +0000 Subject: [PATCH 04/10] fix: address Copilot review comments for PartiallyOrderedKnapsack - Precompute transitive predecessors in constructor instead of recomputing Floyd-Warshall on every evaluate() call - Custom Serialize/Deserialize to rebuild predecessors on deserialization - Rename CLI flag --item-precedences to --precedences (with alias for compat) - Fix error message to reflect --precedences is optional - Fix Typst $P = "NP"$ to $P = N P$ for correct math typography - Add missing schema fields (display_name, aliases, dimensions) - Add opt keyword to declare_variants! - Regenerate problem_schemas.json and reduction_graph.json Co-Authored-By: Claude Opus 4.6 --- docs/paper/reductions.typ | 2 +- docs/src/reductions/problem_schemas.json | 26 ++++++ docs/src/reductions/reduction_graph.json | 70 +++++++++------ problemreductions-cli/src/cli.rs | 6 +- problemreductions-cli/src/commands/create.rs | 8 +- src/models/misc/partially_ordered_knapsack.rs | 88 +++++++++++++------ 6 files changed, 140 insertions(+), 60 deletions(-) diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index 1ada09c4a..56c09294e 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -1168,7 +1168,7 @@ Biclique Cover is equivalent to factoring the biadjacency matrix $M$ of the bipa #problem-def("PartiallyOrderedKnapsack")[ Given a finite set $U$ with $|U| = n$ items, a partial order $<$ on $U$ (given by its cover relations), for each $u in U$ a size $s(u) in ZZ^+$ and a value $v(u) in ZZ^+$, and a capacity $B in ZZ^+$, find a downward-closed subset $U' subset.eq U$ (i.e., if $u in U'$ and $u' < u$ then $u' in U'$) maximizing $sum_(u in U') v(u)$ subject to $sum_(u in U') s(u) lt.eq B$. ][ - Garey and Johnson's problem A6 MP12 @garey1979. Unlike standard Knapsack, the partial order constraint makes the problem _strongly_ NP-complete --- it remains NP-complete even when $s(u) = v(u)$ for all $u$, so no pseudo-polynomial algorithm exists unless $P = "NP"$. The problem arises in manufacturing scheduling, project selection, and mining operations. For tree partial orders, Johnson and Niemi @johnson1983 gave pseudo-polynomial $O(n dot B)$ tree DP and an FPTAS. Kolliopoulos and Steiner @kolliopoulos2007 extended the FPTAS to 2-dimensional partial orders with $O(n^4 slash epsilon)$ running time. + Garey and Johnson's problem A6 MP12 @garey1979. Unlike standard Knapsack, the partial order constraint makes the problem _strongly_ NP-complete --- it remains NP-complete even when $s(u) = v(u)$ for all $u$, so no pseudo-polynomial algorithm exists unless $P = N P$. The problem arises in manufacturing scheduling, project selection, and mining operations. For tree partial orders, Johnson and Niemi @johnson1983 gave pseudo-polynomial $O(n dot B)$ tree DP and an FPTAS. Kolliopoulos and Steiner @kolliopoulos2007 extended the FPTAS to 2-dimensional partial orders with $O(n^4 slash epsilon)$ running time. *Example.* Consider $n = 6$ items with partial order given by cover relations $a < c$, $a < d$, $b < e$, $d < f$, $e < f$. Sizes $(2, 3, 4, 1, 2, 3)$, values $(3, 2, 5, 4, 3, 8)$, and capacity $B = 11$. Selecting $U' = {a, b, d, e, f}$ is downward-closed (all predecessors included), has total size $2 + 3 + 1 + 2 + 3 = 11 lt.eq B$, and total value $3 + 2 + 4 + 3 + 8 = 20$. Adding $c$ would exceed capacity ($15 > 11$). ] diff --git a/docs/src/reductions/problem_schemas.json b/docs/src/reductions/problem_schemas.json index 43b2a4456..ebee534a5 100644 --- a/docs/src/reductions/problem_schemas.json +++ b/docs/src/reductions/problem_schemas.json @@ -488,6 +488,32 @@ } ] }, + { + "name": "PartiallyOrderedKnapsack", + "description": "Select items to maximize total value subject to precedence constraints and weight capacity", + "fields": [ + { + "name": "sizes", + "type_name": "Vec", + "description": "Item sizes s(u) for each item" + }, + { + "name": "values", + "type_name": "Vec", + "description": "Item values v(u) for each item" + }, + { + "name": "precedences", + "type_name": "Vec<(usize, usize)>", + "description": "Precedence pairs (a, b) meaning a must be included before b" + }, + { + "name": "capacity", + "type_name": "i64", + "description": "Knapsack capacity B" + } + ] + }, { "name": "PartitionIntoTriangles", "description": "Partition vertices into triangles (K3 subgraphs)", diff --git a/docs/src/reductions/reduction_graph.json b/docs/src/reductions/reduction_graph.json index bb8c02551..a0b999e63 100644 --- a/docs/src/reductions/reduction_graph.json +++ b/docs/src/reductions/reduction_graph.json @@ -1,3 +1,12 @@ +{ + "nodes": [ + { + "name": "BMF", + "variant": {}, + +Exported to: docs/src/reductions/reduction_graph.json + +JSON content: { "nodes": [ { @@ -415,6 +424,13 @@ "doc_path": "models/misc/struct.PaintShop.html", "complexity": "2^num_cars" }, + { + "name": "PartiallyOrderedKnapsack", + "variant": {}, + "category": "misc", + "doc_path": "models/misc/struct.PartiallyOrderedKnapsack.html", + "complexity": "2^num_items" + }, { "name": "PartitionIntoTriangles", "variant": { @@ -535,7 +551,7 @@ }, { "source": 4, - "target": 52, + "target": 53, "overhead": [ { "field": "num_spins", @@ -595,7 +611,7 @@ }, { "source": 11, - "target": 47, + "target": 48, "overhead": [ { "field": "num_vars", @@ -636,7 +652,7 @@ }, { "source": 18, - "target": 47, + "target": 48, "overhead": [ { "field": "num_vars", @@ -662,7 +678,7 @@ }, { "source": 19, - "target": 47, + "target": 48, "overhead": [ { "field": "num_vars", @@ -688,7 +704,7 @@ }, { "source": 20, - "target": 47, + "target": 48, "overhead": [ { "field": "num_vars", @@ -699,7 +715,7 @@ }, { "source": 20, - "target": 54, + "target": 55, "overhead": [ { "field": "num_elements", @@ -710,7 +726,7 @@ }, { "source": 21, - "target": 49, + "target": 50, "overhead": [ { "field": "num_clauses", @@ -729,7 +745,7 @@ }, { "source": 22, - "target": 47, + "target": 48, "overhead": [ { "field": "num_vars", @@ -755,7 +771,7 @@ }, { "source": 24, - "target": 52, + "target": 53, "overhead": [ { "field": "num_spins", @@ -1070,7 +1086,7 @@ }, { "source": 36, - "target": 47, + "target": 48, "overhead": [ { "field": "num_vars", @@ -1185,7 +1201,7 @@ "doc_path": "rules/minimumvertexcover_minimumsetcovering/index.html" }, { - "source": 47, + "source": 48, "target": 11, "overhead": [ { @@ -1200,8 +1216,8 @@ "doc_path": "rules/qubo_ilp/index.html" }, { - "source": 47, - "target": 51, + "source": 48, + "target": 52, "overhead": [ { "field": "num_spins", @@ -1211,7 +1227,7 @@ "doc_path": "rules/spinglass_qubo/index.html" }, { - "source": 49, + "source": 50, "target": 4, "overhead": [ { @@ -1226,7 +1242,7 @@ "doc_path": "rules/sat_circuitsat/index.html" }, { - "source": 49, + "source": 50, "target": 15, "overhead": [ { @@ -1241,7 +1257,7 @@ "doc_path": "rules/sat_coloring/index.html" }, { - "source": 49, + "source": 50, "target": 20, "overhead": [ { @@ -1256,7 +1272,7 @@ "doc_path": "rules/sat_ksat/index.html" }, { - "source": 49, + "source": 50, "target": 29, "overhead": [ { @@ -1271,7 +1287,7 @@ "doc_path": "rules/sat_maximumindependentset/index.html" }, { - "source": 49, + "source": 50, "target": 38, "overhead": [ { @@ -1286,8 +1302,8 @@ "doc_path": "rules/sat_minimumdominatingset/index.html" }, { - "source": 51, - "target": 47, + "source": 52, + "target": 48, "overhead": [ { "field": "num_vars", @@ -1297,7 +1313,7 @@ "doc_path": "rules/spinglass_qubo/index.html" }, { - "source": 52, + "source": 53, "target": 24, "overhead": [ { @@ -1312,8 +1328,8 @@ "doc_path": "rules/spinglass_maxcut/index.html" }, { - "source": 52, - "target": 51, + "source": 53, + "target": 52, "overhead": [ { "field": "num_spins", @@ -1327,7 +1343,7 @@ "doc_path": "rules/spinglass_casts/index.html" }, { - "source": 55, + "source": 56, "target": 11, "overhead": [ { @@ -1342,8 +1358,8 @@ "doc_path": "rules/travelingsalesman_ilp/index.html" }, { - "source": 55, - "target": 47, + "source": 56, + "target": 48, "overhead": [ { "field": "num_vars", @@ -1353,4 +1369,4 @@ "doc_path": "rules/travelingsalesman_qubo/index.html" } ] -} \ No newline at end of file +} diff --git a/problemreductions-cli/src/cli.rs b/problemreductions-cli/src/cli.rs index 3a593ad71..a1da677db 100644 --- a/problemreductions-cli/src/cli.rs +++ b/problemreductions-cli/src/cli.rs @@ -239,7 +239,7 @@ Flags by problem type: LCS --strings FAS --arcs [--weights] [--num-vertices] FVS --arcs [--weights] [--num-vertices] - PartiallyOrderedKnapsack --sizes, --values, --capacity, --item-precedences + PartiallyOrderedKnapsack --sizes, --values, --capacity, --precedences FlowShopScheduling --task-lengths, --deadline [--num-processors] SCS --strings, --bound [--alphabet-size] ILP, CircuitSAT (via reduction only) @@ -387,8 +387,8 @@ pub struct CreateArgs { #[arg(long)] pub values: Option, /// Precedence pairs (e.g., "0>2,0>3,1>4") for PartiallyOrderedKnapsack - #[arg(long)] - pub item_precedences: Option, + #[arg(long, alias = "item-precedences")] + pub precedences: Option, /// Task lengths for FlowShopScheduling (semicolon-separated rows: "3,4,2;2,3,5;4,1,3") #[arg(long)] pub task_lengths: Option, diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index 1b849c0c4..8b36df2bd 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -58,7 +58,7 @@ fn all_data_flags_empty(args: &CreateArgs) -> bool { && args.strings.is_none() && args.arcs.is_none() && args.values.is_none() - && args.item_precedences.is_none() + && args.precedences.is_none() && args.task_lengths.is_none() && args.deadline.is_none() && args.num_processors.is_none() @@ -988,8 +988,8 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { "PartiallyOrderedKnapsack" => { let sizes_str = args.sizes.as_deref().ok_or_else(|| { anyhow::anyhow!( - "PartiallyOrderedKnapsack requires --sizes, --values, --capacity, and --item-precedences\n\n\ - Usage: pred create PartiallyOrderedKnapsack --sizes 2,3,4,1,2,3 --values 3,2,5,4,3,8 --item-precedences \"0>2,0>3,1>4,3>5,4>5\" --capacity 11" + "PartiallyOrderedKnapsack requires --sizes, --values, and --capacity (--precedences is optional)\n\n\ + Usage: pred create PartiallyOrderedKnapsack --sizes 2,3,4,1,2,3 --values 3,2,5,4,3,8 --precedences \"0>2,0>3,1>4,3>5,4>5\" --capacity 11" ) })?; let values_str = args.values.as_deref().ok_or_else(|| { @@ -1001,7 +1001,7 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { let sizes: Vec = util::parse_comma_list(sizes_str)?; let values: Vec = util::parse_comma_list(values_str)?; let capacity: i64 = cap_str.parse()?; - let precedences = match args.item_precedences.as_deref() { + let precedences = match args.precedences.as_deref() { Some(s) if !s.trim().is_empty() => s .split(',') .map(|pair| { diff --git a/src/models/misc/partially_ordered_knapsack.rs b/src/models/misc/partially_ordered_knapsack.rs index 052158085..0be6a6755 100644 --- a/src/models/misc/partially_ordered_knapsack.rs +++ b/src/models/misc/partially_ordered_knapsack.rs @@ -12,6 +12,9 @@ use serde::{Deserialize, Serialize}; inventory::submit! { ProblemSchemaEntry { name: "PartiallyOrderedKnapsack", + display_name: "Partially Ordered Knapsack", + aliases: &["POK"], + dimensions: &[], module_path: module_path!(), description: "Select items to maximize total value subject to precedence constraints and weight capacity", fields: &[ @@ -52,12 +55,43 @@ inventory::submit! { /// let solution = solver.find_best(&problem); /// assert!(solution.is_some()); /// ``` -#[derive(Debug, Clone, Serialize, Deserialize)] +/// Raw serialization helper for [`PartiallyOrderedKnapsack`]. +#[derive(Serialize, Deserialize)] +struct PartiallyOrderedKnapsackRaw { + sizes: Vec, + values: Vec, + precedences: Vec<(usize, usize)>, + capacity: i64, +} + +#[derive(Debug, Clone)] pub struct PartiallyOrderedKnapsack { sizes: Vec, values: Vec, precedences: Vec<(usize, usize)>, capacity: i64, + /// Precomputed transitive predecessors for each item. + /// `predecessors[b]` contains all items that must be selected when `b` is selected. + predecessors: Vec>, +} + +impl Serialize for PartiallyOrderedKnapsack { + fn serialize(&self, serializer: S) -> Result { + PartiallyOrderedKnapsackRaw { + sizes: self.sizes.clone(), + values: self.values.clone(), + precedences: self.precedences.clone(), + capacity: self.capacity, + } + .serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for PartiallyOrderedKnapsack { + fn deserialize>(deserializer: D) -> Result { + let raw = PartiallyOrderedKnapsackRaw::deserialize(deserializer)?; + Ok(Self::new(raw.sizes, raw.values, raw.precedences, raw.capacity)) + } } impl PartiallyOrderedKnapsack { @@ -88,14 +122,36 @@ impl PartiallyOrderedKnapsack { assert!(a < n, "precedence index {a} out of bounds (n={n})"); assert!(b < n, "precedence index {b} out of bounds (n={n})"); } + let predecessors = Self::compute_predecessors(&precedences, n); Self { sizes, values, precedences, capacity, + predecessors, } } + /// Compute transitive predecessors for each item via Floyd-Warshall. + fn compute_predecessors(precedences: &[(usize, usize)], n: usize) -> Vec> { + let mut reachable = vec![vec![false; n]; n]; + for &(a, b) in precedences { + reachable[a][b] = true; + } + for k in 0..n { + for i in 0..n { + for j in 0..n { + if reachable[i][k] && reachable[k][j] { + reachable[i][j] = true; + } + } + } + } + (0..n) + .map(|b| (0..n).filter(|&a| reachable[a][b]).collect()) + .collect() + } + /// Returns the item sizes. pub fn sizes(&self) -> &[i64] { &self.sizes @@ -128,31 +184,13 @@ impl PartiallyOrderedKnapsack { /// Check if the selected items form a downward-closed set. /// - /// Uses the transitive closure of the precedence relation: if item `b` is - /// selected and `a` is a (transitive) predecessor of `b`, then `a` must - /// also be selected. + /// Uses precomputed transitive predecessors: if item `b` is selected, + /// all its predecessors must also be selected. fn is_downward_closed(&self, config: &[usize]) -> bool { - let n = self.num_items(); - // Build adjacency matrix for transitive closure - let mut reachable = vec![vec![false; n]; n]; - for &(a, b) in &self.precedences { - reachable[a][b] = true; - } - // Floyd-Warshall for transitive closure - for k in 0..n { - for i in 0..n { - for j in 0..n { - if reachable[i][k] && reachable[k][j] { - reachable[i][j] = true; - } - } - } - } - // Check: for every selected item b, all predecessors must be selected - for b in 0..n { + for (b, preds) in self.predecessors.iter().enumerate() { if config[b] == 1 { - for a in 0..n { - if reachable[a][b] && config[a] != 1 { + for &a in preds { + if config[a] != 1 { return false; } } @@ -215,7 +253,7 @@ impl OptimizationProblem for PartiallyOrderedKnapsack { } crate::declare_variants! { - PartiallyOrderedKnapsack => "2^num_items", + default opt PartiallyOrderedKnapsack => "2^num_items", } #[cfg(test)] From bee631cb5bdf391e4511ca761c357fdbb189a825 Mon Sep 17 00:00:00 2001 From: zazabap Date: Sun, 15 Mar 2026 08:08:21 +0000 Subject: [PATCH 05/10] fix: address structural and quality review findings - Add canonical model example builder for PartiallyOrderedKnapsack - Add PartiallyOrderedKnapsack to trait_consistency tests - Add nonnegative validation for sizes and capacity - Add cycle detection in precedence graph - Fix corrupted reduction_graph.json (was mixing stdout/JSON) Co-Authored-By: Claude Opus 4.6 --- docs/src/reductions/reduction_graph.json | 11 +----- src/models/misc/mod.rs | 3 +- src/models/misc/partially_ordered_knapsack.rs | 38 ++++++++++++++++++- src/unit_tests/trait_consistency.rs | 8 ++++ 4 files changed, 47 insertions(+), 13 deletions(-) diff --git a/docs/src/reductions/reduction_graph.json b/docs/src/reductions/reduction_graph.json index a0b999e63..e565e5117 100644 --- a/docs/src/reductions/reduction_graph.json +++ b/docs/src/reductions/reduction_graph.json @@ -1,12 +1,3 @@ -{ - "nodes": [ - { - "name": "BMF", - "variant": {}, - -Exported to: docs/src/reductions/reduction_graph.json - -JSON content: { "nodes": [ { @@ -1369,4 +1360,4 @@ JSON content: "doc_path": "rules/travelingsalesman_qubo/index.html" } ] -} +} \ No newline at end of file diff --git a/src/models/misc/mod.rs b/src/models/misc/mod.rs index 538947f1c..ce8f44c86 100644 --- a/src/models/misc/mod.rs +++ b/src/models/misc/mod.rs @@ -17,7 +17,7 @@ mod flow_shop_scheduling; mod knapsack; mod longest_common_subsequence; pub(crate) mod paintshop; -mod partially_ordered_knapsack; +pub(crate) mod partially_ordered_knapsack; pub(crate) mod shortest_common_supersequence; mod subset_sum; @@ -37,5 +37,6 @@ pub(crate) fn canonical_model_example_specs() -> Vec, values: Vec, @@ -117,12 +118,23 @@ impl PartiallyOrderedKnapsack { values.len(), "sizes and values must have the same length" ); + assert!(capacity >= 0, "capacity must be non-negative"); + for (i, &s) in sizes.iter().enumerate() { + assert!(s >= 0, "size[{i}] must be non-negative, got {s}"); + } let n = sizes.len(); for &(a, b) in &precedences { assert!(a < n, "precedence index {a} out of bounds (n={n})"); assert!(b < n, "precedence index {b} out of bounds (n={n})"); } let predecessors = Self::compute_predecessors(&precedences, n); + // Check for cycles: if any item is its own transitive predecessor, the DAG has a cycle + for i in 0..n { + assert!( + !predecessors[i].contains(&i), + "precedences contain a cycle involving item {i}" + ); + } Self { sizes, values, @@ -256,6 +268,28 @@ crate::declare_variants! { default opt PartiallyOrderedKnapsack => "2^num_items", } +#[cfg(feature = "example-db")] +pub(crate) fn canonical_model_example_specs() -> Vec { + vec![crate::example_db::specs::ModelExampleSpec { + id: "partially_ordered_knapsack", + build: || { + use crate::solvers::BruteForce; + let problem = PartiallyOrderedKnapsack::new( + vec![2, 3, 4, 1, 2, 3], + vec![3, 2, 5, 4, 3, 8], + vec![(0, 2), (0, 3), (1, 4), (3, 5), (4, 5)], + 11, + ); + let sample = BruteForce::new() + .find_all_best(&problem) + .into_iter() + .next() + .expect("partially_ordered_knapsack example should solve"); + crate::example_db::specs::optimization_example(problem, vec![sample]) + }, + }] +} + #[cfg(test)] #[path = "../../unit_tests/models/misc/partially_ordered_knapsack.rs"] mod tests; diff --git a/src/unit_tests/trait_consistency.rs b/src/unit_tests/trait_consistency.rs index ebbc68a0e..9332440bd 100644 --- a/src/unit_tests/trait_consistency.rs +++ b/src/unit_tests/trait_consistency.rs @@ -122,6 +122,10 @@ fn test_all_problems_implement_trait_correctly() { &FlowShopScheduling::new(2, vec![vec![1, 2], vec![3, 4]], 10), "FlowShopScheduling", ); + check_problem_trait( + &PartiallyOrderedKnapsack::new(vec![2, 3], vec![3, 2], vec![(0, 1)], 5), + "PartiallyOrderedKnapsack", + ); } #[test] @@ -207,4 +211,8 @@ fn test_direction() { MaximumClique::new(SimpleGraph::new(2, vec![(0, 1)]), vec![1i32; 2]).direction(), Direction::Maximize ); + assert_eq!( + PartiallyOrderedKnapsack::new(vec![2, 3], vec![3, 2], vec![(0, 1)], 5).direction(), + Direction::Maximize + ); } From 78c176d8fd14fdac28830f47822180cc963dde36 Mon Sep 17 00:00:00 2001 From: zazabap Date: Sun, 15 Mar 2026 08:16:14 +0000 Subject: [PATCH 06/10] style: run cargo fmt Co-Authored-By: Claude Opus 4.6 --- problemreductions-cli/src/commands/create.rs | 4 ++-- src/models/graph/maximum_independent_set.rs | 3 +-- src/models/misc/partially_ordered_knapsack.rs | 7 ++++++- src/models/mod.rs | 2 +- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index 8b36df2bd..1e486e1d7 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -8,8 +8,8 @@ use problemreductions::export::{ModelExample, ProblemRef, ProblemSide, RuleExamp use problemreductions::models::algebraic::{ClosestVectorProblem, BMF}; use problemreductions::models::graph::{GraphPartitioning, HamiltonianPath}; use problemreductions::models::misc::{ - BinPacking, FlowShopScheduling, LongestCommonSubsequence, PaintShop, - PartiallyOrderedKnapsack, ShortestCommonSupersequence, SubsetSum, + BinPacking, FlowShopScheduling, LongestCommonSubsequence, PaintShop, PartiallyOrderedKnapsack, + ShortestCommonSupersequence, SubsetSum, }; use problemreductions::prelude::*; use problemreductions::registry::collect_schemas; diff --git a/src/models/graph/maximum_independent_set.rs b/src/models/graph/maximum_independent_set.rs index 0b8a3ddfc..1177398b9 100644 --- a/src/models/graph/maximum_independent_set.rs +++ b/src/models/graph/maximum_independent_set.rs @@ -233,8 +233,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec Deserialize<'de> for PartiallyOrderedKnapsack { fn deserialize>(deserializer: D) -> Result { let raw = PartiallyOrderedKnapsackRaw::deserialize(deserializer)?; - Ok(Self::new(raw.sizes, raw.values, raw.precedences, raw.capacity)) + Ok(Self::new( + raw.sizes, + raw.values, + raw.precedences, + raw.capacity, + )) } } diff --git a/src/models/mod.rs b/src/models/mod.rs index 328b1b387..71a8caaa0 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -18,9 +18,9 @@ pub use graph::{ OptimalLinearArrangement, PartitionIntoTriangles, RuralPostman, SpinGlass, SubgraphIsomorphism, TravelingSalesman, }; +pub use misc::PartiallyOrderedKnapsack; pub use misc::{ BinPacking, Factoring, FlowShopScheduling, Knapsack, LongestCommonSubsequence, PaintShop, ShortestCommonSupersequence, SubsetSum, }; -pub use misc::PartiallyOrderedKnapsack; pub use set::{MaximumSetPacking, MinimumSetCovering}; From 460f58b327a45846f3d7d66fd70e863b1a0a22ca Mon Sep 17 00:00:00 2001 From: zazabap Date: Sun, 15 Mar 2026 08:21:25 +0000 Subject: [PATCH 07/10] fix: resolve clippy needless_range_loop in cycle detection Co-Authored-By: Claude Opus 4.6 --- src/models/misc/partially_ordered_knapsack.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/models/misc/partially_ordered_knapsack.rs b/src/models/misc/partially_ordered_knapsack.rs index 7675d87d9..48b878d9f 100644 --- a/src/models/misc/partially_ordered_knapsack.rs +++ b/src/models/misc/partially_ordered_knapsack.rs @@ -134,9 +134,9 @@ impl PartiallyOrderedKnapsack { } let predecessors = Self::compute_predecessors(&precedences, n); // Check for cycles: if any item is its own transitive predecessor, the DAG has a cycle - for i in 0..n { + for (i, preds) in predecessors.iter().enumerate() { assert!( - !predecessors[i].contains(&i), + !preds.contains(&i), "precedences contain a cycle involving item {i}" ); } From fda90baa0628cda9eb007a039d93a53664eda225 Mon Sep 17 00:00:00 2001 From: Xiwei Pan Date: Thu, 19 Mar 2026 15:30:15 +0800 Subject: [PATCH 08/10] fix: update canonical example format and align paper notation with Knapsack MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Migrate ModelExampleSpec from old `build` closure to new `instance`/`optimal_config`/`optimal_value` struct format - Align paper notation with standard Knapsack: w_i/v_i, capacity C, subset S, partial order ≺ Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/paper/reductions.typ | 6 ++--- src/models/misc/partially_ordered_knapsack.rs | 23 +++++++------------ 2 files changed, 11 insertions(+), 18 deletions(-) diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index 119dd49ac..3bf48a23a 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -2478,11 +2478,11 @@ NP-completeness was established by Garey, Johnson, and Stockmeyer @gareyJohnsonS ] #problem-def("PartiallyOrderedKnapsack")[ - Given a finite set $U$ with $|U| = n$ items, a partial order $<$ on $U$ (given by its cover relations), for each $u in U$ a size $s(u) in ZZ^+$ and a value $v(u) in ZZ^+$, and a capacity $B in ZZ^+$, find a downward-closed subset $U' subset.eq U$ (i.e., if $u in U'$ and $u' < u$ then $u' in U'$) maximizing $sum_(u in U') v(u)$ subject to $sum_(u in U') s(u) lt.eq B$. + Given $n$ items with weights $w_0, dots, w_(n-1) in NN$ and values $v_0, dots, v_(n-1) in NN$, a partial order $prec$ on the items (given by its cover relations), and a capacity $C in NN$, find a downward-closed subset $S subset.eq {0, dots, n - 1}$ (i.e., if $i in S$ and $j prec i$ then $j in S$) maximizing $sum_(i in S) v_i$ subject to $sum_(i in S) w_i lt.eq C$. ][ - Garey and Johnson's problem A6 MP12 @garey1979. Unlike standard Knapsack, the partial order constraint makes the problem _strongly_ NP-complete --- it remains NP-complete even when $s(u) = v(u)$ for all $u$, so no pseudo-polynomial algorithm exists unless $P = N P$. The problem arises in manufacturing scheduling, project selection, and mining operations. For tree partial orders, Johnson and Niemi @johnson1983 gave pseudo-polynomial $O(n dot B)$ tree DP and an FPTAS. Kolliopoulos and Steiner @kolliopoulos2007 extended the FPTAS to 2-dimensional partial orders with $O(n^4 slash epsilon)$ running time. + Garey and Johnson's problem A6 MP12 @garey1979. Unlike standard Knapsack, the partial order constraint makes the problem _strongly_ NP-complete --- it remains NP-complete even when $w_i = v_i$ for all $i$, so no pseudo-polynomial algorithm exists unless $P = N P$. The problem arises in manufacturing scheduling, project selection, and mining operations. For tree partial orders, Johnson and Niemi @johnson1983 gave pseudo-polynomial $O(n dot C)$ tree DP and an FPTAS. Kolliopoulos and Steiner @kolliopoulos2007 extended the FPTAS to 2-dimensional partial orders with $O(n^4 slash epsilon)$ running time. - *Example.* Consider $n = 6$ items with partial order given by cover relations $a < c$, $a < d$, $b < e$, $d < f$, $e < f$. Sizes $(2, 3, 4, 1, 2, 3)$, values $(3, 2, 5, 4, 3, 8)$, and capacity $B = 11$. Selecting $U' = {a, b, d, e, f}$ is downward-closed (all predecessors included), has total size $2 + 3 + 1 + 2 + 3 = 11 lt.eq B$, and total value $3 + 2 + 4 + 3 + 8 = 20$. Adding $c$ would exceed capacity ($15 > 11$). + *Example.* Let $n = 6$ items with weights $(2, 3, 4, 1, 2, 3)$, values $(3, 2, 5, 4, 3, 8)$, and capacity $C = 11$. The partial order has cover relations $0 prec 2$, $0 prec 3$, $1 prec 4$, $3 prec 5$, $4 prec 5$. Selecting $S = {0, 1, 3, 4, 5}$ is downward-closed (all predecessors included), has total weight $2 + 3 + 1 + 2 + 3 = 11 lt.eq C$, and total value $3 + 2 + 4 + 3 + 8 = 20$. Adding item 2 would exceed capacity ($15 > 11$). ] #problem-def("RuralPostman")[ diff --git a/src/models/misc/partially_ordered_knapsack.rs b/src/models/misc/partially_ordered_knapsack.rs index 48b878d9f..729199c08 100644 --- a/src/models/misc/partially_ordered_knapsack.rs +++ b/src/models/misc/partially_ordered_knapsack.rs @@ -277,21 +277,14 @@ crate::declare_variants! { pub(crate) fn canonical_model_example_specs() -> Vec { vec![crate::example_db::specs::ModelExampleSpec { id: "partially_ordered_knapsack", - build: || { - use crate::solvers::BruteForce; - let problem = PartiallyOrderedKnapsack::new( - vec![2, 3, 4, 1, 2, 3], - vec![3, 2, 5, 4, 3, 8], - vec![(0, 2), (0, 3), (1, 4), (3, 5), (4, 5)], - 11, - ); - let sample = BruteForce::new() - .find_all_best(&problem) - .into_iter() - .next() - .expect("partially_ordered_knapsack example should solve"); - crate::example_db::specs::optimization_example(problem, vec![sample]) - }, + instance: Box::new(PartiallyOrderedKnapsack::new( + vec![2, 3, 4, 1, 2, 3], + vec![3, 2, 5, 4, 3, 8], + vec![(0, 2), (0, 3), (1, 4), (3, 5), (4, 5)], + 11, + )), + optimal_config: vec![1, 1, 0, 1, 1, 1], + optimal_value: serde_json::json!({"Valid": 20}), }] } From 0d7673ae57905e983f94c8dbefd55a453ff9fa8c Mon Sep 17 00:00:00 2001 From: Xiwei Pan Date: Thu, 19 Mar 2026 15:34:26 +0800 Subject: [PATCH 09/10] fix: resolve duplicate imports and missing fields from merge - Merge two duplicate `misc` import blocks into one - Add missing `values` and `precedences` fields to test `empty_args()` Co-Authored-By: Claude Opus 4.6 (1M context) --- problemreductions-cli/src/commands/create.rs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index f315d2f4b..7c8bc24e7 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -12,15 +12,11 @@ use problemreductions::models::graph::{ GraphPartitioning, HamiltonianCircuit, HamiltonianPath, LengthBoundedDisjointPaths, MinimumMultiwayCut, MultipleChoiceBranching, SteinerTree, StrongConnectivityAugmentation, }; -use problemreductions::models::misc::{ - BinPacking, FlowShopScheduling, LongestCommonSubsequence, PaintShop, PartiallyOrderedKnapsack, - ShortestCommonSupersequence, SubsetSum, -}; use problemreductions::models::misc::{ BinPacking, FlowShopScheduling, LongestCommonSubsequence, MinimumTardinessSequencing, - MultiprocessorScheduling, PaintShop, SequencingWithReleaseTimesAndDeadlines, - SequencingWithinIntervals, ShortestCommonSupersequence, StringToStringCorrection, SubsetSum, - SumOfSquaresPartition, + MultiprocessorScheduling, PaintShop, PartiallyOrderedKnapsack, + SequencingWithReleaseTimesAndDeadlines, SequencingWithinIntervals, + ShortestCommonSupersequence, StringToStringCorrection, SubsetSum, SumOfSquaresPartition, }; use problemreductions::models::BiconnectivityAugmentation; use problemreductions::prelude::*; @@ -3807,6 +3803,8 @@ mod tests { pattern: None, strings: None, arcs: None, + values: None, + precedences: None, distance_matrix: None, potential_edges: None, budget: None, From 7f3899c31bd601dbcf3c0cd2ad50b04072e6e48b Mon Sep 17 00:00:00 2001 From: Xiwei Pan Date: Thu, 19 Mar 2026 15:45:20 +0800 Subject: [PATCH 10/10] =?UTF-8?q?fix:=20rename=20sizes=E2=86=92weights,=20?= =?UTF-8?q?add=20value=20validation=20and=20panic=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rename `sizes` field to `weights` throughout (code, schema, docs, CLI, tests) to match standard Knapsack terminology - Paper uses w_i notation consistent with Knapsack section - Add non-negative validation for values (matching Knapsack pattern) - Add #[should_panic] tests for cycle detection, negative capacity, negative weight, and negative value - Fix doc comment leak on PartiallyOrderedKnapsackRaw Co-Authored-By: Claude Opus 4.6 (1M context) --- problemreductions-cli/src/commands/create.rs | 4 +- src/models/misc/partially_ordered_knapsack.rs | 64 ++++++++++--------- .../models/misc/partially_ordered_knapsack.rs | 30 ++++++++- 3 files changed, 63 insertions(+), 35 deletions(-) diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index cabf4c083..def73462a 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -2120,7 +2120,7 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { let cap_str = args.capacity.as_deref().ok_or_else(|| { anyhow::anyhow!("PartiallyOrderedKnapsack requires --capacity (e.g., 11)") })?; - let sizes: Vec = util::parse_comma_list(sizes_str)?; + let weights: Vec = util::parse_comma_list(sizes_str)?; let values: Vec = util::parse_comma_list(values_str)?; let capacity: i64 = cap_str.parse()?; let precedences = match args.precedences.as_deref() { @@ -2143,7 +2143,7 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { }; ( ser(PartiallyOrderedKnapsack::new( - sizes, + weights, values, precedences, capacity, diff --git a/src/models/misc/partially_ordered_knapsack.rs b/src/models/misc/partially_ordered_knapsack.rs index 729199c08..b4bc7c239 100644 --- a/src/models/misc/partially_ordered_knapsack.rs +++ b/src/models/misc/partially_ordered_knapsack.rs @@ -18,7 +18,7 @@ inventory::submit! { module_path: module_path!(), description: "Select items to maximize total value subject to precedence constraints and weight capacity", fields: &[ - FieldInfo { name: "sizes", type_name: "Vec", description: "Item sizes s(u) for each item" }, + FieldInfo { name: "weights", type_name: "Vec", description: "Item weights w(u) for each item" }, FieldInfo { name: "values", type_name: "Vec", description: "Item values v(u) for each item" }, FieldInfo { name: "precedences", type_name: "Vec<(usize, usize)>", description: "Precedence pairs (a, b) meaning a must be included before b" }, FieldInfo { name: "capacity", type_name: "i64", description: "Knapsack capacity B" }, @@ -28,10 +28,10 @@ inventory::submit! { /// The Partially Ordered Knapsack problem. /// -/// Given `n` items, each with size `s(u)` and value `v(u)`, a partial order -/// on the items (given as precedence pairs), and a capacity `B`, find a subset -/// `U' ⊆ U` that is downward-closed (if `u ∈ U'` and `u' < u`, then `u' ∈ U'`), -/// satisfies `∑_{u∈U'} s(u) ≤ B`, and maximizes `∑_{u∈U'} v(u)`. +/// Given `n` items, each with weight `w(u)` and value `v(u)`, a partial order +/// on the items (given as precedence pairs), and a capacity `C`, find a subset +/// `S ⊆ {0,…,n-1}` that is downward-closed (if `i ∈ S` and `j ≺ i`, then `j ∈ S`), +/// satisfies `∑_{i∈S} w_i ≤ C`, and maximizes `∑_{i∈S} v_i`. /// /// # Representation /// @@ -46,7 +46,7 @@ inventory::submit! { /// use problemreductions::{Problem, Solver, BruteForce}; /// /// let problem = PartiallyOrderedKnapsack::new( -/// vec![2, 3, 4, 1, 2, 3], // sizes +/// vec![2, 3, 4, 1, 2, 3], // weights /// vec![3, 2, 5, 4, 3, 8], // values /// vec![(0, 2), (0, 3), (1, 4), (3, 5), (4, 5)], // precedences /// 11, // capacity @@ -55,10 +55,11 @@ inventory::submit! { /// let solution = solver.find_best(&problem); /// assert!(solution.is_some()); /// ``` -/// Raw serialization helper for [`PartiallyOrderedKnapsack`]. +/// +// Raw serialization helper for [`PartiallyOrderedKnapsack`]. #[derive(Serialize, Deserialize)] struct PartiallyOrderedKnapsackRaw { - sizes: Vec, + weights: Vec, values: Vec, precedences: Vec<(usize, usize)>, capacity: i64, @@ -66,7 +67,7 @@ struct PartiallyOrderedKnapsackRaw { #[derive(Debug, Clone)] pub struct PartiallyOrderedKnapsack { - sizes: Vec, + weights: Vec, values: Vec, precedences: Vec<(usize, usize)>, capacity: i64, @@ -78,7 +79,7 @@ pub struct PartiallyOrderedKnapsack { impl Serialize for PartiallyOrderedKnapsack { fn serialize(&self, serializer: S) -> Result { PartiallyOrderedKnapsackRaw { - sizes: self.sizes.clone(), + weights: self.weights.clone(), values: self.values.clone(), precedences: self.precedences.clone(), capacity: self.capacity, @@ -91,7 +92,7 @@ impl<'de> Deserialize<'de> for PartiallyOrderedKnapsack { fn deserialize>(deserializer: D) -> Result { let raw = PartiallyOrderedKnapsackRaw::deserialize(deserializer)?; Ok(Self::new( - raw.sizes, + raw.weights, raw.values, raw.precedences, raw.capacity, @@ -103,31 +104,34 @@ impl PartiallyOrderedKnapsack { /// Create a new PartiallyOrderedKnapsack instance. /// /// # Arguments - /// * `sizes` - Size s(u) for each item + /// * `weights` - Weight w(u) for each item /// * `values` - Value v(u) for each item /// * `precedences` - Precedence pairs `(a, b)` meaning item `a` must be included before item `b` - /// * `capacity` - Knapsack capacity B + /// * `capacity` - Knapsack capacity C /// /// # Panics - /// Panics if `sizes` and `values` have different lengths, if any size or - /// capacity is negative, if any precedence index is out of bounds, or if - /// the precedences contain a cycle. + /// Panics if `weights` and `values` have different lengths, if any weight, + /// value, or capacity is negative, if any precedence index is out of bounds, + /// or if the precedences contain a cycle. pub fn new( - sizes: Vec, + weights: Vec, values: Vec, precedences: Vec<(usize, usize)>, capacity: i64, ) -> Self { assert_eq!( - sizes.len(), + weights.len(), values.len(), - "sizes and values must have the same length" + "weights and values must have the same length" ); assert!(capacity >= 0, "capacity must be non-negative"); - for (i, &s) in sizes.iter().enumerate() { - assert!(s >= 0, "size[{i}] must be non-negative, got {s}"); + for (i, &w) in weights.iter().enumerate() { + assert!(w >= 0, "weight[{i}] must be non-negative, got {w}"); + } + for (i, &v) in values.iter().enumerate() { + assert!(v >= 0, "value[{i}] must be non-negative, got {v}"); } - let n = sizes.len(); + let n = weights.len(); for &(a, b) in &precedences { assert!(a < n, "precedence index {a} out of bounds (n={n})"); assert!(b < n, "precedence index {b} out of bounds (n={n})"); @@ -141,7 +145,7 @@ impl PartiallyOrderedKnapsack { ); } Self { - sizes, + weights, values, precedences, capacity, @@ -169,9 +173,9 @@ impl PartiallyOrderedKnapsack { .collect() } - /// Returns the item sizes. - pub fn sizes(&self) -> &[i64] { - &self.sizes + /// Returns the item weights. + pub fn weights(&self) -> &[i64] { + &self.weights } /// Returns the item values. @@ -191,7 +195,7 @@ impl PartiallyOrderedKnapsack { /// Returns the number of items. pub fn num_items(&self) -> usize { - self.sizes.len() + self.weights.len() } /// Returns the number of precedence relations. @@ -241,13 +245,13 @@ impl Problem for PartiallyOrderedKnapsack { return SolutionSize::Invalid; } // Check capacity constraint - let total_size: i64 = config + let total_weight: i64 = config .iter() .enumerate() .filter(|(_, &x)| x == 1) - .map(|(i, _)| self.sizes[i]) + .map(|(i, _)| self.weights[i]) .sum(); - if total_size > self.capacity { + if total_weight > self.capacity { return SolutionSize::Invalid; } // Compute total value diff --git a/src/unit_tests/models/misc/partially_ordered_knapsack.rs b/src/unit_tests/models/misc/partially_ordered_knapsack.rs index 2b16feae4..8a7f53642 100644 --- a/src/unit_tests/models/misc/partially_ordered_knapsack.rs +++ b/src/unit_tests/models/misc/partially_ordered_knapsack.rs @@ -22,7 +22,7 @@ fn example_instance() -> PartiallyOrderedKnapsack { fn test_partially_ordered_knapsack_basic() { let problem = example_instance(); assert_eq!(problem.num_items(), 6); - assert_eq!(problem.sizes(), &[2, 3, 4, 1, 2, 3]); + assert_eq!(problem.weights(), &[2, 3, 4, 1, 2, 3]); assert_eq!(problem.values(), &[3, 2, 5, 4, 3, 8]); assert_eq!( problem.precedences(), @@ -167,14 +167,14 @@ fn test_partially_ordered_knapsack_serialization() { let problem = example_instance(); let json = serde_json::to_value(&problem).unwrap(); let restored: PartiallyOrderedKnapsack = serde_json::from_value(json).unwrap(); - assert_eq!(restored.sizes(), problem.sizes()); + assert_eq!(restored.weights(), problem.weights()); assert_eq!(restored.values(), problem.values()); assert_eq!(restored.precedences(), problem.precedences()); assert_eq!(restored.capacity(), problem.capacity()); } #[test] -#[should_panic(expected = "sizes and values must have the same length")] +#[should_panic(expected = "weights and values must have the same length")] fn test_partially_ordered_knapsack_mismatched_lengths() { PartiallyOrderedKnapsack::new(vec![1, 2], vec![3], vec![], 5); } @@ -184,3 +184,27 @@ fn test_partially_ordered_knapsack_mismatched_lengths() { fn test_partially_ordered_knapsack_invalid_precedence() { PartiallyOrderedKnapsack::new(vec![1, 2], vec![3, 4], vec![(0, 5)], 5); } + +#[test] +#[should_panic(expected = "precedences contain a cycle")] +fn test_partially_ordered_knapsack_cycle() { + PartiallyOrderedKnapsack::new(vec![1, 2, 3], vec![1, 2, 3], vec![(0, 1), (1, 2), (2, 0)], 10); +} + +#[test] +#[should_panic(expected = "capacity must be non-negative")] +fn test_partially_ordered_knapsack_negative_capacity() { + PartiallyOrderedKnapsack::new(vec![1, 2], vec![3, 4], vec![], -1); +} + +#[test] +#[should_panic(expected = "weight[1] must be non-negative")] +fn test_partially_ordered_knapsack_negative_weight() { + PartiallyOrderedKnapsack::new(vec![1, -2], vec![3, 4], vec![], 5); +} + +#[test] +#[should_panic(expected = "value[0] must be non-negative")] +fn test_partially_ordered_knapsack_negative_value() { + PartiallyOrderedKnapsack::new(vec![1, 2], vec![-3, 4], vec![], 5); +}