diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index 6202fbde5..70827d5cd 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -55,6 +55,7 @@ "BicliqueCover": [Biclique Cover], "BinPacking": [Bin Packing], "ClosestVectorProblem": [Closest Vector Problem], + "OptimalLinearArrangement": [Optimal Linear Arrangement], "RuralPostman": [Rural Postman], "LongestCommonSubsequence": [Longest Common Subsequence], "SubsetSum": [Subset Sum], @@ -63,7 +64,6 @@ "ShortestCommonSupersequence": [Shortest Common Supersequence], "MinimumSumMulticenter": [Minimum Sum Multicenter], "SubgraphIsomorphism": [Subgraph Isomorphism], - "SubsetSum": [Subset Sum], "FlowShopScheduling": [Flow Shop Scheduling], ) @@ -578,6 +578,15 @@ One of the most intensely studied NP-hard problems, with applications in logisti caption: [Complete graph $K_4$ with weighted edges. The optimal tour $v_0 -> v_1 -> v_2 -> v_3 -> v_0$ (blue edges) has cost 6.], ) ] +#problem-def("OptimalLinearArrangement")[ + Given an undirected graph $G=(V,E)$ and a non-negative integer $K$, is there a bijection $f: V -> {0, 1, dots, |V|-1}$ such that $sum_({u,v} in E) |f(u) - f(v)| <= K$? +][ +A classical NP-complete decision problem from Garey & Johnson (GT42) @garey1979, with applications in VLSI design, graph drawing, and sparse matrix reordering. The problem asks whether vertices can be placed on a line so that the total "stretch" of all edges is at most $K$. + +NP-completeness was established by Garey, Johnson, and Stockmeyer @gareyJohnsonStockmeyer1976, via reduction from Simple Max Cut. The problem remains NP-complete on bipartite graphs, but is solvable in polynomial time on trees. The best known exact algorithm for general graphs uses dynamic programming over subsets in $O^*(2^n)$ time and space (Held-Karp style), analogous to TSP. + +*Example.* Consider the path graph $P_3$: vertices ${v_0, v_1, v_2}$ with edges ${v_0, v_1}$ and ${v_1, v_2}$. The identity arrangement $f(v_i) = i$ gives cost $|0-1| + |1-2| = 2$. With bound $K = 2$, this is a YES instance. For a triangle $K_3$ with the same vertex set plus edge ${v_0, v_2}$, any arrangement yields cost 4, so a bound of $K = 3$ gives a NO instance. +] #problem-def("MaximumClique")[ Given $G = (V, E)$, find $K subset.eq V$ maximizing $|K|$ such that all pairs in $K$ are adjacent: $forall u, v in K: (u, v) in E$. Equivalent to MIS on the complement graph $overline(G)$. ][ diff --git a/docs/paper/references.bib b/docs/paper/references.bib index 88bbc3723..505532c49 100644 --- a/docs/paper/references.bib +++ b/docs/paper/references.bib @@ -22,6 +22,16 @@ @book{garey1979 year = {1979} } +@article{gareyJohnsonStockmeyer1976, + author = {Michael R. Garey and David S. Johnson and Larry Stockmeyer}, + title = {Some Simplified {NP}-Complete Graph Problems}, + journal = {Theoretical Computer Science}, + volume = {1}, + number = {3}, + pages = {237--267}, + year = {1976} +} + @article{glover2019, author = {Fred Glover and Gary Kochenberger and Yu Du}, title = {Quantum Bridge Analytics {I}: a tutorial on formulating and using {QUBO} models}, diff --git a/docs/src/reductions/problem_schemas.json b/docs/src/reductions/problem_schemas.json index e8f6f968a..d46b150ae 100644 --- a/docs/src/reductions/problem_schemas.json +++ b/docs/src/reductions/problem_schemas.json @@ -424,6 +424,22 @@ } ] }, + { + "name": "OptimalLinearArrangement", + "description": "Find a vertex ordering on a line with total edge length at most K", + "fields": [ + { + "name": "graph", + "type_name": "G", + "description": "The undirected graph G=(V,E)" + }, + { + "name": "bound", + "type_name": "usize", + "description": "Upper bound K on total edge length" + } + ] + }, { "name": "PaintShop", "description": "Minimize color changes in paint shop sequence", diff --git a/problemreductions-cli/src/cli.rs b/problemreductions-cli/src/cli.rs index 64d98d723..3a18e8a60 100644 --- a/problemreductions-cli/src/cli.rs +++ b/problemreductions-cli/src/cli.rs @@ -220,6 +220,7 @@ Flags by problem type: BicliqueCover --left, --right, --biedges, --k BMF --matrix (0/1), --rank CVP --basis, --target-vec [--bounds] + OptimalLinearArrangement --graph, --bound RuralPostman (RPP) --graph, --edge-weights, --required-edges, --bound SubgraphIsomorphism --graph (host), --pattern (pattern) LCS --strings diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index c7a809919..97035d180 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -108,6 +108,7 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str { } "PartitionIntoTriangles" => "--graph 0-1,1-2,0-2", "Factoring" => "--target 15 --m 4 --n 4", + "OptimalLinearArrangement" => "--graph 0-1,1-2,2-3 --bound 5", "MinimumFeedbackArcSet" => "--arcs \"0>1,1>2,2>0\"", "RuralPostman" => { "--graph 0-1,1-2,2-3,3-0 --edge-weights 1,1,1,1 --required-edges 0,2 --bound 4" @@ -609,6 +610,25 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { ) } + // OptimalLinearArrangement — graph + bound + "OptimalLinearArrangement" => { + let (graph, _) = parse_graph(args).map_err(|e| { + anyhow::anyhow!( + "{e}\n\nUsage: pred create OptimalLinearArrangement --graph 0-1,1-2,2-3 --bound 5" + ) + })?; + let bound = args.bound.ok_or_else(|| { + anyhow::anyhow!( + "OptimalLinearArrangement requires --bound (upper bound K on total edge length)\n\n\ + Usage: pred create OptimalLinearArrangement --graph 0-1,1-2,2-3 --bound 5" + ) + })? as usize; + ( + ser(OptimalLinearArrangement::new(graph, bound))?, + resolved_variant.clone(), + ) + } + // FlowShopScheduling "FlowShopScheduling" => { let task_str = args.task_lengths.as_deref().ok_or_else(|| { @@ -1421,11 +1441,28 @@ fn create_random( util::ser_kcoloring(graph, k)? } + // OptimalLinearArrangement — graph + bound + "OptimalLinearArrangement" => { + let edge_prob = args.edge_prob.unwrap_or(0.5); + if !(0.0..=1.0).contains(&edge_prob) { + bail!("--edge-prob must be between 0.0 and 1.0"); + } + let graph = util::create_random_graph(num_vertices, edge_prob, args.seed); + // Default bound: (n-1) * num_edges ensures satisfiability (max edge stretch is n-1) + let n = graph.num_vertices(); + let bound = args + .bound + .map(|b| b as usize) + .unwrap_or((n.saturating_sub(1)) * graph.num_edges()); + let variant = variant_map(&[("graph", "SimpleGraph")]); + (ser(OptimalLinearArrangement::new(graph, bound))?, variant) + } + _ => bail!( "Random generation is not supported for {canonical}. \ Supported: graph-based problems (MIS, MVC, MaxCut, MaxClique, \ MaximumMatching, MinimumDominatingSet, SpinGlass, KColoring, TravelingSalesman, \ - HamiltonianPath)" + OptimalLinearArrangement, HamiltonianPath)" ), }; diff --git a/problemreductions-cli/src/dispatch.rs b/problemreductions-cli/src/dispatch.rs index 2d95f3bb6..fc2823fd9 100644 --- a/problemreductions-cli/src/dispatch.rs +++ b/problemreductions-cli/src/dispatch.rs @@ -255,6 +255,7 @@ pub fn load_problem( _ => deser_opt::>(data), }, "Knapsack" => deser_opt::(data), + "OptimalLinearArrangement" => deser_sat::>(data), "SubgraphIsomorphism" => deser_sat::(data), "PartitionIntoTriangles" => deser_sat::>(data), "LongestCommonSubsequence" => deser_opt::(data), @@ -330,6 +331,7 @@ pub fn serialize_any_problem( _ => try_ser::>(any), }, "Knapsack" => try_ser::(any), + "OptimalLinearArrangement" => try_ser::>(any), "SubgraphIsomorphism" => try_ser::(any), "PartitionIntoTriangles" => try_ser::>(any), "LongestCommonSubsequence" => try_ser::(any), diff --git a/problemreductions-cli/src/problem_name.rs b/problemreductions-cli/src/problem_name.rs index 55d1cf900..bc2c14414 100644 --- a/problemreductions-cli/src/problem_name.rs +++ b/problemreductions-cli/src/problem_name.rs @@ -23,6 +23,7 @@ pub const ALIASES: &[(&str, &str)] = &[ ("RPP", "RuralPostman"), ("LCS", "LongestCommonSubsequence"), ("MaxMatching", "MaximumMatching"), + ("OLA", "OptimalLinearArrangement"), ("FVS", "MinimumFeedbackVertexSet"), ("SCS", "ShortestCommonSupersequence"), ("FAS", "MinimumFeedbackArcSet"), @@ -61,6 +62,7 @@ pub fn resolve_alias(input: &str) -> String { "binpacking" => "BinPacking".to_string(), "cvp" | "closestvectorproblem" => "ClosestVectorProblem".to_string(), "knapsack" => "Knapsack".to_string(), + "optimallineararrangement" | "ola" => "OptimalLinearArrangement".to_string(), "subgraphisomorphism" => "SubgraphIsomorphism".to_string(), "partitionintotriangles" => "PartitionIntoTriangles".to_string(), "lcs" | "longestcommonsubsequence" => "LongestCommonSubsequence".to_string(), diff --git a/src/lib.rs b/src/lib.rs index 4e0596ee2..4ecbe505d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -47,8 +47,8 @@ pub mod prelude { pub use crate::models::graph::{ KColoring, MaxCut, MaximalIS, MaximumClique, MaximumIndependentSet, MaximumMatching, MinimumDominatingSet, MinimumFeedbackArcSet, MinimumFeedbackVertexSet, - MinimumSumMulticenter, MinimumVertexCover, PartitionIntoTriangles, RuralPostman, - TravelingSalesman, + MinimumSumMulticenter, MinimumVertexCover, OptimalLinearArrangement, + PartitionIntoTriangles, RuralPostman, TravelingSalesman, }; pub use crate::models::misc::{ BinPacking, Factoring, FlowShopScheduling, Knapsack, LongestCommonSubsequence, PaintShop, diff --git a/src/models/graph/mod.rs b/src/models/graph/mod.rs index e49e4ca5d..d63802b71 100644 --- a/src/models/graph/mod.rs +++ b/src/models/graph/mod.rs @@ -17,6 +17,7 @@ //! - [`SpinGlass`]: Ising model Hamiltonian //! - [`HamiltonianPath`]: Hamiltonian path (simple path visiting every vertex) //! - [`BicliqueCover`]: Biclique cover on bipartite graphs +//! - [`OptimalLinearArrangement`]: Optimal linear arrangement (total edge length at most K) //! - [`MinimumFeedbackArcSet`]: Minimum feedback arc set on directed graphs //! - [`MinimumSumMulticenter`]: Min-sum multicenter (p-median) //! - [`RuralPostman`]: Rural Postman (circuit covering required edges) @@ -37,6 +38,7 @@ pub(crate) mod minimum_feedback_arc_set; pub(crate) mod minimum_feedback_vertex_set; pub(crate) mod minimum_sum_multicenter; pub(crate) mod minimum_vertex_cover; +pub(crate) mod optimal_linear_arrangement; pub(crate) mod partition_into_triangles; pub(crate) mod rural_postman; pub(crate) mod spin_glass; @@ -58,6 +60,7 @@ pub use minimum_feedback_arc_set::MinimumFeedbackArcSet; pub use minimum_feedback_vertex_set::MinimumFeedbackVertexSet; pub use minimum_sum_multicenter::MinimumSumMulticenter; pub use minimum_vertex_cover::MinimumVertexCover; +pub use optimal_linear_arrangement::OptimalLinearArrangement; pub use partition_into_triangles::PartitionIntoTriangles; pub use rural_postman::RuralPostman; pub use spin_glass::SpinGlass; diff --git a/src/models/graph/optimal_linear_arrangement.rs b/src/models/graph/optimal_linear_arrangement.rs new file mode 100644 index 000000000..bc4cd341b --- /dev/null +++ b/src/models/graph/optimal_linear_arrangement.rs @@ -0,0 +1,167 @@ +//! Optimal Linear Arrangement problem implementation. +//! +//! The Optimal Linear Arrangement problem asks whether there exists a one-to-one +//! function f: V -> {0, 1, ..., |V|-1} such that the total edge length +//! sum_{{u,v} in E} |f(u) - f(v)| is at most K. + +use crate::registry::{FieldInfo, ProblemSchemaEntry}; +use crate::topology::{Graph, SimpleGraph}; +use crate::traits::{Problem, SatisfactionProblem}; +use serde::{Deserialize, Serialize}; + +inventory::submit! { + ProblemSchemaEntry { + name: "OptimalLinearArrangement", + module_path: module_path!(), + description: "Find a vertex ordering on a line with total edge length at most K", + fields: &[ + FieldInfo { name: "graph", type_name: "G", description: "The undirected graph G=(V,E)" }, + FieldInfo { name: "bound", type_name: "usize", description: "Upper bound K on total edge length" }, + ], + } +} + +/// The Optimal Linear Arrangement problem. +/// +/// Given an undirected graph G = (V, E) and a non-negative integer K, +/// determine whether there exists a one-to-one function f: V -> {0, 1, ..., |V|-1} +/// such that sum_{{u,v} in E} |f(u) - f(v)| <= K. +/// +/// This is the decision (satisfaction) version of the problem, following the +/// Garey & Johnson formulation (GT42). +/// +/// # Representation +/// +/// Each vertex is assigned a variable representing its position in the arrangement. +/// Variable i takes a value in {0, 1, ..., n-1}, and a valid configuration must be +/// a permutation (all positions are distinct) with total edge length at most K. +/// +/// # Type Parameters +/// +/// * `G` - The graph type (e.g., `SimpleGraph`) +/// +/// # Example +/// +/// ``` +/// use problemreductions::models::graph::OptimalLinearArrangement; +/// use problemreductions::topology::SimpleGraph; +/// use problemreductions::{Problem, Solver, BruteForce}; +/// +/// // Path graph: 0-1-2-3 with bound 3 +/// let graph = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]); +/// let problem = OptimalLinearArrangement::new(graph, 3); +/// +/// let solver = BruteForce::new(); +/// let solution = solver.find_satisfying(&problem); +/// assert!(solution.is_some()); +/// ``` +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(bound(deserialize = "G: serde::Deserialize<'de>"))] +pub struct OptimalLinearArrangement { + /// The underlying graph. + graph: G, + /// Upper bound K on total edge length. + bound: usize, +} + +impl OptimalLinearArrangement { + /// Create a new Optimal Linear Arrangement problem. + /// + /// # Arguments + /// * `graph` - The undirected graph G = (V, E) + /// * `bound` - The upper bound K on total edge length + pub fn new(graph: G, bound: usize) -> Self { + Self { graph, bound } + } + + /// Get a reference to the underlying graph. + pub fn graph(&self) -> &G { + &self.graph + } + + /// Get the bound K. + pub fn bound(&self) -> usize { + self.bound + } + + /// Get the number of vertices in the underlying graph. + pub fn num_vertices(&self) -> usize { + self.graph.num_vertices() + } + + /// Get the number of edges in the underlying graph. + pub fn num_edges(&self) -> usize { + self.graph.num_edges() + } + + /// Check if a configuration is a valid permutation with total edge length at most K. + pub fn is_valid_solution(&self, config: &[usize]) -> bool { + match self.total_edge_length(config) { + Some(length) => length <= self.bound, + None => false, + } + } + + /// Check if a configuration forms a valid permutation of {0, ..., n-1}. + fn is_valid_permutation(&self, config: &[usize]) -> bool { + let n = self.graph.num_vertices(); + if config.len() != n { + return false; + } + let mut seen = vec![false; n]; + for &pos in config { + if pos >= n || seen[pos] { + return false; + } + seen[pos] = true; + } + true + } + + /// Compute the total edge length for a given arrangement. + /// + /// Returns `None` if the configuration is not a valid permutation. + pub fn total_edge_length(&self, config: &[usize]) -> Option { + if !self.is_valid_permutation(config) { + return None; + } + let mut total = 0usize; + for (u, v) in self.graph.edges() { + let fu = config[u]; + let fv = config[v]; + total += fu.abs_diff(fv); + } + Some(total) + } +} + +impl Problem for OptimalLinearArrangement +where + G: Graph + crate::variant::VariantParam, +{ + const NAME: &'static str = "OptimalLinearArrangement"; + type Metric = bool; + + fn variant() -> Vec<(&'static str, &'static str)> { + crate::variant_params![G] + } + + fn dims(&self) -> Vec { + let n = self.graph.num_vertices(); + vec![n; n] + } + + fn evaluate(&self, config: &[usize]) -> bool { + self.is_valid_solution(config) + } +} + +impl SatisfactionProblem for OptimalLinearArrangement {} + +crate::declare_variants! { + OptimalLinearArrangement => "2^num_vertices", +} + +#[cfg(test)] +#[path = "../../unit_tests/models/graph/optimal_linear_arrangement.rs"] +mod tests; diff --git a/src/models/mod.rs b/src/models/mod.rs index b22cc2a1c..72a1aa2a0 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -15,7 +15,8 @@ pub use graph::{ BicliqueCover, GraphPartitioning, HamiltonianPath, IsomorphicSpanningTree, KColoring, MaxCut, MaximalIS, MaximumClique, MaximumIndependentSet, MaximumMatching, MinimumDominatingSet, MinimumFeedbackArcSet, MinimumFeedbackVertexSet, MinimumSumMulticenter, MinimumVertexCover, - PartitionIntoTriangles, RuralPostman, SpinGlass, SubgraphIsomorphism, TravelingSalesman, + OptimalLinearArrangement, PartitionIntoTriangles, RuralPostman, SpinGlass, + SubgraphIsomorphism, TravelingSalesman, }; pub use misc::{ BinPacking, Factoring, FlowShopScheduling, Knapsack, LongestCommonSubsequence, PaintShop, diff --git a/src/unit_tests/models/graph/optimal_linear_arrangement.rs b/src/unit_tests/models/graph/optimal_linear_arrangement.rs new file mode 100644 index 000000000..c8371791a --- /dev/null +++ b/src/unit_tests/models/graph/optimal_linear_arrangement.rs @@ -0,0 +1,251 @@ +use super::*; +use crate::solvers::{BruteForce, Solver}; +use crate::topology::SimpleGraph; +use crate::traits::Problem; + +/// Issue example: 6 vertices, 7 edges, bound K=11 (YES instance) +fn issue_example_yes() -> OptimalLinearArrangement { + let graph = SimpleGraph::new( + 6, + vec![(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (0, 3), (2, 5)], + ); + OptimalLinearArrangement::new(graph, 11) +} + +/// Issue example: same graph, bound K=9 (NO instance) +fn issue_example_no() -> OptimalLinearArrangement { + let graph = SimpleGraph::new( + 6, + vec![(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (0, 3), (2, 5)], + ); + OptimalLinearArrangement::new(graph, 9) +} + +/// Path graph: 0-1-2-3-4-5, bound K=5 +fn path_example() -> OptimalLinearArrangement { + let graph = SimpleGraph::new(6, vec![(0, 1), (1, 2), (2, 3), (3, 4), (4, 5)]); + OptimalLinearArrangement::new(graph, 5) +} + +#[test] +fn test_optimallineararrangement_basic() { + let problem = issue_example_yes(); + + // Check dims: 6 variables, each with domain size 6 + assert_eq!(problem.dims(), vec![6, 6, 6, 6, 6, 6]); + + // Identity arrangement: f(i) = i + // Cost: |0-1| + |1-2| + |2-3| + |3-4| + |4-5| + |0-3| + |2-5| = 1+1+1+1+1+3+3 = 11 + let config = vec![0, 1, 2, 3, 4, 5]; + assert!(problem.evaluate(&config)); + assert_eq!(problem.total_edge_length(&config), Some(11)); +} + +#[test] +fn test_optimallineararrangement_no_instance() { + let problem = issue_example_no(); + + // Identity arrangement has cost 11 > 9 + let config = vec![0, 1, 2, 3, 4, 5]; + assert!(!problem.evaluate(&config)); + assert_eq!(problem.total_edge_length(&config), Some(11)); + + // Brute-force confirms no arrangement achieves cost <= 9 + let solver = BruteForce::new(); + assert!(solver.find_satisfying(&problem).is_none()); +} + +#[test] +fn test_optimallineararrangement_path() { + let problem = path_example(); + + // Identity arrangement on a path: each edge has length 1, total = 5 + let config = vec![0, 1, 2, 3, 4, 5]; + assert!(problem.evaluate(&config)); + assert_eq!(problem.total_edge_length(&config), Some(5)); +} + +#[test] +fn test_optimallineararrangement_invalid_config() { + let problem = issue_example_yes(); + + // Not a permutation: repeated value + assert!(!problem.evaluate(&[0, 0, 1, 2, 3, 4])); + assert_eq!(problem.total_edge_length(&[0, 0, 1, 2, 3, 4]), None); + + // Out of range + assert!(!problem.evaluate(&[0, 1, 2, 3, 4, 6])); + assert_eq!(problem.total_edge_length(&[0, 1, 2, 3, 4, 6]), None); + + // Wrong length + assert!(!problem.evaluate(&[0, 1, 2])); + assert_eq!(problem.total_edge_length(&[0, 1, 2]), None); +} + +#[test] +fn test_optimallineararrangement_serialization() { + let problem = issue_example_yes(); + let json = serde_json::to_string(&problem).unwrap(); + let deserialized: OptimalLinearArrangement = serde_json::from_str(&json).unwrap(); + assert_eq!(deserialized.graph().num_vertices(), 6); + assert_eq!(deserialized.graph().num_edges(), 7); + assert_eq!(deserialized.bound(), 11); + + // Verify evaluation is consistent after round-trip + let config = vec![0, 1, 2, 3, 4, 5]; + assert_eq!(problem.evaluate(&config), deserialized.evaluate(&config)); +} + +#[test] +fn test_optimallineararrangement_solver() { + // Small graph: triangle, bound = 4 + // Any permutation of 3 vertices on a triangle has cost 4 + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]); + let problem = OptimalLinearArrangement::new(graph, 4); + + let solver = BruteForce::new(); + let solution = solver.find_satisfying(&problem); + assert!(solution.is_some()); + let sol = solution.unwrap(); + assert!(problem.evaluate(&sol)); + + // All satisfying solutions should be valid + let all_sat = solver.find_all_satisfying(&problem); + assert!(!all_sat.is_empty()); + for s in &all_sat { + assert!(problem.evaluate(s)); + } +} + +#[test] +fn test_optimallineararrangement_solver_no_solution() { + // Triangle with very tight bound + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]); + // Minimum cost for triangle is 4, so bound 3 should have no solution + let problem = OptimalLinearArrangement::new(graph, 3); + + let solver = BruteForce::new(); + let solution = solver.find_satisfying(&problem); + assert!(solution.is_none()); + + let all_sat = solver.find_all_satisfying(&problem); + assert!(all_sat.is_empty()); +} + +#[test] +fn test_optimallineararrangement_empty_graph() { + // No edges: any permutation has cost 0 + let graph = SimpleGraph::new(3, vec![]); + let problem = OptimalLinearArrangement::new(graph, 0); + + let solver = BruteForce::new(); + let all_sat = solver.find_all_satisfying(&problem); + // All 3! = 6 permutations should be valid + assert_eq!(all_sat.len(), 6); + for s in &all_sat { + assert!(problem.evaluate(s)); + assert_eq!(problem.total_edge_length(s), Some(0)); + } +} + +#[test] +fn test_optimallineararrangement_single_vertex() { + let graph = SimpleGraph::new(1, vec![]); + let problem = OptimalLinearArrangement::new(graph, 0); + + assert_eq!(problem.dims(), vec![1]); + assert!(problem.evaluate(&[0])); + assert_eq!(problem.total_edge_length(&[0]), Some(0)); +} + +#[test] +fn test_optimallineararrangement_size_getters() { + let problem = issue_example_yes(); + assert_eq!(problem.num_vertices(), 6); + assert_eq!(problem.num_edges(), 7); + assert_eq!(problem.bound(), 11); +} + +#[test] +fn test_optimallineararrangement_graph_accessor() { + let problem = issue_example_yes(); + let graph = problem.graph(); + assert_eq!(graph.num_vertices(), 6); + assert_eq!(graph.num_edges(), 7); +} + +#[test] +fn test_optimallineararrangement_problem_name() { + assert_eq!( + as Problem>::NAME, + "OptimalLinearArrangement" + ); +} + +#[test] +fn test_optimallineararrangement_two_vertices() { + // Single edge: 0-1, bound = 1 + let graph = SimpleGraph::new(2, vec![(0, 1)]); + let problem = OptimalLinearArrangement::new(graph, 1); + + // Both permutations [0,1] and [1,0] have cost 1 + assert!(problem.evaluate(&[0, 1])); + assert!(problem.evaluate(&[1, 0])); + assert_eq!(problem.total_edge_length(&[0, 1]), Some(1)); + assert_eq!(problem.total_edge_length(&[1, 0]), Some(1)); +} + +#[test] +fn test_optimallineararrangement_permutation_matters() { + // Path 0-1-2-3, bound = 4 + let graph = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]); + let problem = OptimalLinearArrangement::new(graph, 4); + + // Identity: cost = 1+1+1 = 3 <= 4, valid + assert!(problem.evaluate(&[0, 1, 2, 3])); + assert_eq!(problem.total_edge_length(&[0, 1, 2, 3]), Some(3)); + + // Reversed: cost = 1+1+1 = 3 <= 4, valid + assert!(problem.evaluate(&[3, 2, 1, 0])); + assert_eq!(problem.total_edge_length(&[3, 2, 1, 0]), Some(3)); + + // Scrambled: [2, 0, 3, 1] -> f(0)=2, f(1)=0, f(2)=3, f(3)=1 + // |2-0| + |0-3| + |3-1| = 2+3+2 = 7 > 4 + let scrambled = vec![2, 0, 3, 1]; + assert!(!problem.evaluate(&scrambled)); + assert_eq!(problem.total_edge_length(&scrambled), Some(7)); +} + +#[test] +fn test_optimallineararrangement_is_valid_solution() { + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); + let problem = OptimalLinearArrangement::new(graph, 2); + + // Valid permutation, cost = 2 <= 2 + assert!(problem.is_valid_solution(&[0, 1, 2])); + // Valid permutation, cost = 2 <= 2 + assert!(problem.is_valid_solution(&[2, 1, 0])); + // Not a permutation + assert!(!problem.is_valid_solution(&[0, 0, 1])); + // Wrong length + assert!(!problem.is_valid_solution(&[0, 1])); + // Out of range + assert!(!problem.is_valid_solution(&[0, 1, 3])); +} + +#[test] +fn test_optimallineararrangement_complete_graph_k4() { + // K4: all 6 edges present, bound = 10 + // For K4, any linear arrangement has cost 1+2+3+1+2+1 = 10 + let graph = SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]); + let problem = OptimalLinearArrangement::new(graph, 10); + + let solver = BruteForce::new(); + let all_sat = solver.find_all_satisfying(&problem); + // All 4! = 24 permutations should be valid since all have cost 10 + assert_eq!(all_sat.len(), 24); + for sol in &all_sat { + assert!(problem.evaluate(sol)); + assert_eq!(problem.total_edge_length(sol), Some(10)); + } +} diff --git a/src/unit_tests/trait_consistency.rs b/src/unit_tests/trait_consistency.rs index 20fea88a0..ebbc68a0e 100644 --- a/src/unit_tests/trait_consistency.rs +++ b/src/unit_tests/trait_consistency.rs @@ -103,6 +103,10 @@ fn test_all_problems_implement_trait_correctly() { &HamiltonianPath::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)])), "HamiltonianPath", ); + check_problem_trait( + &OptimalLinearArrangement::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), 3), + "OptimalLinearArrangement", + ); check_problem_trait( &IsomorphicSpanningTree::new( SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]),