From 6b6cfa2b3b62f6fd8df74eb142c82c12c7109d1d Mon Sep 17 00:00:00 2001 From: zazabap Date: Thu, 12 Mar 2026 14:33:53 +0000 Subject: [PATCH 1/8] Add plan for #406: OptimalLinearArrangement Co-Authored-By: Claude Opus 4.6 --- .../2026-03-12-optimal-linear-arrangement.md | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 docs/plans/2026-03-12-optimal-linear-arrangement.md diff --git a/docs/plans/2026-03-12-optimal-linear-arrangement.md b/docs/plans/2026-03-12-optimal-linear-arrangement.md new file mode 100644 index 000000000..803302078 --- /dev/null +++ b/docs/plans/2026-03-12-optimal-linear-arrangement.md @@ -0,0 +1,61 @@ +# Plan: Add OptimalLinearArrangement Model (Issue #406) + +## Overview + +Add the OptimalLinearArrangement problem model -- a classical NP-complete graph optimization problem (Garey & Johnson GT42) that asks for a vertex ordering on a line minimizing total edge length. + +## Design Decisions + +- **Optimization problem** with `Direction::Minimize` -- minimize sum of |f(u)-f(v)| over edges +- **Type parameter**: `G: Graph` only (no weight parameter -- edges are unweighted) +- **Configuration space**: `vec![n; n]` where n = |V|. Each variable assigns a position (0..n) to a vertex. A valid configuration is a permutation (all positions distinct). +- **Metric**: `SolutionSize` -- total edge length as usize +- **Category**: `graph/` (input is a graph) +- **Complexity**: O*(2^n) via Held-Karp-style DP over subsets + +## Steps + +### Step 1: Create model file `src/models/graph/optimal_linear_arrangement.rs` + +- `inventory::submit!` for ProblemSchemaEntry +- Struct `OptimalLinearArrangement` with field `graph: G` +- Constructor `new(graph: G)` +- Accessors: `graph()`, `num_vertices()`, `num_edges()` +- `is_valid_solution()` -- checks permutation validity +- `total_edge_length()` helper -- computes sum |f(u)-f(v)| for a permutation +- `Problem` impl: NAME="OptimalLinearArrangement", Metric=SolutionSize, dims=vec![n;n], evaluate checks permutation then returns Valid(cost) +- `OptimizationProblem` impl: Value=usize, direction=Minimize +- `declare_variants!` with `SimpleGraph => "2^num_vertices"` +- `variant_params![G]` (single type parameter) +- `#[cfg(test)] #[path]` link to unit tests + +### Step 2: Register model + +- `src/models/graph/mod.rs`: add `pub(crate) mod optimal_linear_arrangement;` and `pub use` +- `src/models/mod.rs`: add to graph re-export line +- `src/lib.rs` prelude: add `OptimalLinearArrangement` + +### Step 3: Register in CLI + +- `problemreductions-cli/src/dispatch.rs`: add match arms in `load_problem()` and `serialize_any_problem()` +- `problemreductions-cli/src/problem_name.rs`: add `"optimallineararrangement" => "OptimalLinearArrangement"` alias +- `problemreductions-cli/src/commands/create.rs`: add creation handler (graph-only, no weights) +- `problemreductions-cli/src/cli.rs`: add entry to "Flags by problem type" help table + +### Step 4: Write unit tests `src/unit_tests/models/graph/optimal_linear_arrangement.rs` + +- `test_optimal_linear_arrangement_creation` -- construct instance, verify dims +- `test_optimal_linear_arrangement_evaluation` -- valid and invalid configs +- `test_optimal_linear_arrangement_direction` -- verify Minimize +- `test_optimal_linear_arrangement_solver` -- brute-force finds optimal permutation +- `test_optimal_linear_arrangement_path_graph` -- path graph optimal = n-1 +- `test_optimal_linear_arrangement_serialization` -- round-trip serde + +### Step 5: Write paper entry + +- Add `"OptimalLinearArrangement": [Optimal Linear Arrangement]` to display-name dict +- Add `#problem-def("OptimalLinearArrangement")` entry + +### Step 6: Verify + +- `make check` (fmt + clippy + test) From abdcab929ec158863367997eebbe3c97b09367a9 Mon Sep 17 00:00:00 2001 From: zazabap Date: Thu, 12 Mar 2026 14:42:27 +0000 Subject: [PATCH 2/8] Implement #406: Add OptimalLinearArrangement model - Add OptimalLinearArrangement optimization problem (minimize total edge length) - Register in graph module, lib.rs prelude, CLI dispatch, alias, create command - Add 16 unit tests covering creation, evaluation, solver, serialization - Add problem-def entry in paper with display-name and references - Regenerate problem schemas JSON Co-Authored-By: Claude Opus 4.6 --- docs/paper/reductions.typ | 10 + docs/paper/references.bib | 10 + .../2026-03-12-optimal-linear-arrangement.md | 206 +++++++++++++----- docs/src/reductions/problem_schemas.json | 11 + problemreductions-cli/src/cli.rs | 1 + problemreductions-cli/src/commands/create.rs | 28 ++- problemreductions-cli/src/dispatch.rs | 2 + problemreductions-cli/src/problem_name.rs | 1 + src/lib.rs | 2 +- src/models/graph/mod.rs | 3 + .../graph/optimal_linear_arrangement.rs | 160 ++++++++++++++ src/models/mod.rs | 3 +- .../graph/optimal_linear_arrangement.rs | 169 ++++++++++++++ 13 files changed, 548 insertions(+), 58 deletions(-) create mode 100644 src/models/graph/optimal_linear_arrangement.rs create mode 100644 src/unit_tests/models/graph/optimal_linear_arrangement.rs diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index 915878827..1e3cb4b4e 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -52,6 +52,7 @@ "BicliqueCover": [Biclique Cover], "BinPacking": [Bin Packing], "ClosestVectorProblem": [Closest Vector Problem], + "OptimalLinearArrangement": [Optimal Linear Arrangement], ) // Definition label: "def:" — each definition block must have a matching label @@ -456,6 +457,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)$, find a bijection $f: V -> {1, 2, dots, |V|}$ that minimizes $sum_({u,v} in E) |f(u) - f(v)|$. +][ +A classical NP-complete problem from Garey & Johnson (GT42) @garey1979, with applications in VLSI design, graph drawing, and sparse matrix reordering. The problem asks to place vertices on a line so that the total "stretch" of all edges is minimized. + +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+1$ gives cost $|1-2| + |2-3| = 2$, which is optimal. For a triangle $K_3$ with the same vertex set plus edge ${v_0, v_2}$, any arrangement yields cost $|f(v_0)-f(v_1)| + |f(v_1)-f(v_2)| + |f(v_0)-f(v_2)| = 4$. +] #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 c3129a691..ba32b4a4d 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/plans/2026-03-12-optimal-linear-arrangement.md b/docs/plans/2026-03-12-optimal-linear-arrangement.md index 803302078..88d231cc9 100644 --- a/docs/plans/2026-03-12-optimal-linear-arrangement.md +++ b/docs/plans/2026-03-12-optimal-linear-arrangement.md @@ -1,61 +1,157 @@ -# Plan: Add OptimalLinearArrangement Model (Issue #406) +# Plan: Add OptimalLinearArrangement Model (#406) -## Overview - -Add the OptimalLinearArrangement problem model -- a classical NP-complete graph optimization problem (Garey & Johnson GT42) that asks for a vertex ordering on a line minimizing total edge length. - -## Design Decisions - -- **Optimization problem** with `Direction::Minimize` -- minimize sum of |f(u)-f(v)| over edges -- **Type parameter**: `G: Graph` only (no weight parameter -- edges are unweighted) -- **Configuration space**: `vec![n; n]` where n = |V|. Each variable assigns a position (0..n) to a vertex. A valid configuration is a permutation (all positions distinct). -- **Metric**: `SolutionSize` -- total edge length as usize -- **Category**: `graph/` (input is a graph) -- **Complexity**: O*(2^n) via Held-Karp-style DP over subsets - -## Steps - -### Step 1: Create model file `src/models/graph/optimal_linear_arrangement.rs` - -- `inventory::submit!` for ProblemSchemaEntry -- Struct `OptimalLinearArrangement` with field `graph: G` -- Constructor `new(graph: G)` -- Accessors: `graph()`, `num_vertices()`, `num_edges()` -- `is_valid_solution()` -- checks permutation validity -- `total_edge_length()` helper -- computes sum |f(u)-f(v)| for a permutation -- `Problem` impl: NAME="OptimalLinearArrangement", Metric=SolutionSize, dims=vec![n;n], evaluate checks permutation then returns Valid(cost) -- `OptimizationProblem` impl: Value=usize, direction=Minimize -- `declare_variants!` with `SimpleGraph => "2^num_vertices"` -- `variant_params![G]` (single type parameter) -- `#[cfg(test)] #[path]` link to unit tests +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. -### Step 2: Register model - -- `src/models/graph/mod.rs`: add `pub(crate) mod optimal_linear_arrangement;` and `pub use` -- `src/models/mod.rs`: add to graph re-export line -- `src/lib.rs` prelude: add `OptimalLinearArrangement` - -### Step 3: Register in CLI - -- `problemreductions-cli/src/dispatch.rs`: add match arms in `load_problem()` and `serialize_any_problem()` -- `problemreductions-cli/src/problem_name.rs`: add `"optimallineararrangement" => "OptimalLinearArrangement"` alias -- `problemreductions-cli/src/commands/create.rs`: add creation handler (graph-only, no weights) -- `problemreductions-cli/src/cli.rs`: add entry to "Flags by problem type" help table - -### Step 4: Write unit tests `src/unit_tests/models/graph/optimal_linear_arrangement.rs` - -- `test_optimal_linear_arrangement_creation` -- construct instance, verify dims -- `test_optimal_linear_arrangement_evaluation` -- valid and invalid configs -- `test_optimal_linear_arrangement_direction` -- verify Minimize -- `test_optimal_linear_arrangement_solver` -- brute-force finds optimal permutation -- `test_optimal_linear_arrangement_path_graph` -- path graph optimal = n-1 -- `test_optimal_linear_arrangement_serialization` -- round-trip serde +## Overview -### Step 5: Write paper entry +Add the Optimal Linear Arrangement (OLA) problem as a graph optimization model. Given a graph G=(V,E), find a bijection f: V → {0,1,...,n-1} minimizing Σ_{(u,v)∈E} |f(u)-f(v)|. -- Add `"OptimalLinearArrangement": [Optimal Linear Arrangement]` to display-name dict -- Add `#problem-def("OptimalLinearArrangement")` entry +**Reference:** Garey & Johnson, A1.3 GT42. NP-complete [Garey, Johnson, Stockmeyer 1976]. -### Step 6: Verify +## Design Decisions -- `make check` (fmt + clippy + test) +- **Optimization problem**: `Direction::Minimize`, `Metric = SolutionSize` +- **Config space**: n variables, each with domain size n (positions 0..n-1). `dims() = vec![n; n]`. Valid configs are permutations. +- **Type parameter**: Only `G: Graph` (no weight type — objective is purely structural) +- **Complexity**: `2^num_vertices` (Held-Karp-style DP, analogous to TSP) +- **No weight methods**: No `weights()`, `set_weights()`, `is_weighted()` — not applicable + +## Tasks + +### Task 1: Create model file + +**File:** `src/models/graph/optimal_linear_arrangement.rs` + +Follow `src/models/graph/traveling_salesman.rs` as reference. + +#### Struct +```rust +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct OptimalLinearArrangement { + graph: G, +} +``` + +#### Schema registration +```rust +inventory::submit! { + ProblemSchemaEntry { + name: "OptimalLinearArrangement", + module_path: module_path!(), + description: "Find vertex ordering minimizing total edge length (Optimal Linear Arrangement)", + fields: &[ + FieldInfo { name: "graph", type_name: "G", description: "The undirected graph G=(V,E)" }, + ], + } +} +``` + +#### Inherent methods +- `new(graph: G) -> Self` +- `graph(&self) -> &G` +- `num_vertices(&self) -> usize` (under `G: Graph` bound, for overhead expressions) +- `num_edges(&self) -> usize` (under `G: Graph` bound, for overhead expressions) +- `is_valid_permutation(config: &[usize]) -> bool` — checks config has length n and is a permutation of 0..n-1 +- `arrangement_cost(config: &[usize]) -> i64` — computes Σ|config[u] - config[v]| for all edges (u,v) + +#### Problem trait +```rust +impl Problem for OptimalLinearArrangement { + const NAME: &'static str = "OptimalLinearArrangement"; + type Metric = SolutionSize; + fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![G] } + fn dims(&self) -> Vec { vec![self.graph.num_vertices(); self.graph.num_vertices()] } + fn evaluate(&self, config: &[usize]) -> SolutionSize { + if !self.is_valid_permutation(config) { return SolutionSize::Invalid; } + SolutionSize::Valid(self.arrangement_cost(config)) + } +} +``` + +#### OptimizationProblem trait +```rust +impl OptimizationProblem for OptimalLinearArrangement { + type Value = i64; + fn direction(&self) -> Direction { Direction::Minimize } +} +``` + +#### Variant declaration +```rust +crate::declare_variants! { + OptimalLinearArrangement => "2^num_vertices", +} +``` + +#### Test module link +```rust +#[cfg(test)] +#[path = "../../unit_tests/models/graph/optimal_linear_arrangement.rs"] +mod tests; +``` + +### Task 2: Register in module hierarchy + +1. `src/models/graph/mod.rs`: Add `pub(crate) mod optimal_linear_arrangement;` and `pub use optimal_linear_arrangement::OptimalLinearArrangement;` +2. `src/models/mod.rs`: Add `OptimalLinearArrangement` to the `pub use graph::{...}` line + +### Task 3: Register in CLI + +#### dispatch.rs +In `load_problem()` match: +```rust +"OptimalLinearArrangement" => deser_opt::>(data), +``` + +In `serialize_any_problem()` match: +```rust +"OptimalLinearArrangement" => try_ser::>(any), +``` + +Import `OptimalLinearArrangement` from the models at the top. + +#### problem_name.rs +In `resolve_alias()` match add: +```rust +"optimallineararrangement" | "ola" => "OptimalLinearArrangement".to_string(), +``` + +In `ALIASES` const add: +```rust +("OLA", "OptimalLinearArrangement"), +``` + +#### commands/create.rs +Add `"OptimalLinearArrangement"` as a graph-only problem (no weights): +- In the main `match` in `create()`, add a new arm that parses `--graph` and creates `OptimalLinearArrangement::new(graph)` +- Add to `example_for()`: `"OptimalLinearArrangement" => "--graph 0-1,1-2,2-3,0-3"` +- Add to `create_random()` support for random graph generation + +### Task 4: Write unit tests + +**File:** `src/unit_tests/models/graph/optimal_linear_arrangement.rs` + +Follow `src/unit_tests/models/graph/traveling_salesman.rs` as reference. + +Tests to include: +1. `test_optimal_linear_arrangement_creation` — basic construction, dims, num_vertices +2. `test_evaluate_valid_permutation` — identity permutation on path graph 0-1-2-3-4, cost = 4 +3. `test_evaluate_invalid_not_permutation` — duplicate positions → Invalid +4. `test_evaluate_invalid_out_of_range` — position >= n → Invalid +5. `test_evaluate_invalid_wrong_length` — wrong config length → Invalid +6. `test_direction` — Direction::Minimize +7. `test_problem_name` — NAME = "OptimalLinearArrangement" +8. `test_brute_force_path_graph` — path 0-1-2-3-4: optimal cost = 4 (identity arrangement) +9. `test_brute_force_issue_example` — 6-vertex graph from issue with 7 edges: verify optimal cost +10. `test_size_getters` — num_vertices, num_edges +11. `test_is_valid_permutation` — valid/invalid cases directly + +### Task 5: Write paper entry + +Add to `docs/paper/reductions.typ`: +- Add `"OptimalLinearArrangement": [Optimal Linear Arrangement]` to `display-name` dict +- Add `#problem-def("OptimalLinearArrangement")` entry with formal definition, background, example + +### Task 6: Verify + +Run `make check` (fmt + clippy + test). Fix any issues. diff --git a/docs/src/reductions/problem_schemas.json b/docs/src/reductions/problem_schemas.json index 50d327e9b..3db9a6859 100644 --- a/docs/src/reductions/problem_schemas.json +++ b/docs/src/reductions/problem_schemas.json @@ -333,6 +333,17 @@ } ] }, + { + "name": "OptimalLinearArrangement", + "description": "Find vertex ordering on a line minimizing total edge length", + "fields": [ + { + "name": "graph", + "type_name": "G", + "description": "The underlying graph G=(V,E)" + } + ] + }, { "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 91e9bd252..79f1138e0 100644 --- a/problemreductions-cli/src/cli.rs +++ b/problemreductions-cli/src/cli.rs @@ -216,6 +216,7 @@ Flags by problem type: BicliqueCover --left, --right, --biedges, --k BMF --matrix (0/1), --rank CVP --basis, --target-vec [--bounds] + OptimalLinearArrangement --graph ILP, CircuitSAT (via reduction only) Geometry graph variants (use slash notation, e.g., MIS/KingsSubgraph): diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index 3594a24a0..9e84623ac 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -83,6 +83,7 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str { "SpinGlass" => "--graph 0-1,1-2 --couplings 1,1", "KColoring" => "--graph 0-1,1-2,2-0 --k 3", "Factoring" => "--target 15 --m 4 --n 4", + "OptimalLinearArrangement" => "--graph 0-1,1-2,2-3", _ => "", } } @@ -442,6 +443,19 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { ) } + // OptimalLinearArrangement — graph only, no weights + "OptimalLinearArrangement" => { + let (graph, _) = parse_graph(args).map_err(|e| { + anyhow::anyhow!( + "{e}\n\nUsage: pred create OptimalLinearArrangement --graph 0-1,1-2,2-3" + ) + })?; + ( + ser(OptimalLinearArrangement::new(graph))?, + resolved_variant.clone(), + ) + } + _ => bail!("{}", crate::problem_name::unknown_problem_error(canonical)), }; @@ -935,10 +949,22 @@ fn create_random( util::ser_kcoloring(graph, k)? } + // OptimalLinearArrangement — graph only, no weights + "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); + let variant = variant_map(&[("graph", "SimpleGraph")]); + (ser(OptimalLinearArrangement::new(graph))?, variant) + } + _ => bail!( "Random generation is not supported for {canonical}. \ Supported: graph-based problems (MIS, MVC, MaxCut, MaxClique, \ - MaximumMatching, MinimumDominatingSet, SpinGlass, KColoring, TravelingSalesman)" + MaximumMatching, MinimumDominatingSet, SpinGlass, KColoring, TravelingSalesman, \ + OptimalLinearArrangement)" ), }; diff --git a/problemreductions-cli/src/dispatch.rs b/problemreductions-cli/src/dispatch.rs index 7a8498421..f499be61f 100644 --- a/problemreductions-cli/src/dispatch.rs +++ b/problemreductions-cli/src/dispatch.rs @@ -245,6 +245,7 @@ pub fn load_problem( _ => deser_opt::>(data), }, "Knapsack" => deser_opt::(data), + "OptimalLinearArrangement" => deser_opt::>(data), _ => bail!("{}", crate::problem_name::unknown_problem_error(&canonical)), } } @@ -305,6 +306,7 @@ pub fn serialize_any_problem( _ => try_ser::>(any), }, "Knapsack" => try_ser::(any), + "OptimalLinearArrangement" => 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 acd9b4b59..9b3889a6d 100644 --- a/problemreductions-cli/src/problem_name.rs +++ b/problemreductions-cli/src/problem_name.rs @@ -52,6 +52,7 @@ pub fn resolve_alias(input: &str) -> String { "binpacking" => "BinPacking".to_string(), "cvp" | "closestvectorproblem" => "ClosestVectorProblem".to_string(), "knapsack" => "Knapsack".to_string(), + "optimallineararrangement" => "OptimalLinearArrangement".to_string(), _ => input.to_string(), // pass-through for exact names } } diff --git a/src/lib.rs b/src/lib.rs index b0d99699a..c4a205379 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -43,7 +43,7 @@ pub mod prelude { pub use crate::models::graph::{BicliqueCover, SpinGlass}; pub use crate::models::graph::{ KColoring, MaxCut, MaximalIS, MaximumClique, MaximumIndependentSet, MaximumMatching, - MinimumDominatingSet, MinimumVertexCover, TravelingSalesman, + MinimumDominatingSet, MinimumVertexCover, OptimalLinearArrangement, TravelingSalesman, }; pub use crate::models::misc::{BinPacking, Factoring, Knapsack, PaintShop}; pub use crate::models::set::{MaximumSetPacking, MinimumSetCovering}; diff --git a/src/models/graph/mod.rs b/src/models/graph/mod.rs index 1198f7fbc..00d96645a 100644 --- a/src/models/graph/mod.rs +++ b/src/models/graph/mod.rs @@ -12,6 +12,7 @@ //! - [`TravelingSalesman`]: Traveling Salesman (minimum weight Hamiltonian cycle) //! - [`SpinGlass`]: Ising model Hamiltonian //! - [`BicliqueCover`]: Biclique cover on bipartite graphs +//! - [`OptimalLinearArrangement`]: Optimal linear arrangement (minimum total edge length) pub(crate) mod biclique_cover; pub(crate) mod kcoloring; @@ -22,6 +23,7 @@ pub(crate) mod maximum_independent_set; pub(crate) mod maximum_matching; pub(crate) mod minimum_dominating_set; pub(crate) mod minimum_vertex_cover; +pub(crate) mod optimal_linear_arrangement; pub(crate) mod spin_glass; pub(crate) mod traveling_salesman; @@ -34,5 +36,6 @@ pub use maximum_independent_set::MaximumIndependentSet; pub use maximum_matching::MaximumMatching; pub use minimum_dominating_set::MinimumDominatingSet; pub use minimum_vertex_cover::MinimumVertexCover; +pub use optimal_linear_arrangement::OptimalLinearArrangement; pub use spin_glass::SpinGlass; pub use traveling_salesman::TravelingSalesman; diff --git a/src/models/graph/optimal_linear_arrangement.rs b/src/models/graph/optimal_linear_arrangement.rs new file mode 100644 index 000000000..8b027ca47 --- /dev/null +++ b/src/models/graph/optimal_linear_arrangement.rs @@ -0,0 +1,160 @@ +//! Optimal Linear Arrangement problem implementation. +//! +//! The Optimal Linear Arrangement problem asks for a permutation of vertices +//! on a line that minimizes the total edge length (sum of |f(u) - f(v)| for all edges). + +use crate::registry::{FieldInfo, ProblemSchemaEntry}; +use crate::topology::{Graph, SimpleGraph}; +use crate::traits::{OptimizationProblem, Problem}; +use crate::types::{Direction, SolutionSize}; +use serde::{Deserialize, Serialize}; + +inventory::submit! { + ProblemSchemaEntry { + name: "OptimalLinearArrangement", + module_path: module_path!(), + description: "Find vertex ordering on a line minimizing total edge length", + fields: &[ + FieldInfo { name: "graph", type_name: "G", description: "The underlying graph G=(V,E)" }, + ], + } +} + +/// The Optimal Linear Arrangement problem. +/// +/// Given a graph G = (V, E), find a bijection f: V -> {0, 1, ..., |V|-1} +/// that minimizes the total edge length: sum_{(u,v) in E} |f(u) - f(v)|. +/// +/// # 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). +/// +/// # 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 +/// let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); +/// let problem = OptimalLinearArrangement::new(graph); +/// +/// let solver = BruteForce::new(); +/// let best = solver.find_best(&problem).unwrap(); +/// // Optimal: identity arrangement, cost = 2 +/// assert_eq!(problem.evaluate(&best).unwrap(), 2); +/// ``` +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct OptimalLinearArrangement { + /// The underlying graph. + graph: G, +} + +impl OptimalLinearArrangement { + /// Create an Optimal Linear Arrangement problem from a graph. + pub fn new(graph: G) -> Self { + Self { graph } + } + + /// Get a reference to the underlying graph. + pub fn graph(&self) -> &G { + &self.graph + } + + /// 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. + pub fn is_valid_solution(&self, config: &[usize]) -> bool { + self.is_valid_permutation(config) + } + + /// 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 = SolutionSize; + + 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]) -> SolutionSize { + match self.total_edge_length(config) { + Some(cost) => SolutionSize::Valid(cost), + None => SolutionSize::Invalid, + } + } +} + +impl OptimizationProblem for OptimalLinearArrangement +where + G: Graph + crate::variant::VariantParam, +{ + type Value = usize; + + fn direction(&self) -> Direction { + Direction::Minimize + } +} + +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 96b4b79d1..bee44aefb 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -13,7 +13,8 @@ pub use algebraic::{ClosestVectorProblem, BMF, ILP, QUBO}; pub use formula::{CNFClause, CircuitSAT, KSatisfiability, Satisfiability}; pub use graph::{ BicliqueCover, KColoring, MaxCut, MaximalIS, MaximumClique, MaximumIndependentSet, - MaximumMatching, MinimumDominatingSet, MinimumVertexCover, SpinGlass, TravelingSalesman, + MaximumMatching, MinimumDominatingSet, MinimumVertexCover, OptimalLinearArrangement, SpinGlass, + TravelingSalesman, }; pub use misc::{BinPacking, Factoring, Knapsack, PaintShop}; pub use set::{MaximumSetPacking, MinimumSetCovering}; 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..3142f52e4 --- /dev/null +++ b/src/unit_tests/models/graph/optimal_linear_arrangement.rs @@ -0,0 +1,169 @@ +use super::*; +use crate::solvers::{BruteForce, Solver}; +use crate::topology::SimpleGraph; +use crate::traits::{OptimizationProblem, Problem}; +use crate::types::{Direction, SolutionSize}; + +fn path_graph_5() -> OptimalLinearArrangement { + // Path: 0-1-2-3-4 + OptimalLinearArrangement::new(SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4)])) +} + +fn triangle_graph() -> OptimalLinearArrangement { + // Triangle: 0-1, 1-2, 0-2 + OptimalLinearArrangement::new(SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)])) +} + +#[test] +fn test_optimal_linear_arrangement_creation() { + let problem = path_graph_5(); + assert_eq!(problem.graph().num_vertices(), 5); + assert_eq!(problem.graph().num_edges(), 4); + assert_eq!(problem.dims().len(), 5); + // Each variable can take values 0..5 + assert_eq!(problem.dims(), vec![5; 5]); +} + +#[test] +fn test_optimal_linear_arrangement_evaluation_valid() { + let problem = triangle_graph(); + // Permutation [0, 1, 2]: cost = |0-1| + |1-2| + |0-2| = 1 + 1 + 2 = 4 + assert_eq!(problem.evaluate(&[0, 1, 2]), SolutionSize::Valid(4)); + // Permutation [1, 0, 2]: cost = |1-0| + |0-2| + |1-2| = 1 + 2 + 1 = 4 + assert_eq!(problem.evaluate(&[1, 0, 2]), SolutionSize::Valid(4)); +} + +#[test] +fn test_optimal_linear_arrangement_evaluation_invalid() { + let problem = triangle_graph(); + // Not a permutation: repeated position + assert_eq!(problem.evaluate(&[0, 0, 1]), SolutionSize::Invalid); + // Out of range + assert_eq!(problem.evaluate(&[0, 1, 5]), SolutionSize::Invalid); + // Wrong length + assert_eq!(problem.evaluate(&[0, 1]), SolutionSize::Invalid); +} + +#[test] +fn test_optimal_linear_arrangement_direction() { + let problem = triangle_graph(); + assert_eq!(problem.direction(), Direction::Minimize); +} + +#[test] +fn test_optimal_linear_arrangement_path_graph() { + // For a path graph with n vertices, the optimal arrangement has cost n-1 + // (just place vertices in path order) + let problem = path_graph_5(); + // Identity permutation: 0->0, 1->1, 2->2, 3->3, 4->4 + // Cost = |0-1| + |1-2| + |2-3| + |3-4| = 1+1+1+1 = 4 + assert_eq!(problem.evaluate(&[0, 1, 2, 3, 4]), SolutionSize::Valid(4)); +} + +#[test] +fn test_optimal_linear_arrangement_solver_triangle() { + let problem = triangle_graph(); + let solver = BruteForce::new(); + let solutions = solver.find_all_best(&problem); + assert!(!solutions.is_empty()); + // All optimal solutions should have cost 4 + for sol in &solutions { + assert_eq!(problem.evaluate(sol), SolutionSize::Valid(4)); + } +} + +#[test] +fn test_optimal_linear_arrangement_solver_path() { + let problem = OptimalLinearArrangement::new(SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)])); + let solver = BruteForce::new(); + let best = solver.find_best(&problem).unwrap(); + // Optimal cost for a path of 4 vertices is 3 + assert_eq!(problem.evaluate(&best), SolutionSize::Valid(3)); +} + +#[test] +fn test_optimal_linear_arrangement_issue_example() { + // Instance 1 from issue: 6 vertices, 7 edges + let problem = OptimalLinearArrangement::new(SimpleGraph::new( + 6, + vec![(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (0, 3), (2, 5)], + )); + // Arrangement f(0)=0, f(1)=1, f(2)=2, f(3)=3, f(4)=4, f(5)=5 (identity, 0-indexed) + // Cost: |0-1|+|1-2|+|2-3|+|3-4|+|4-5|+|0-3|+|2-5| = 1+1+1+1+1+3+3 = 11 + assert_eq!( + problem.evaluate(&[0, 1, 2, 3, 4, 5]), + SolutionSize::Valid(11) + ); +} + +#[test] +fn test_optimal_linear_arrangement_serialization() { + let problem = triangle_graph(); + let json = serde_json::to_value(&problem).unwrap(); + let deserialized: OptimalLinearArrangement = serde_json::from_value(json).unwrap(); + assert_eq!(deserialized.graph().num_vertices(), 3); + assert_eq!(deserialized.graph().num_edges(), 3); +} + +#[test] +fn test_problem_name() { + assert_eq!( + as Problem>::NAME, + "OptimalLinearArrangement" + ); +} + +#[test] +fn test_is_valid_solution() { + let problem = triangle_graph(); + assert!(problem.is_valid_solution(&[0, 1, 2])); + assert!(problem.is_valid_solution(&[2, 0, 1])); + assert!(!problem.is_valid_solution(&[0, 0, 1])); + assert!(!problem.is_valid_solution(&[0, 1])); + assert!(!problem.is_valid_solution(&[0, 1, 3])); +} + +#[test] +fn test_total_edge_length() { + let problem = triangle_graph(); + assert_eq!(problem.total_edge_length(&[0, 1, 2]), Some(4)); + assert_eq!(problem.total_edge_length(&[0, 0, 1]), None); +} + +#[test] +fn test_size_getters() { + let problem = triangle_graph(); + assert_eq!(problem.num_vertices(), 3); + assert_eq!(problem.num_edges(), 3); +} + +#[test] +fn test_new() { + let problem = OptimalLinearArrangement::new(SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)])); + assert_eq!(problem.graph().num_vertices(), 4); + assert_eq!(problem.graph().num_edges(), 3); +} + +#[test] +fn test_single_vertex() { + // Graph with one vertex and no edges + let problem = OptimalLinearArrangement::new(SimpleGraph::new(1, vec![])); + assert_eq!(problem.dims(), vec![1]); + assert_eq!(problem.evaluate(&[0]), SolutionSize::Valid(0)); +} + +#[test] +fn test_complete_graph_k4() { + // K4: all 6 edges present + let problem = OptimalLinearArrangement::new(SimpleGraph::new( + 4, + vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)], + )); + let solver = BruteForce::new(); + let solutions = solver.find_all_best(&problem); + assert!(!solutions.is_empty()); + // For K4, optimal cost = 10 (any linear arrangement of K4 has cost 1+2+3+1+2+1 = 10) + for sol in &solutions { + assert_eq!(problem.evaluate(sol), SolutionSize::Valid(10)); + } +} From 3a4da2606c5d18ba655d49e2f26b5810355dc6dc Mon Sep 17 00:00:00 2001 From: zazabap Date: Thu, 12 Mar 2026 14:42:31 +0000 Subject: [PATCH 3/8] chore: remove plan file after implementation Co-Authored-By: Claude Opus 4.6 --- .../2026-03-12-optimal-linear-arrangement.md | 157 ------------------ 1 file changed, 157 deletions(-) delete mode 100644 docs/plans/2026-03-12-optimal-linear-arrangement.md diff --git a/docs/plans/2026-03-12-optimal-linear-arrangement.md b/docs/plans/2026-03-12-optimal-linear-arrangement.md deleted file mode 100644 index 88d231cc9..000000000 --- a/docs/plans/2026-03-12-optimal-linear-arrangement.md +++ /dev/null @@ -1,157 +0,0 @@ -# Plan: Add OptimalLinearArrangement Model (#406) - -> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. - -## Overview - -Add the Optimal Linear Arrangement (OLA) problem as a graph optimization model. Given a graph G=(V,E), find a bijection f: V → {0,1,...,n-1} minimizing Σ_{(u,v)∈E} |f(u)-f(v)|. - -**Reference:** Garey & Johnson, A1.3 GT42. NP-complete [Garey, Johnson, Stockmeyer 1976]. - -## Design Decisions - -- **Optimization problem**: `Direction::Minimize`, `Metric = SolutionSize` -- **Config space**: n variables, each with domain size n (positions 0..n-1). `dims() = vec![n; n]`. Valid configs are permutations. -- **Type parameter**: Only `G: Graph` (no weight type — objective is purely structural) -- **Complexity**: `2^num_vertices` (Held-Karp-style DP, analogous to TSP) -- **No weight methods**: No `weights()`, `set_weights()`, `is_weighted()` — not applicable - -## Tasks - -### Task 1: Create model file - -**File:** `src/models/graph/optimal_linear_arrangement.rs` - -Follow `src/models/graph/traveling_salesman.rs` as reference. - -#### Struct -```rust -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct OptimalLinearArrangement { - graph: G, -} -``` - -#### Schema registration -```rust -inventory::submit! { - ProblemSchemaEntry { - name: "OptimalLinearArrangement", - module_path: module_path!(), - description: "Find vertex ordering minimizing total edge length (Optimal Linear Arrangement)", - fields: &[ - FieldInfo { name: "graph", type_name: "G", description: "The undirected graph G=(V,E)" }, - ], - } -} -``` - -#### Inherent methods -- `new(graph: G) -> Self` -- `graph(&self) -> &G` -- `num_vertices(&self) -> usize` (under `G: Graph` bound, for overhead expressions) -- `num_edges(&self) -> usize` (under `G: Graph` bound, for overhead expressions) -- `is_valid_permutation(config: &[usize]) -> bool` — checks config has length n and is a permutation of 0..n-1 -- `arrangement_cost(config: &[usize]) -> i64` — computes Σ|config[u] - config[v]| for all edges (u,v) - -#### Problem trait -```rust -impl Problem for OptimalLinearArrangement { - const NAME: &'static str = "OptimalLinearArrangement"; - type Metric = SolutionSize; - fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![G] } - fn dims(&self) -> Vec { vec![self.graph.num_vertices(); self.graph.num_vertices()] } - fn evaluate(&self, config: &[usize]) -> SolutionSize { - if !self.is_valid_permutation(config) { return SolutionSize::Invalid; } - SolutionSize::Valid(self.arrangement_cost(config)) - } -} -``` - -#### OptimizationProblem trait -```rust -impl OptimizationProblem for OptimalLinearArrangement { - type Value = i64; - fn direction(&self) -> Direction { Direction::Minimize } -} -``` - -#### Variant declaration -```rust -crate::declare_variants! { - OptimalLinearArrangement => "2^num_vertices", -} -``` - -#### Test module link -```rust -#[cfg(test)] -#[path = "../../unit_tests/models/graph/optimal_linear_arrangement.rs"] -mod tests; -``` - -### Task 2: Register in module hierarchy - -1. `src/models/graph/mod.rs`: Add `pub(crate) mod optimal_linear_arrangement;` and `pub use optimal_linear_arrangement::OptimalLinearArrangement;` -2. `src/models/mod.rs`: Add `OptimalLinearArrangement` to the `pub use graph::{...}` line - -### Task 3: Register in CLI - -#### dispatch.rs -In `load_problem()` match: -```rust -"OptimalLinearArrangement" => deser_opt::>(data), -``` - -In `serialize_any_problem()` match: -```rust -"OptimalLinearArrangement" => try_ser::>(any), -``` - -Import `OptimalLinearArrangement` from the models at the top. - -#### problem_name.rs -In `resolve_alias()` match add: -```rust -"optimallineararrangement" | "ola" => "OptimalLinearArrangement".to_string(), -``` - -In `ALIASES` const add: -```rust -("OLA", "OptimalLinearArrangement"), -``` - -#### commands/create.rs -Add `"OptimalLinearArrangement"` as a graph-only problem (no weights): -- In the main `match` in `create()`, add a new arm that parses `--graph` and creates `OptimalLinearArrangement::new(graph)` -- Add to `example_for()`: `"OptimalLinearArrangement" => "--graph 0-1,1-2,2-3,0-3"` -- Add to `create_random()` support for random graph generation - -### Task 4: Write unit tests - -**File:** `src/unit_tests/models/graph/optimal_linear_arrangement.rs` - -Follow `src/unit_tests/models/graph/traveling_salesman.rs` as reference. - -Tests to include: -1. `test_optimal_linear_arrangement_creation` — basic construction, dims, num_vertices -2. `test_evaluate_valid_permutation` — identity permutation on path graph 0-1-2-3-4, cost = 4 -3. `test_evaluate_invalid_not_permutation` — duplicate positions → Invalid -4. `test_evaluate_invalid_out_of_range` — position >= n → Invalid -5. `test_evaluate_invalid_wrong_length` — wrong config length → Invalid -6. `test_direction` — Direction::Minimize -7. `test_problem_name` — NAME = "OptimalLinearArrangement" -8. `test_brute_force_path_graph` — path 0-1-2-3-4: optimal cost = 4 (identity arrangement) -9. `test_brute_force_issue_example` — 6-vertex graph from issue with 7 edges: verify optimal cost -10. `test_size_getters` — num_vertices, num_edges -11. `test_is_valid_permutation` — valid/invalid cases directly - -### Task 5: Write paper entry - -Add to `docs/paper/reductions.typ`: -- Add `"OptimalLinearArrangement": [Optimal Linear Arrangement]` to `display-name` dict -- Add `#problem-def("OptimalLinearArrangement")` entry with formal definition, background, example - -### Task 6: Verify - -Run `make check` (fmt + clippy + test). Fix any issues. From 260a3cd44255188107ea6cefb4cabe56cd4ddd70 Mon Sep 17 00:00:00 2001 From: zazabap Date: Thu, 12 Mar 2026 14:48:59 +0000 Subject: [PATCH 4/8] fix: use 0-indexed positions in paper, remove non-standard OLA alias - Paper definition now uses {0, ..., |V|-1} to match code implementation - Removed OLA from ALIASES array (not established enough in literature) - Keep "ola" as lowercase alias in resolve_alias for convenience Co-Authored-By: Claude Opus 4.6 --- docs/paper/reductions.typ | 4 ++-- problemreductions-cli/src/problem_name.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index 1e3cb4b4e..34b05f6f8 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -458,13 +458,13 @@ caption: [Complete graph $K_4$ with weighted edges. The optimal tour $v_0 -> v_1 ) ] #problem-def("OptimalLinearArrangement")[ - Given an undirected graph $G=(V,E)$, find a bijection $f: V -> {1, 2, dots, |V|}$ that minimizes $sum_({u,v} in E) |f(u) - f(v)|$. + Given an undirected graph $G=(V,E)$, find a bijection $f: V -> {0, 1, dots, |V|-1}$ that minimizes $sum_({u,v} in E) |f(u) - f(v)|$. ][ A classical NP-complete problem from Garey & Johnson (GT42) @garey1979, with applications in VLSI design, graph drawing, and sparse matrix reordering. The problem asks to place vertices on a line so that the total "stretch" of all edges is minimized. 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+1$ gives cost $|1-2| + |2-3| = 2$, which is optimal. For a triangle $K_3$ with the same vertex set plus edge ${v_0, v_2}$, any arrangement yields cost $|f(v_0)-f(v_1)| + |f(v_1)-f(v_2)| + |f(v_0)-f(v_2)| = 4$. +*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$, which is optimal. For a triangle $K_3$ with the same vertex set plus edge ${v_0, v_2}$, any arrangement yields cost $|f(v_0)-f(v_1)| + |f(v_1)-f(v_2)| + |f(v_0)-f(v_2)| = 4$. ] #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/problemreductions-cli/src/problem_name.rs b/problemreductions-cli/src/problem_name.rs index 9b3889a6d..c0269b774 100644 --- a/problemreductions-cli/src/problem_name.rs +++ b/problemreductions-cli/src/problem_name.rs @@ -52,7 +52,7 @@ pub fn resolve_alias(input: &str) -> String { "binpacking" => "BinPacking".to_string(), "cvp" | "closestvectorproblem" => "ClosestVectorProblem".to_string(), "knapsack" => "Knapsack".to_string(), - "optimallineararrangement" => "OptimalLinearArrangement".to_string(), + "optimallineararrangement" | "ola" => "OptimalLinearArrangement".to_string(), _ => input.to_string(), // pass-through for exact names } } From 12eb3ae25f8617b46d488c9d88311740552ba4c5 Mon Sep 17 00:00:00 2001 From: zazabap Date: Thu, 12 Mar 2026 14:53:14 +0000 Subject: [PATCH 5/8] fix: add OLA to ALIASES const for shell completion Co-Authored-By: Claude Opus 4.6 --- problemreductions-cli/src/problem_name.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/problemreductions-cli/src/problem_name.rs b/problemreductions-cli/src/problem_name.rs index c0269b774..cc25225a6 100644 --- a/problemreductions-cli/src/problem_name.rs +++ b/problemreductions-cli/src/problem_name.rs @@ -21,6 +21,7 @@ pub const ALIASES: &[(&str, &str)] = &[ ("TSP", "TravelingSalesman"), ("CVP", "ClosestVectorProblem"), ("MaxMatching", "MaximumMatching"), + ("OLA", "OptimalLinearArrangement"), ]; /// Resolve a short alias to the canonical problem name. From ab59d43deecb1d6657227c31a2bfb89f932f473a Mon Sep 17 00:00:00 2001 From: zazabap Date: Thu, 12 Mar 2026 15:35:51 +0000 Subject: [PATCH 6/8] fix: convert OptimalLinearArrangement to SatisfactionProblem with bound K The issue specifies this as a decision problem (Garey & Johnson GT42): given graph G and bound K, is there a permutation f with total edge length <= K? This converts from OptimizationProblem to SatisfactionProblem with Metric = bool, adding the `bound` field as specified. Co-Authored-By: Claude Opus 4.6 --- docs/paper/reductions.typ | 6 +- docs/src/reductions/problem_schemas.json | 9 +- problemreductions-cli/src/dispatch.rs | 2 +- .../graph/optimal_linear_arrangement.rs | 79 ++--- .../graph/optimal_linear_arrangement.rs | 294 +++++++++++------- 5 files changed, 240 insertions(+), 150 deletions(-) diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index 34b05f6f8..0ebfe43ca 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -458,13 +458,13 @@ caption: [Complete graph $K_4$ with weighted edges. The optimal tour $v_0 -> v_1 ) ] #problem-def("OptimalLinearArrangement")[ - Given an undirected graph $G=(V,E)$, find a bijection $f: V -> {0, 1, dots, |V|-1}$ that minimizes $sum_({u,v} in E) |f(u) - f(v)|$. + 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 problem from Garey & Johnson (GT42) @garey1979, with applications in VLSI design, graph drawing, and sparse matrix reordering. The problem asks to place vertices on a line so that the total "stretch" of all edges is minimized. +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$, which is optimal. For a triangle $K_3$ with the same vertex set plus edge ${v_0, v_2}$, any arrangement yields cost $|f(v_0)-f(v_1)| + |f(v_1)-f(v_2)| + |f(v_0)-f(v_2)| = 4$. +*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/src/reductions/problem_schemas.json b/docs/src/reductions/problem_schemas.json index 3db9a6859..31ff69411 100644 --- a/docs/src/reductions/problem_schemas.json +++ b/docs/src/reductions/problem_schemas.json @@ -335,12 +335,17 @@ }, { "name": "OptimalLinearArrangement", - "description": "Find vertex ordering on a line minimizing total edge length", + "description": "Find a vertex ordering on a line with total edge length at most K", "fields": [ { "name": "graph", "type_name": "G", - "description": "The underlying graph G=(V,E)" + "description": "The undirected graph G=(V,E)" + }, + { + "name": "bound", + "type_name": "usize", + "description": "Upper bound K on total edge length" } ] }, diff --git a/problemreductions-cli/src/dispatch.rs b/problemreductions-cli/src/dispatch.rs index f499be61f..be6bac5c8 100644 --- a/problemreductions-cli/src/dispatch.rs +++ b/problemreductions-cli/src/dispatch.rs @@ -245,7 +245,7 @@ pub fn load_problem( _ => deser_opt::>(data), }, "Knapsack" => deser_opt::(data), - "OptimalLinearArrangement" => deser_opt::>(data), + "OptimalLinearArrangement" => deser_sat::>(data), _ => bail!("{}", crate::problem_name::unknown_problem_error(&canonical)), } } diff --git a/src/models/graph/optimal_linear_arrangement.rs b/src/models/graph/optimal_linear_arrangement.rs index 8b027ca47..bc4cd341b 100644 --- a/src/models/graph/optimal_linear_arrangement.rs +++ b/src/models/graph/optimal_linear_arrangement.rs @@ -1,35 +1,40 @@ //! Optimal Linear Arrangement problem implementation. //! -//! The Optimal Linear Arrangement problem asks for a permutation of vertices -//! on a line that minimizes the total edge length (sum of |f(u) - f(v)| for all edges). +//! 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::{OptimizationProblem, Problem}; -use crate::types::{Direction, SolutionSize}; +use crate::traits::{Problem, SatisfactionProblem}; use serde::{Deserialize, Serialize}; inventory::submit! { ProblemSchemaEntry { name: "OptimalLinearArrangement", module_path: module_path!(), - description: "Find vertex ordering on a line minimizing total edge length", + description: "Find a vertex ordering on a line with total edge length at most K", fields: &[ - FieldInfo { name: "graph", type_name: "G", description: "The underlying graph G=(V,E)" }, + 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 a graph G = (V, E), find a bijection f: V -> {0, 1, ..., |V|-1} -/// that minimizes the total edge length: sum_{(u,v) in E} |f(u) - f(v)|. +/// 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). +/// a permutation (all positions are distinct) with total edge length at most K. /// /// # Type Parameters /// @@ -42,25 +47,31 @@ inventory::submit! { /// use problemreductions::topology::SimpleGraph; /// use problemreductions::{Problem, Solver, BruteForce}; /// -/// // Path graph: 0-1-2 -/// let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); -/// let problem = OptimalLinearArrangement::new(graph); +/// // 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 best = solver.find_best(&problem).unwrap(); -/// // Optimal: identity arrangement, cost = 2 -/// assert_eq!(problem.evaluate(&best).unwrap(), 2); +/// 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 an Optimal Linear Arrangement problem from a graph. - pub fn new(graph: G) -> Self { - Self { graph } + /// 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. @@ -68,6 +79,11 @@ impl OptimalLinearArrangement { &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() @@ -78,9 +94,12 @@ impl OptimalLinearArrangement { self.graph.num_edges() } - /// Check if a configuration is a valid permutation. + /// Check if a configuration is a valid permutation with total edge length at most K. pub fn is_valid_solution(&self, config: &[usize]) -> bool { - self.is_valid_permutation(config) + 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}. @@ -121,7 +140,7 @@ where G: Graph + crate::variant::VariantParam, { const NAME: &'static str = "OptimalLinearArrangement"; - type Metric = SolutionSize; + type Metric = bool; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![G] @@ -132,24 +151,12 @@ where vec![n; n] } - fn evaluate(&self, config: &[usize]) -> SolutionSize { - match self.total_edge_length(config) { - Some(cost) => SolutionSize::Valid(cost), - None => SolutionSize::Invalid, - } + fn evaluate(&self, config: &[usize]) -> bool { + self.is_valid_solution(config) } } -impl OptimizationProblem for OptimalLinearArrangement -where - G: Graph + crate::variant::VariantParam, -{ - type Value = usize; - - fn direction(&self) -> Direction { - Direction::Minimize - } -} +impl SatisfactionProblem for OptimalLinearArrangement {} crate::declare_variants! { OptimalLinearArrangement => "2^num_vertices", diff --git a/src/unit_tests/models/graph/optimal_linear_arrangement.rs b/src/unit_tests/models/graph/optimal_linear_arrangement.rs index 3142f52e4..b0d16d86d 100644 --- a/src/unit_tests/models/graph/optimal_linear_arrangement.rs +++ b/src/unit_tests/models/graph/optimal_linear_arrangement.rs @@ -1,112 +1,177 @@ use super::*; use crate::solvers::{BruteForce, Solver}; use crate::topology::SimpleGraph; -use crate::traits::{OptimizationProblem, Problem}; -use crate::types::{Direction, SolutionSize}; +use crate::traits::Problem; -fn path_graph_5() -> OptimalLinearArrangement { - // Path: 0-1-2-3-4 - OptimalLinearArrangement::new(SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4)])) +/// 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) } -fn triangle_graph() -> OptimalLinearArrangement { - // Triangle: 0-1, 1-2, 0-2 - OptimalLinearArrangement::new(SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)])) +/// 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_optimal_linear_arrangement_creation() { - let problem = path_graph_5(); - assert_eq!(problem.graph().num_vertices(), 5); - assert_eq!(problem.graph().num_edges(), 4); - assert_eq!(problem.dims().len(), 5); - // Each variable can take values 0..5 - assert_eq!(problem.dims(), vec![5; 5]); +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)); } #[test] -fn test_optimal_linear_arrangement_evaluation_valid() { - let problem = triangle_graph(); - // Permutation [0, 1, 2]: cost = |0-1| + |1-2| + |0-2| = 1 + 1 + 2 = 4 - assert_eq!(problem.evaluate(&[0, 1, 2]), SolutionSize::Valid(4)); - // Permutation [1, 0, 2]: cost = |1-0| + |0-2| + |1-2| = 1 + 2 + 1 = 4 - assert_eq!(problem.evaluate(&[1, 0, 2]), SolutionSize::Valid(4)); +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_optimal_linear_arrangement_evaluation_invalid() { - let problem = triangle_graph(); - // Not a permutation: repeated position - assert_eq!(problem.evaluate(&[0, 0, 1]), SolutionSize::Invalid); +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_eq!(problem.evaluate(&[0, 1, 5]), SolutionSize::Invalid); + 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_eq!(problem.evaluate(&[0, 1]), SolutionSize::Invalid); + assert!(!problem.evaluate(&[0, 1, 2])); + assert_eq!(problem.total_edge_length(&[0, 1, 2]), None); } #[test] -fn test_optimal_linear_arrangement_direction() { - let problem = triangle_graph(); - assert_eq!(problem.direction(), Direction::Minimize); +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_optimal_linear_arrangement_path_graph() { - // For a path graph with n vertices, the optimal arrangement has cost n-1 - // (just place vertices in path order) - let problem = path_graph_5(); - // Identity permutation: 0->0, 1->1, 2->2, 3->3, 4->4 - // Cost = |0-1| + |1-2| + |2-3| + |3-4| = 1+1+1+1 = 4 - assert_eq!(problem.evaluate(&[0, 1, 2, 3, 4]), SolutionSize::Valid(4)); +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_optimal_linear_arrangement_solver_triangle() { - let problem = triangle_graph(); +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 solutions = solver.find_all_best(&problem); - assert!(!solutions.is_empty()); - // All optimal solutions should have cost 4 - for sol in &solutions { - assert_eq!(problem.evaluate(sol), SolutionSize::Valid(4)); - } + 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_optimal_linear_arrangement_solver_path() { - let problem = OptimalLinearArrangement::new(SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)])); +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 best = solver.find_best(&problem).unwrap(); - // Optimal cost for a path of 4 vertices is 3 - assert_eq!(problem.evaluate(&best), SolutionSize::Valid(3)); + 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_optimal_linear_arrangement_issue_example() { - // Instance 1 from issue: 6 vertices, 7 edges - let problem = OptimalLinearArrangement::new(SimpleGraph::new( - 6, - vec![(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (0, 3), (2, 5)], - )); - // Arrangement f(0)=0, f(1)=1, f(2)=2, f(3)=3, f(4)=4, f(5)=5 (identity, 0-indexed) - // Cost: |0-1|+|1-2|+|2-3|+|3-4|+|4-5|+|0-3|+|2-5| = 1+1+1+1+1+3+3 = 11 - assert_eq!( - problem.evaluate(&[0, 1, 2, 3, 4, 5]), - SolutionSize::Valid(11) - ); +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_optimal_linear_arrangement_serialization() { - let problem = triangle_graph(); - let json = serde_json::to_value(&problem).unwrap(); - let deserialized: OptimalLinearArrangement = serde_json::from_value(json).unwrap(); - assert_eq!(deserialized.graph().num_vertices(), 3); - assert_eq!(deserialized.graph().num_edges(), 3); +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_problem_name() { +fn test_optimallineararrangement_problem_name() { assert_eq!( as Problem>::NAME, "OptimalLinearArrangement" @@ -114,56 +179,69 @@ fn test_problem_name() { } #[test] -fn test_is_valid_solution() { - let problem = triangle_graph(); - assert!(problem.is_valid_solution(&[0, 1, 2])); - assert!(problem.is_valid_solution(&[2, 0, 1])); - assert!(!problem.is_valid_solution(&[0, 0, 1])); - assert!(!problem.is_valid_solution(&[0, 1])); - assert!(!problem.is_valid_solution(&[0, 1, 3])); -} +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); -#[test] -fn test_total_edge_length() { - let problem = triangle_graph(); - assert_eq!(problem.total_edge_length(&[0, 1, 2]), Some(4)); - assert_eq!(problem.total_edge_length(&[0, 0, 1]), None); + // 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_size_getters() { - let problem = triangle_graph(); - assert_eq!(problem.num_vertices(), 3); - assert_eq!(problem.num_edges(), 3); -} +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); -#[test] -fn test_new() { - let problem = OptimalLinearArrangement::new(SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)])); - assert_eq!(problem.graph().num_vertices(), 4); - assert_eq!(problem.graph().num_edges(), 3); + // 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_single_vertex() { - // Graph with one vertex and no edges - let problem = OptimalLinearArrangement::new(SimpleGraph::new(1, vec![])); - assert_eq!(problem.dims(), vec![1]); - assert_eq!(problem.evaluate(&[0]), SolutionSize::Valid(0)); +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_complete_graph_k4() { - // K4: all 6 edges present - let problem = OptimalLinearArrangement::new(SimpleGraph::new( - 4, - vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)], - )); +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 solutions = solver.find_all_best(&problem); - assert!(!solutions.is_empty()); - // For K4, optimal cost = 10 (any linear arrangement of K4 has cost 1+2+3+1+2+1 = 10) - for sol in &solutions { - assert_eq!(problem.evaluate(sol), SolutionSize::Valid(10)); + 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)); } } From cd167f72a9f0006fb5a1ba95d1544ff2132c0396 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Sat, 14 Mar 2026 09:12:52 +0800 Subject: [PATCH 7/8] =?UTF-8?q?fix:=20address=20Copilot=20review=20?= =?UTF-8?q?=E2=80=94=20add=20missing=20bound=20param=20to=20OLA=20CLI=20in?= =?UTF-8?q?tegration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CLI create/random now requires --bound for OptimalLinearArrangement - Updated help text and examples to include --bound - Fixed module doc to reflect decision formulation (at most K) Co-Authored-By: Claude Opus 4.6 --- problemreductions-cli/src/cli.rs | 2 +- problemreductions-cli/src/commands/create.rs | 21 ++++++++++++++------ src/models/graph/mod.rs | 2 +- 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/problemreductions-cli/src/cli.rs b/problemreductions-cli/src/cli.rs index 9372fad0a..3a18e8a60 100644 --- a/problemreductions-cli/src/cli.rs +++ b/problemreductions-cli/src/cli.rs @@ -220,7 +220,7 @@ Flags by problem type: BicliqueCover --left, --right, --biedges, --k BMF --matrix (0/1), --rank CVP --basis, --target-vec [--bounds] - OptimalLinearArrangement --graph + 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 079f10be1..b82677200 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -108,7 +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", + "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" @@ -610,15 +610,21 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { ) } - // OptimalLinearArrangement — graph only, no weights + // 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" + "{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))?, + ser(OptimalLinearArrangement::new(graph, bound))?, resolved_variant.clone(), ) } @@ -1435,15 +1441,18 @@ fn create_random( util::ser_kcoloring(graph, k)? } - // OptimalLinearArrangement — graph only, no weights + // 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: sum of all possible edge lengths (n*(n-1)/2) ensures satisfiability + let n = graph.num_vertices(); + let bound = args.bound.map(|b| b as usize).unwrap_or(n * (n - 1) / 2); let variant = variant_map(&[("graph", "SimpleGraph")]); - (ser(OptimalLinearArrangement::new(graph))?, variant) + (ser(OptimalLinearArrangement::new(graph, bound))?, variant) } _ => bail!( diff --git a/src/models/graph/mod.rs b/src/models/graph/mod.rs index 72dca4b3e..d63802b71 100644 --- a/src/models/graph/mod.rs +++ b/src/models/graph/mod.rs @@ -17,7 +17,7 @@ //! - [`SpinGlass`]: Ising model Hamiltonian //! - [`HamiltonianPath`]: Hamiltonian path (simple path visiting every vertex) //! - [`BicliqueCover`]: Biclique cover on bipartite graphs -//! - [`OptimalLinearArrangement`]: Optimal linear arrangement (minimum total edge length) +//! - [`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) From 4fae43ceb0d02e1f1b0b871a64f57a373738effc Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Sat, 14 Mar 2026 09:22:09 +0800 Subject: [PATCH 8/8] fix: add trait_consistency entry, strengthen tests, fix random bound - Add OptimalLinearArrangement to trait_consistency checks - Strengthen NO instance test with brute-force verification - Fix random generation default bound formula (n-1)*num_edges Co-Authored-By: Claude Opus 4.6 --- problemreductions-cli/src/commands/create.rs | 7 +++++-- src/unit_tests/models/graph/optimal_linear_arrangement.rs | 4 ++++ src/unit_tests/trait_consistency.rs | 4 ++++ 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index b82677200..97035d180 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -1448,9 +1448,12 @@ fn create_random( 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: sum of all possible edge lengths (n*(n-1)/2) ensures satisfiability + // 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 * (n - 1) / 2); + 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) } diff --git a/src/unit_tests/models/graph/optimal_linear_arrangement.rs b/src/unit_tests/models/graph/optimal_linear_arrangement.rs index b0d16d86d..c8371791a 100644 --- a/src/unit_tests/models/graph/optimal_linear_arrangement.rs +++ b/src/unit_tests/models/graph/optimal_linear_arrangement.rs @@ -49,6 +49,10 @@ fn test_optimallineararrangement_no_instance() { 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] 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)]),