diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index 07b45a924..a37708dec 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -87,6 +87,7 @@ "QUBO": [QUBO], "ILP": [Integer Linear Programming], "Knapsack": [Knapsack], + "PartiallyOrderedKnapsack": [Partially Ordered Knapsack], "Satisfiability": [SAT], "KSatisfiability": [$k$-SAT], "CircuitSAT": [CircuitSAT], @@ -2477,6 +2478,14 @@ NP-completeness was established by Garey, Johnson, and Stockmeyer @gareyJohnsonS *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 $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 $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.* 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$). +] + #{ let x = load-model-example("RectilinearPictureCompression") let mat = x.instance.matrix diff --git a/docs/paper/references.bib b/docs/paper/references.bib index f1313a079..da6f3e5ef 100644 --- a/docs/paper/references.bib +++ b/docs/paper/references.bib @@ -677,6 +677,28 @@ @article{cygan2014 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} +} + @article{raiha1981, author = {Kari-Jouko R{\"a}ih{\"a} and Esko Ukkonen}, title = {The Shortest Common Supersequence Problem over Binary Alphabet is {NP}-Complete}, diff --git a/problemreductions-cli/src/cli.rs b/problemreductions-cli/src/cli.rs index 77d9d8f64..39ae83f2c 100644 --- a/problemreductions-cli/src/cli.rs +++ b/problemreductions-cli/src/cli.rs @@ -259,6 +259,7 @@ Flags by problem type: LCS --strings, --bound [--alphabet-size] FAS --arcs [--weights] [--num-vertices] FVS --arcs [--weights] [--num-vertices] + PartiallyOrderedKnapsack --sizes, --values, --capacity, --precedences QAP --matrix (cost), --distance-matrix StrongConnectivityAugmentation --arcs, --candidate-arcs, --bound [--num-vertices] FlowShopScheduling --task-lengths, --deadline [--num-processors] @@ -472,6 +473,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, alias = "item-precedences")] + pub precedences: Option, /// Distance matrix for QuadraticAssignment (semicolon-separated rows, e.g., "0,1,2;1,0,1;2,1,0") #[arg(long)] pub distance_matrix: Option, diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index 6ac146c85..def73462a 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -14,9 +14,9 @@ use problemreductions::models::graph::{ }; use problemreductions::models::misc::{ BinPacking, FlowShopScheduling, LongestCommonSubsequence, MinimumTardinessSequencing, - MultiprocessorScheduling, PaintShop, RectilinearPictureCompression, - SequencingWithReleaseTimesAndDeadlines, SequencingWithinIntervals, ShortestCommonSupersequence, - StringToStringCorrection, SubsetSum, SumOfSquaresPartition, + MultiprocessorScheduling, PaintShop, PartiallyOrderedKnapsack, RectilinearPictureCompression, + SequencingWithReleaseTimesAndDeadlines, SequencingWithinIntervals, + ShortestCommonSupersequence, StringToStringCorrection, SubsetSum, SumOfSquaresPartition, }; use problemreductions::models::BiconnectivityAugmentation; use problemreductions::prelude::*; @@ -84,6 +84,8 @@ fn all_data_flags_empty(args: &CreateArgs) -> bool { && args.pattern.is_none() && args.strings.is_none() && args.arcs.is_none() + && args.values.is_none() + && args.precedences.is_none() && args.distance_matrix.is_none() && args.candidate_arcs.is_none() && args.potential_edges.is_none() @@ -2104,6 +2106,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, 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(|| { + 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 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() { + 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( + weights, + values, + precedences, + capacity, + ))?, + resolved_variant.clone(), + ) + } + // PrimeAttributeName "PrimeAttributeName" => { let universe = args.universe.ok_or_else(|| { @@ -3788,6 +3836,8 @@ mod tests { pattern: None, strings: None, arcs: None, + values: None, + precedences: None, distance_matrix: None, potential_edges: None, budget: None, diff --git a/src/models/misc/mod.rs b/src/models/misc/mod.rs index 6c0049285..ad6aef19c 100644 --- a/src/models/misc/mod.rs +++ b/src/models/misc/mod.rs @@ -9,6 +9,7 @@ //! - [`LongestCommonSubsequence`]: Longest Common Subsequence //! - [`MinimumTardinessSequencing`]: Minimize tardy tasks in single-machine scheduling //! - [`PaintShop`]: Minimize color switches in paint shop scheduling +//! - [`PartiallyOrderedKnapsack`]: Knapsack with precedence constraints //! - [`PrecedenceConstrainedScheduling`]: Schedule unit tasks on processors by deadline //! - [`RectilinearPictureCompression`]: Cover 1-entries with bounded rectangles //! - [`SequencingWithReleaseTimesAndDeadlines`]: Single-machine scheduling feasibility @@ -26,6 +27,7 @@ mod longest_common_subsequence; mod minimum_tardiness_sequencing; mod multiprocessor_scheduling; pub(crate) mod paintshop; +pub(crate) mod partially_ordered_knapsack; mod precedence_constrained_scheduling; mod rectilinear_picture_compression; mod sequencing_with_release_times_and_deadlines; @@ -44,6 +46,7 @@ pub use longest_common_subsequence::LongestCommonSubsequence; pub use minimum_tardiness_sequencing::MinimumTardinessSequencing; pub use multiprocessor_scheduling::MultiprocessorScheduling; pub use paintshop::PaintShop; +pub use partially_ordered_knapsack::PartiallyOrderedKnapsack; pub use precedence_constrained_scheduling::PrecedenceConstrainedScheduling; pub use rectilinear_picture_compression::RectilinearPictureCompression; pub use sequencing_with_release_times_and_deadlines::SequencingWithReleaseTimesAndDeadlines; @@ -65,6 +68,7 @@ pub(crate) fn canonical_model_example_specs() -> 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" }, + ], + } +} + +/// The Partially Ordered Knapsack problem. +/// +/// 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 +/// +/// 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], // weights +/// 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()); +/// ``` +/// +// Raw serialization helper for [`PartiallyOrderedKnapsack`]. +#[derive(Serialize, Deserialize)] +struct PartiallyOrderedKnapsackRaw { + weights: Vec, + values: Vec, + precedences: Vec<(usize, usize)>, + capacity: i64, +} + +#[derive(Debug, Clone)] +pub struct PartiallyOrderedKnapsack { + weights: 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 { + weights: self.weights.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.weights, + raw.values, + raw.precedences, + raw.capacity, + )) + } +} + +impl PartiallyOrderedKnapsack { + /// Create a new PartiallyOrderedKnapsack instance. + /// + /// # Arguments + /// * `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 C + /// + /// # Panics + /// 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( + weights: Vec, + values: Vec, + precedences: Vec<(usize, usize)>, + capacity: i64, + ) -> Self { + assert_eq!( + weights.len(), + values.len(), + "weights and values must have the same length" + ); + assert!(capacity >= 0, "capacity must be non-negative"); + 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 = 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})"); + } + 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, preds) in predecessors.iter().enumerate() { + assert!( + !preds.contains(&i), + "precedences contain a cycle involving item {i}" + ); + } + Self { + weights, + 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 weights. + pub fn weights(&self) -> &[i64] { + &self.weights + } + + /// 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.weights.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 precomputed transitive predecessors: if item `b` is selected, + /// all its predecessors must also be selected. + fn is_downward_closed(&self, config: &[usize]) -> bool { + for (b, preds) in self.predecessors.iter().enumerate() { + if config[b] == 1 { + for &a in preds { + if 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_weight: i64 = config + .iter() + .enumerate() + .filter(|(_, &x)| x == 1) + .map(|(i, _)| self.weights[i]) + .sum(); + if total_weight > 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! { + 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", + 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}), + }] +} + +#[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 d712ad00e..c6828711c 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -24,6 +24,7 @@ pub use graph::{ StrongConnectivityAugmentation, SubgraphIsomorphism, TravelingSalesman, UndirectedTwoCommodityIntegralFlow, }; +pub use misc::PartiallyOrderedKnapsack; pub use misc::{ BinPacking, Factoring, FlowShopScheduling, Knapsack, LongestCommonSubsequence, MinimumTardinessSequencing, MultiprocessorScheduling, PaintShop, 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..8a7f53642 --- /dev/null +++ b/src/unit_tests/models/misc/partially_ordered_knapsack.rs @@ -0,0 +1,210 @@ +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.weights(), &[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.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 = "weights 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); +} + +#[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); +} diff --git a/src/unit_tests/trait_consistency.rs b/src/unit_tests/trait_consistency.rs index 094969a60..29a139915 100644 --- a/src/unit_tests/trait_consistency.rs +++ b/src/unit_tests/trait_consistency.rs @@ -142,6 +142,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", + ); check_problem_trait( &SequencingWithReleaseTimesAndDeadlines::new(vec![1, 2, 1], vec![0, 0, 2], vec![3, 3, 4]), "SequencingWithReleaseTimesAndDeadlines", @@ -226,4 +230,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 + ); }