diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index 2b632fad..56c09294 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -60,6 +60,7 @@ "QUBO": [QUBO], "ILP": [Integer Linear Programming], "Knapsack": [Knapsack], + "PartiallyOrderedKnapsack": [Partially Ordered Knapsack], "Satisfiability": [SAT], "KSatisfiability": [$k$-SAT], "CircuitSAT": [CircuitSAT], @@ -1164,6 +1165,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 = 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$). +] + #problem-def("RuralPostman")[ Given an undirected graph $G = (V, E)$ with edge lengths $l: E -> ZZ_(gt.eq 0)$, a subset $E' subset.eq E$ of required edges, and a bound $B in ZZ^+$, determine whether there exists a circuit (closed walk) in $G$ that traverses every edge in $E'$ and has total length at most $B$. ][ diff --git a/docs/paper/references.bib b/docs/paper/references.bib index 64bd0b76..dec54e50 100644 --- a/docs/paper/references.bib +++ b/docs/paper/references.bib @@ -541,6 +541,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/docs/src/reductions/problem_schemas.json b/docs/src/reductions/problem_schemas.json index 43b2a445..ebee534a 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 bb8c0255..e565e511 100644 --- a/docs/src/reductions/reduction_graph.json +++ b/docs/src/reductions/reduction_graph.json @@ -415,6 +415,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 +542,7 @@ }, { "source": 4, - "target": 52, + "target": 53, "overhead": [ { "field": "num_spins", @@ -595,7 +602,7 @@ }, { "source": 11, - "target": 47, + "target": 48, "overhead": [ { "field": "num_vars", @@ -636,7 +643,7 @@ }, { "source": 18, - "target": 47, + "target": 48, "overhead": [ { "field": "num_vars", @@ -662,7 +669,7 @@ }, { "source": 19, - "target": 47, + "target": 48, "overhead": [ { "field": "num_vars", @@ -688,7 +695,7 @@ }, { "source": 20, - "target": 47, + "target": 48, "overhead": [ { "field": "num_vars", @@ -699,7 +706,7 @@ }, { "source": 20, - "target": 54, + "target": 55, "overhead": [ { "field": "num_elements", @@ -710,7 +717,7 @@ }, { "source": 21, - "target": 49, + "target": 50, "overhead": [ { "field": "num_clauses", @@ -729,7 +736,7 @@ }, { "source": 22, - "target": 47, + "target": 48, "overhead": [ { "field": "num_vars", @@ -755,7 +762,7 @@ }, { "source": 24, - "target": 52, + "target": 53, "overhead": [ { "field": "num_spins", @@ -1070,7 +1077,7 @@ }, { "source": 36, - "target": 47, + "target": 48, "overhead": [ { "field": "num_vars", @@ -1185,7 +1192,7 @@ "doc_path": "rules/minimumvertexcover_minimumsetcovering/index.html" }, { - "source": 47, + "source": 48, "target": 11, "overhead": [ { @@ -1200,8 +1207,8 @@ "doc_path": "rules/qubo_ilp/index.html" }, { - "source": 47, - "target": 51, + "source": 48, + "target": 52, "overhead": [ { "field": "num_spins", @@ -1211,7 +1218,7 @@ "doc_path": "rules/spinglass_qubo/index.html" }, { - "source": 49, + "source": 50, "target": 4, "overhead": [ { @@ -1226,7 +1233,7 @@ "doc_path": "rules/sat_circuitsat/index.html" }, { - "source": 49, + "source": 50, "target": 15, "overhead": [ { @@ -1241,7 +1248,7 @@ "doc_path": "rules/sat_coloring/index.html" }, { - "source": 49, + "source": 50, "target": 20, "overhead": [ { @@ -1256,7 +1263,7 @@ "doc_path": "rules/sat_ksat/index.html" }, { - "source": 49, + "source": 50, "target": 29, "overhead": [ { @@ -1271,7 +1278,7 @@ "doc_path": "rules/sat_maximumindependentset/index.html" }, { - "source": 49, + "source": 50, "target": 38, "overhead": [ { @@ -1286,8 +1293,8 @@ "doc_path": "rules/sat_minimumdominatingset/index.html" }, { - "source": 51, - "target": 47, + "source": 52, + "target": 48, "overhead": [ { "field": "num_vars", @@ -1297,7 +1304,7 @@ "doc_path": "rules/spinglass_qubo/index.html" }, { - "source": 52, + "source": 53, "target": 24, "overhead": [ { @@ -1312,8 +1319,8 @@ "doc_path": "rules/spinglass_maxcut/index.html" }, { - "source": 52, - "target": 51, + "source": 53, + "target": 52, "overhead": [ { "field": "num_spins", @@ -1327,7 +1334,7 @@ "doc_path": "rules/spinglass_casts/index.html" }, { - "source": 55, + "source": 56, "target": 11, "overhead": [ { @@ -1342,8 +1349,8 @@ "doc_path": "rules/travelingsalesman_ilp/index.html" }, { - "source": 55, - "target": 47, + "source": 56, + "target": 48, "overhead": [ { "field": "num_vars", diff --git a/problemreductions-cli/src/cli.rs b/problemreductions-cli/src/cli.rs index 0e67c051..a1da677d 100644 --- a/problemreductions-cli/src/cli.rs +++ b/problemreductions-cli/src/cli.rs @@ -239,6 +239,7 @@ Flags by problem type: LCS --strings FAS --arcs [--weights] [--num-vertices] FVS --arcs [--weights] [--num-vertices] + PartiallyOrderedKnapsack --sizes, --values, --capacity, --precedences FlowShopScheduling --task-lengths, --deadline [--num-processors] SCS --strings, --bound [--alphabet-size] ILP, CircuitSAT (via reduction only) @@ -382,6 +383,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, /// 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 b89d65c9..1e486e1d 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -8,7 +8,7 @@ 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, + BinPacking, FlowShopScheduling, LongestCommonSubsequence, PaintShop, PartiallyOrderedKnapsack, ShortestCommonSupersequence, SubsetSum, }; use problemreductions::prelude::*; @@ -57,6 +57,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.task_lengths.is_none() && args.deadline.is_none() && args.num_processors.is_none() @@ -982,6 +984,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 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.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/src/models/graph/maximum_independent_set.rs b/src/models/graph/maximum_independent_set.rs index 0b8a3ddf..1177398b 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 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()); +/// ``` +/// 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 { + /// 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, if any size or + /// capacity is negative, if any precedence index is out of bounds, or if + /// the precedences contain a cycle. + 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" + ); + 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, preds) in predecessors.iter().enumerate() { + assert!( + !preds.contains(&i), + "precedences contain a cycle involving item {i}" + ); + } + 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 + } + + /// 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 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_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! { + 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/models/mod.rs b/src/models/mod.rs index e4448805..71a8caaa 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -18,6 +18,7 @@ pub use graph::{ OptimalLinearArrangement, PartitionIntoTriangles, RuralPostman, SpinGlass, SubgraphIsomorphism, TravelingSalesman, }; +pub use misc::PartiallyOrderedKnapsack; pub use misc::{ BinPacking, Factoring, FlowShopScheduling, Knapsack, LongestCommonSubsequence, PaintShop, ShortestCommonSupersequence, SubsetSum, 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 00000000..2b16feae --- /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); +} diff --git a/src/unit_tests/trait_consistency.rs b/src/unit_tests/trait_consistency.rs index ebbc68a0..9332440b 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 + ); }