Skip to content

Commit e73796d

Browse files
zazabapclaudeGiggleLiu
authored
Fix #406: Add OptimalLinearArrangement satisfaction problem (#616)
* Add plan for #406: OptimalLinearArrangement Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Implement #406: Add OptimalLinearArrangement model - Add OptimalLinearArrangement<G> 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 <noreply@anthropic.com> * chore: remove plan file after implementation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * fix: add OLA to ALIASES const for shell completion Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * fix: address Copilot review — add missing bound param to OLA CLI integration - 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: GiggleLiu <cacate0129@gmail.com>
1 parent cd96c8a commit e73796d

13 files changed

Lines changed: 508 additions & 5 deletions

File tree

docs/paper/reductions.typ

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
"BicliqueCover": [Biclique Cover],
5656
"BinPacking": [Bin Packing],
5757
"ClosestVectorProblem": [Closest Vector Problem],
58+
"OptimalLinearArrangement": [Optimal Linear Arrangement],
5859
"RuralPostman": [Rural Postman],
5960
"LongestCommonSubsequence": [Longest Common Subsequence],
6061
"SubsetSum": [Subset Sum],
@@ -63,7 +64,6 @@
6364
"ShortestCommonSupersequence": [Shortest Common Supersequence],
6465
"MinimumSumMulticenter": [Minimum Sum Multicenter],
6566
"SubgraphIsomorphism": [Subgraph Isomorphism],
66-
"SubsetSum": [Subset Sum],
6767
"FlowShopScheduling": [Flow Shop Scheduling],
6868
)
6969

@@ -578,6 +578,15 @@ One of the most intensely studied NP-hard problems, with applications in logisti
578578
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.],
579579
) <fig:k4-tsp>
580580
]
581+
#problem-def("OptimalLinearArrangement")[
582+
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$?
583+
][
584+
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$.
585+
586+
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.
587+
588+
*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.
589+
]
581590
#problem-def("MaximumClique")[
582591
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)$.
583592
][

docs/paper/references.bib

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,16 @@ @book{garey1979
2222
year = {1979}
2323
}
2424

25+
@article{gareyJohnsonStockmeyer1976,
26+
author = {Michael R. Garey and David S. Johnson and Larry Stockmeyer},
27+
title = {Some Simplified {NP}-Complete Graph Problems},
28+
journal = {Theoretical Computer Science},
29+
volume = {1},
30+
number = {3},
31+
pages = {237--267},
32+
year = {1976}
33+
}
34+
2535
@article{glover2019,
2636
author = {Fred Glover and Gary Kochenberger and Yu Du},
2737
title = {Quantum Bridge Analytics {I}: a tutorial on formulating and using {QUBO} models},

docs/src/reductions/problem_schemas.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,22 @@
424424
}
425425
]
426426
},
427+
{
428+
"name": "OptimalLinearArrangement",
429+
"description": "Find a vertex ordering on a line with total edge length at most K",
430+
"fields": [
431+
{
432+
"name": "graph",
433+
"type_name": "G",
434+
"description": "The undirected graph G=(V,E)"
435+
},
436+
{
437+
"name": "bound",
438+
"type_name": "usize",
439+
"description": "Upper bound K on total edge length"
440+
}
441+
]
442+
},
427443
{
428444
"name": "PaintShop",
429445
"description": "Minimize color changes in paint shop sequence",

problemreductions-cli/src/cli.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,7 @@ Flags by problem type:
220220
BicliqueCover --left, --right, --biedges, --k
221221
BMF --matrix (0/1), --rank
222222
CVP --basis, --target-vec [--bounds]
223+
OptimalLinearArrangement --graph, --bound
223224
RuralPostman (RPP) --graph, --edge-weights, --required-edges, --bound
224225
SubgraphIsomorphism --graph (host), --pattern (pattern)
225226
LCS --strings

problemreductions-cli/src/commands/create.rs

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str {
108108
}
109109
"PartitionIntoTriangles" => "--graph 0-1,1-2,0-2",
110110
"Factoring" => "--target 15 --m 4 --n 4",
111+
"OptimalLinearArrangement" => "--graph 0-1,1-2,2-3 --bound 5",
111112
"MinimumFeedbackArcSet" => "--arcs \"0>1,1>2,2>0\"",
112113
"RuralPostman" => {
113114
"--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<()> {
609610
)
610611
}
611612

613+
// OptimalLinearArrangement — graph + bound
614+
"OptimalLinearArrangement" => {
615+
let (graph, _) = parse_graph(args).map_err(|e| {
616+
anyhow::anyhow!(
617+
"{e}\n\nUsage: pred create OptimalLinearArrangement --graph 0-1,1-2,2-3 --bound 5"
618+
)
619+
})?;
620+
let bound = args.bound.ok_or_else(|| {
621+
anyhow::anyhow!(
622+
"OptimalLinearArrangement requires --bound (upper bound K on total edge length)\n\n\
623+
Usage: pred create OptimalLinearArrangement --graph 0-1,1-2,2-3 --bound 5"
624+
)
625+
})? as usize;
626+
(
627+
ser(OptimalLinearArrangement::new(graph, bound))?,
628+
resolved_variant.clone(),
629+
)
630+
}
631+
612632
// FlowShopScheduling
613633
"FlowShopScheduling" => {
614634
let task_str = args.task_lengths.as_deref().ok_or_else(|| {
@@ -1421,11 +1441,28 @@ fn create_random(
14211441
util::ser_kcoloring(graph, k)?
14221442
}
14231443

1444+
// OptimalLinearArrangement — graph + bound
1445+
"OptimalLinearArrangement" => {
1446+
let edge_prob = args.edge_prob.unwrap_or(0.5);
1447+
if !(0.0..=1.0).contains(&edge_prob) {
1448+
bail!("--edge-prob must be between 0.0 and 1.0");
1449+
}
1450+
let graph = util::create_random_graph(num_vertices, edge_prob, args.seed);
1451+
// Default bound: (n-1) * num_edges ensures satisfiability (max edge stretch is n-1)
1452+
let n = graph.num_vertices();
1453+
let bound = args
1454+
.bound
1455+
.map(|b| b as usize)
1456+
.unwrap_or((n.saturating_sub(1)) * graph.num_edges());
1457+
let variant = variant_map(&[("graph", "SimpleGraph")]);
1458+
(ser(OptimalLinearArrangement::new(graph, bound))?, variant)
1459+
}
1460+
14241461
_ => bail!(
14251462
"Random generation is not supported for {canonical}. \
14261463
Supported: graph-based problems (MIS, MVC, MaxCut, MaxClique, \
14271464
MaximumMatching, MinimumDominatingSet, SpinGlass, KColoring, TravelingSalesman, \
1428-
HamiltonianPath)"
1465+
OptimalLinearArrangement, HamiltonianPath)"
14291466
),
14301467
};
14311468

problemreductions-cli/src/dispatch.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,7 @@ pub fn load_problem(
255255
_ => deser_opt::<ClosestVectorProblem<i32>>(data),
256256
},
257257
"Knapsack" => deser_opt::<Knapsack>(data),
258+
"OptimalLinearArrangement" => deser_sat::<OptimalLinearArrangement<SimpleGraph>>(data),
258259
"SubgraphIsomorphism" => deser_sat::<SubgraphIsomorphism>(data),
259260
"PartitionIntoTriangles" => deser_sat::<PartitionIntoTriangles<SimpleGraph>>(data),
260261
"LongestCommonSubsequence" => deser_opt::<LongestCommonSubsequence>(data),
@@ -330,6 +331,7 @@ pub fn serialize_any_problem(
330331
_ => try_ser::<ClosestVectorProblem<i32>>(any),
331332
},
332333
"Knapsack" => try_ser::<Knapsack>(any),
334+
"OptimalLinearArrangement" => try_ser::<OptimalLinearArrangement<SimpleGraph>>(any),
333335
"SubgraphIsomorphism" => try_ser::<SubgraphIsomorphism>(any),
334336
"PartitionIntoTriangles" => try_ser::<PartitionIntoTriangles<SimpleGraph>>(any),
335337
"LongestCommonSubsequence" => try_ser::<LongestCommonSubsequence>(any),

problemreductions-cli/src/problem_name.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ pub const ALIASES: &[(&str, &str)] = &[
2323
("RPP", "RuralPostman"),
2424
("LCS", "LongestCommonSubsequence"),
2525
("MaxMatching", "MaximumMatching"),
26+
("OLA", "OptimalLinearArrangement"),
2627
("FVS", "MinimumFeedbackVertexSet"),
2728
("SCS", "ShortestCommonSupersequence"),
2829
("FAS", "MinimumFeedbackArcSet"),
@@ -61,6 +62,7 @@ pub fn resolve_alias(input: &str) -> String {
6162
"binpacking" => "BinPacking".to_string(),
6263
"cvp" | "closestvectorproblem" => "ClosestVectorProblem".to_string(),
6364
"knapsack" => "Knapsack".to_string(),
65+
"optimallineararrangement" | "ola" => "OptimalLinearArrangement".to_string(),
6466
"subgraphisomorphism" => "SubgraphIsomorphism".to_string(),
6567
"partitionintotriangles" => "PartitionIntoTriangles".to_string(),
6668
"lcs" | "longestcommonsubsequence" => "LongestCommonSubsequence".to_string(),

src/lib.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,8 @@ pub mod prelude {
4747
pub use crate::models::graph::{
4848
KColoring, MaxCut, MaximalIS, MaximumClique, MaximumIndependentSet, MaximumMatching,
4949
MinimumDominatingSet, MinimumFeedbackArcSet, MinimumFeedbackVertexSet,
50-
MinimumSumMulticenter, MinimumVertexCover, PartitionIntoTriangles, RuralPostman,
51-
TravelingSalesman,
50+
MinimumSumMulticenter, MinimumVertexCover, OptimalLinearArrangement,
51+
PartitionIntoTriangles, RuralPostman, TravelingSalesman,
5252
};
5353
pub use crate::models::misc::{
5454
BinPacking, Factoring, FlowShopScheduling, Knapsack, LongestCommonSubsequence, PaintShop,

src/models/graph/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
//! - [`SpinGlass`]: Ising model Hamiltonian
1818
//! - [`HamiltonianPath`]: Hamiltonian path (simple path visiting every vertex)
1919
//! - [`BicliqueCover`]: Biclique cover on bipartite graphs
20+
//! - [`OptimalLinearArrangement`]: Optimal linear arrangement (total edge length at most K)
2021
//! - [`MinimumFeedbackArcSet`]: Minimum feedback arc set on directed graphs
2122
//! - [`MinimumSumMulticenter`]: Min-sum multicenter (p-median)
2223
//! - [`RuralPostman`]: Rural Postman (circuit covering required edges)
@@ -37,6 +38,7 @@ pub(crate) mod minimum_feedback_arc_set;
3738
pub(crate) mod minimum_feedback_vertex_set;
3839
pub(crate) mod minimum_sum_multicenter;
3940
pub(crate) mod minimum_vertex_cover;
41+
pub(crate) mod optimal_linear_arrangement;
4042
pub(crate) mod partition_into_triangles;
4143
pub(crate) mod rural_postman;
4244
pub(crate) mod spin_glass;
@@ -58,6 +60,7 @@ pub use minimum_feedback_arc_set::MinimumFeedbackArcSet;
5860
pub use minimum_feedback_vertex_set::MinimumFeedbackVertexSet;
5961
pub use minimum_sum_multicenter::MinimumSumMulticenter;
6062
pub use minimum_vertex_cover::MinimumVertexCover;
63+
pub use optimal_linear_arrangement::OptimalLinearArrangement;
6164
pub use partition_into_triangles::PartitionIntoTriangles;
6265
pub use rural_postman::RuralPostman;
6366
pub use spin_glass::SpinGlass;
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
//! Optimal Linear Arrangement problem implementation.
2+
//!
3+
//! The Optimal Linear Arrangement problem asks whether there exists a one-to-one
4+
//! function f: V -> {0, 1, ..., |V|-1} such that the total edge length
5+
//! sum_{{u,v} in E} |f(u) - f(v)| is at most K.
6+
7+
use crate::registry::{FieldInfo, ProblemSchemaEntry};
8+
use crate::topology::{Graph, SimpleGraph};
9+
use crate::traits::{Problem, SatisfactionProblem};
10+
use serde::{Deserialize, Serialize};
11+
12+
inventory::submit! {
13+
ProblemSchemaEntry {
14+
name: "OptimalLinearArrangement",
15+
module_path: module_path!(),
16+
description: "Find a vertex ordering on a line with total edge length at most K",
17+
fields: &[
18+
FieldInfo { name: "graph", type_name: "G", description: "The undirected graph G=(V,E)" },
19+
FieldInfo { name: "bound", type_name: "usize", description: "Upper bound K on total edge length" },
20+
],
21+
}
22+
}
23+
24+
/// The Optimal Linear Arrangement problem.
25+
///
26+
/// Given an undirected graph G = (V, E) and a non-negative integer K,
27+
/// determine whether there exists a one-to-one function f: V -> {0, 1, ..., |V|-1}
28+
/// such that sum_{{u,v} in E} |f(u) - f(v)| <= K.
29+
///
30+
/// This is the decision (satisfaction) version of the problem, following the
31+
/// Garey & Johnson formulation (GT42).
32+
///
33+
/// # Representation
34+
///
35+
/// Each vertex is assigned a variable representing its position in the arrangement.
36+
/// Variable i takes a value in {0, 1, ..., n-1}, and a valid configuration must be
37+
/// a permutation (all positions are distinct) with total edge length at most K.
38+
///
39+
/// # Type Parameters
40+
///
41+
/// * `G` - The graph type (e.g., `SimpleGraph`)
42+
///
43+
/// # Example
44+
///
45+
/// ```
46+
/// use problemreductions::models::graph::OptimalLinearArrangement;
47+
/// use problemreductions::topology::SimpleGraph;
48+
/// use problemreductions::{Problem, Solver, BruteForce};
49+
///
50+
/// // Path graph: 0-1-2-3 with bound 3
51+
/// let graph = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]);
52+
/// let problem = OptimalLinearArrangement::new(graph, 3);
53+
///
54+
/// let solver = BruteForce::new();
55+
/// let solution = solver.find_satisfying(&problem);
56+
/// assert!(solution.is_some());
57+
/// ```
58+
#[derive(Debug, Clone, Serialize, Deserialize)]
59+
#[serde(bound(deserialize = "G: serde::Deserialize<'de>"))]
60+
pub struct OptimalLinearArrangement<G> {
61+
/// The underlying graph.
62+
graph: G,
63+
/// Upper bound K on total edge length.
64+
bound: usize,
65+
}
66+
67+
impl<G: Graph> OptimalLinearArrangement<G> {
68+
/// Create a new Optimal Linear Arrangement problem.
69+
///
70+
/// # Arguments
71+
/// * `graph` - The undirected graph G = (V, E)
72+
/// * `bound` - The upper bound K on total edge length
73+
pub fn new(graph: G, bound: usize) -> Self {
74+
Self { graph, bound }
75+
}
76+
77+
/// Get a reference to the underlying graph.
78+
pub fn graph(&self) -> &G {
79+
&self.graph
80+
}
81+
82+
/// Get the bound K.
83+
pub fn bound(&self) -> usize {
84+
self.bound
85+
}
86+
87+
/// Get the number of vertices in the underlying graph.
88+
pub fn num_vertices(&self) -> usize {
89+
self.graph.num_vertices()
90+
}
91+
92+
/// Get the number of edges in the underlying graph.
93+
pub fn num_edges(&self) -> usize {
94+
self.graph.num_edges()
95+
}
96+
97+
/// Check if a configuration is a valid permutation with total edge length at most K.
98+
pub fn is_valid_solution(&self, config: &[usize]) -> bool {
99+
match self.total_edge_length(config) {
100+
Some(length) => length <= self.bound,
101+
None => false,
102+
}
103+
}
104+
105+
/// Check if a configuration forms a valid permutation of {0, ..., n-1}.
106+
fn is_valid_permutation(&self, config: &[usize]) -> bool {
107+
let n = self.graph.num_vertices();
108+
if config.len() != n {
109+
return false;
110+
}
111+
let mut seen = vec![false; n];
112+
for &pos in config {
113+
if pos >= n || seen[pos] {
114+
return false;
115+
}
116+
seen[pos] = true;
117+
}
118+
true
119+
}
120+
121+
/// Compute the total edge length for a given arrangement.
122+
///
123+
/// Returns `None` if the configuration is not a valid permutation.
124+
pub fn total_edge_length(&self, config: &[usize]) -> Option<usize> {
125+
if !self.is_valid_permutation(config) {
126+
return None;
127+
}
128+
let mut total = 0usize;
129+
for (u, v) in self.graph.edges() {
130+
let fu = config[u];
131+
let fv = config[v];
132+
total += fu.abs_diff(fv);
133+
}
134+
Some(total)
135+
}
136+
}
137+
138+
impl<G> Problem for OptimalLinearArrangement<G>
139+
where
140+
G: Graph + crate::variant::VariantParam,
141+
{
142+
const NAME: &'static str = "OptimalLinearArrangement";
143+
type Metric = bool;
144+
145+
fn variant() -> Vec<(&'static str, &'static str)> {
146+
crate::variant_params![G]
147+
}
148+
149+
fn dims(&self) -> Vec<usize> {
150+
let n = self.graph.num_vertices();
151+
vec![n; n]
152+
}
153+
154+
fn evaluate(&self, config: &[usize]) -> bool {
155+
self.is_valid_solution(config)
156+
}
157+
}
158+
159+
impl<G: Graph + crate::variant::VariantParam> SatisfactionProblem for OptimalLinearArrangement<G> {}
160+
161+
crate::declare_variants! {
162+
OptimalLinearArrangement<SimpleGraph> => "2^num_vertices",
163+
}
164+
165+
#[cfg(test)]
166+
#[path = "../../unit_tests/models/graph/optimal_linear_arrangement.rs"]
167+
mod tests;

0 commit comments

Comments
 (0)