Skip to content
Merged
11 changes: 10 additions & 1 deletion docs/paper/reductions.typ
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
"BicliqueCover": [Biclique Cover],
"BinPacking": [Bin Packing],
"ClosestVectorProblem": [Closest Vector Problem],
"OptimalLinearArrangement": [Optimal Linear Arrangement],
"RuralPostman": [Rural Postman],
"LongestCommonSubsequence": [Longest Common Subsequence],
"SubsetSum": [Subset Sum],
Expand All @@ -63,7 +64,6 @@
"ShortestCommonSupersequence": [Shortest Common Supersequence],
"MinimumSumMulticenter": [Minimum Sum Multicenter],
"SubgraphIsomorphism": [Subgraph Isomorphism],
"SubsetSum": [Subset Sum],
"FlowShopScheduling": [Flow Shop Scheduling],
)

Expand Down Expand Up @@ -578,6 +578,15 @@ One of the most intensely studied NP-hard problems, with applications in logisti
caption: [Complete graph $K_4$ with weighted edges. The optimal tour $v_0 -> v_1 -> v_2 -> v_3 -> v_0$ (blue edges) has cost 6.],
) <fig:k4-tsp>
]
#problem-def("OptimalLinearArrangement")[
Given an undirected graph $G=(V,E)$ and a non-negative integer $K$, is there a bijection $f: V -> {0, 1, dots, |V|-1}$ such that $sum_({u,v} in E) |f(u) - f(v)| <= K$?
][
A classical NP-complete decision problem from Garey & Johnson (GT42) @garey1979, with applications in VLSI design, graph drawing, and sparse matrix reordering. The problem asks whether vertices can be placed on a line so that the total "stretch" of all edges is at most $K$.

NP-completeness was established by Garey, Johnson, and Stockmeyer @gareyJohnsonStockmeyer1976, via reduction from Simple Max Cut. The problem remains NP-complete on bipartite graphs, but is solvable in polynomial time on trees. The best known exact algorithm for general graphs uses dynamic programming over subsets in $O^*(2^n)$ time and space (Held-Karp style), analogous to TSP.

*Example.* Consider the path graph $P_3$: vertices ${v_0, v_1, v_2}$ with edges ${v_0, v_1}$ and ${v_1, v_2}$. The identity arrangement $f(v_i) = i$ gives cost $|0-1| + |1-2| = 2$. With bound $K = 2$, this is a YES instance. For a triangle $K_3$ with the same vertex set plus edge ${v_0, v_2}$, any arrangement yields cost 4, so a bound of $K = 3$ gives a NO instance.
]
#problem-def("MaximumClique")[
Given $G = (V, E)$, find $K subset.eq V$ maximizing $|K|$ such that all pairs in $K$ are adjacent: $forall u, v in K: (u, v) in E$. Equivalent to MIS on the complement graph $overline(G)$.
][
Expand Down
10 changes: 10 additions & 0 deletions docs/paper/references.bib
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down
16 changes: 16 additions & 0 deletions docs/src/reductions/problem_schemas.json
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,22 @@
}
]
},
{
"name": "OptimalLinearArrangement",
"description": "Find a vertex ordering on a line with total edge length at most K",
"fields": [
{
"name": "graph",
"type_name": "G",
"description": "The undirected graph G=(V,E)"
},
{
"name": "bound",
"type_name": "usize",
"description": "Upper bound K on total edge length"
}
]
},
{
"name": "PaintShop",
"description": "Minimize color changes in paint shop sequence",
Expand Down
1 change: 1 addition & 0 deletions problemreductions-cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,7 @@ Flags by problem type:
BicliqueCover --left, --right, --biedges, --k
BMF --matrix (0/1), --rank
CVP --basis, --target-vec [--bounds]
OptimalLinearArrangement --graph, --bound
RuralPostman (RPP) --graph, --edge-weights, --required-edges, --bound
SubgraphIsomorphism --graph (host), --pattern (pattern)
LCS --strings
Expand Down
39 changes: 38 additions & 1 deletion problemreductions-cli/src/commands/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str {
}
"PartitionIntoTriangles" => "--graph 0-1,1-2,0-2",
"Factoring" => "--target 15 --m 4 --n 4",
"OptimalLinearArrangement" => "--graph 0-1,1-2,2-3 --bound 5",
"MinimumFeedbackArcSet" => "--arcs \"0>1,1>2,2>0\"",
"RuralPostman" => {
"--graph 0-1,1-2,2-3,3-0 --edge-weights 1,1,1,1 --required-edges 0,2 --bound 4"
Expand Down Expand Up @@ -609,6 +610,25 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> {
)
}

// OptimalLinearArrangement — graph + bound
"OptimalLinearArrangement" => {
let (graph, _) = parse_graph(args).map_err(|e| {
anyhow::anyhow!(
"{e}\n\nUsage: pred create OptimalLinearArrangement --graph 0-1,1-2,2-3 --bound 5"
)
})?;
let bound = args.bound.ok_or_else(|| {
anyhow::anyhow!(
"OptimalLinearArrangement requires --bound (upper bound K on total edge length)\n\n\
Usage: pred create OptimalLinearArrangement --graph 0-1,1-2,2-3 --bound 5"
)
})? as usize;
(
ser(OptimalLinearArrangement::new(graph, bound))?,
resolved_variant.clone(),
)
}

// FlowShopScheduling
"FlowShopScheduling" => {
let task_str = args.task_lengths.as_deref().ok_or_else(|| {
Expand Down Expand Up @@ -1421,11 +1441,28 @@ fn create_random(
util::ser_kcoloring(graph, k)?
}

// OptimalLinearArrangement — graph + bound
"OptimalLinearArrangement" => {
let edge_prob = args.edge_prob.unwrap_or(0.5);
if !(0.0..=1.0).contains(&edge_prob) {
bail!("--edge-prob must be between 0.0 and 1.0");
}
let graph = util::create_random_graph(num_vertices, edge_prob, args.seed);
// Default bound: (n-1) * num_edges ensures satisfiability (max edge stretch is n-1)
let n = graph.num_vertices();
let bound = args
.bound
.map(|b| b as usize)
.unwrap_or((n.saturating_sub(1)) * graph.num_edges());
let variant = variant_map(&[("graph", "SimpleGraph")]);
(ser(OptimalLinearArrangement::new(graph, bound))?, variant)
}

_ => bail!(
"Random generation is not supported for {canonical}. \
Supported: graph-based problems (MIS, MVC, MaxCut, MaxClique, \
MaximumMatching, MinimumDominatingSet, SpinGlass, KColoring, TravelingSalesman, \
HamiltonianPath)"
OptimalLinearArrangement, HamiltonianPath)"
),
};

Expand Down
2 changes: 2 additions & 0 deletions problemreductions-cli/src/dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,7 @@ pub fn load_problem(
_ => deser_opt::<ClosestVectorProblem<i32>>(data),
},
"Knapsack" => deser_opt::<Knapsack>(data),
"OptimalLinearArrangement" => deser_sat::<OptimalLinearArrangement<SimpleGraph>>(data),
"SubgraphIsomorphism" => deser_sat::<SubgraphIsomorphism>(data),
"PartitionIntoTriangles" => deser_sat::<PartitionIntoTriangles<SimpleGraph>>(data),
"LongestCommonSubsequence" => deser_opt::<LongestCommonSubsequence>(data),
Expand Down Expand Up @@ -330,6 +331,7 @@ pub fn serialize_any_problem(
_ => try_ser::<ClosestVectorProblem<i32>>(any),
},
"Knapsack" => try_ser::<Knapsack>(any),
"OptimalLinearArrangement" => try_ser::<OptimalLinearArrangement<SimpleGraph>>(any),
"SubgraphIsomorphism" => try_ser::<SubgraphIsomorphism>(any),
"PartitionIntoTriangles" => try_ser::<PartitionIntoTriangles<SimpleGraph>>(any),
"LongestCommonSubsequence" => try_ser::<LongestCommonSubsequence>(any),
Expand Down
2 changes: 2 additions & 0 deletions problemreductions-cli/src/problem_name.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ pub const ALIASES: &[(&str, &str)] = &[
("RPP", "RuralPostman"),
("LCS", "LongestCommonSubsequence"),
("MaxMatching", "MaximumMatching"),
("OLA", "OptimalLinearArrangement"),
("FVS", "MinimumFeedbackVertexSet"),
("SCS", "ShortestCommonSupersequence"),
("FAS", "MinimumFeedbackArcSet"),
Expand Down Expand Up @@ -61,6 +62,7 @@ pub fn resolve_alias(input: &str) -> String {
"binpacking" => "BinPacking".to_string(),
"cvp" | "closestvectorproblem" => "ClosestVectorProblem".to_string(),
"knapsack" => "Knapsack".to_string(),
"optimallineararrangement" | "ola" => "OptimalLinearArrangement".to_string(),
"subgraphisomorphism" => "SubgraphIsomorphism".to_string(),
"partitionintotriangles" => "PartitionIntoTriangles".to_string(),
"lcs" | "longestcommonsubsequence" => "LongestCommonSubsequence".to_string(),
Expand Down
4 changes: 2 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@ pub mod prelude {
pub use crate::models::graph::{
KColoring, MaxCut, MaximalIS, MaximumClique, MaximumIndependentSet, MaximumMatching,
MinimumDominatingSet, MinimumFeedbackArcSet, MinimumFeedbackVertexSet,
MinimumSumMulticenter, MinimumVertexCover, PartitionIntoTriangles, RuralPostman,
TravelingSalesman,
MinimumSumMulticenter, MinimumVertexCover, OptimalLinearArrangement,
PartitionIntoTriangles, RuralPostman, TravelingSalesman,
};
pub use crate::models::misc::{
BinPacking, Factoring, FlowShopScheduling, Knapsack, LongestCommonSubsequence, PaintShop,
Expand Down
3 changes: 3 additions & 0 deletions src/models/graph/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
//! - [`SpinGlass`]: Ising model Hamiltonian
//! - [`HamiltonianPath`]: Hamiltonian path (simple path visiting every vertex)
//! - [`BicliqueCover`]: Biclique cover on bipartite graphs
//! - [`OptimalLinearArrangement`]: Optimal linear arrangement (total edge length at most K)
//! - [`MinimumFeedbackArcSet`]: Minimum feedback arc set on directed graphs
//! - [`MinimumSumMulticenter`]: Min-sum multicenter (p-median)
//! - [`RuralPostman`]: Rural Postman (circuit covering required edges)
Expand All @@ -37,6 +38,7 @@ pub(crate) mod minimum_feedback_arc_set;
pub(crate) mod minimum_feedback_vertex_set;
pub(crate) mod minimum_sum_multicenter;
pub(crate) mod minimum_vertex_cover;
pub(crate) mod optimal_linear_arrangement;
pub(crate) mod partition_into_triangles;
pub(crate) mod rural_postman;
pub(crate) mod spin_glass;
Expand All @@ -58,6 +60,7 @@ pub use minimum_feedback_arc_set::MinimumFeedbackArcSet;
pub use minimum_feedback_vertex_set::MinimumFeedbackVertexSet;
pub use minimum_sum_multicenter::MinimumSumMulticenter;
pub use minimum_vertex_cover::MinimumVertexCover;
pub use optimal_linear_arrangement::OptimalLinearArrangement;
pub use partition_into_triangles::PartitionIntoTriangles;
pub use rural_postman::RuralPostman;
pub use spin_glass::SpinGlass;
Expand Down
167 changes: 167 additions & 0 deletions src/models/graph/optimal_linear_arrangement.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
//! Optimal Linear Arrangement problem implementation.
//!
//! The Optimal Linear Arrangement problem asks whether there exists a one-to-one
//! function f: V -> {0, 1, ..., |V|-1} such that the total edge length
//! sum_{{u,v} in E} |f(u) - f(v)| is at most K.

use crate::registry::{FieldInfo, ProblemSchemaEntry};
use crate::topology::{Graph, SimpleGraph};
use crate::traits::{Problem, SatisfactionProblem};
use serde::{Deserialize, Serialize};

inventory::submit! {
ProblemSchemaEntry {
name: "OptimalLinearArrangement",
module_path: module_path!(),
description: "Find a vertex ordering on a line with total edge length at most K",
fields: &[
FieldInfo { name: "graph", type_name: "G", description: "The undirected graph G=(V,E)" },
FieldInfo { name: "bound", type_name: "usize", description: "Upper bound K on total edge length" },
],
}
}

/// The Optimal Linear Arrangement problem.
///
/// Given an undirected graph G = (V, E) and a non-negative integer K,
/// determine whether there exists a one-to-one function f: V -> {0, 1, ..., |V|-1}
/// such that sum_{{u,v} in E} |f(u) - f(v)| <= K.
///
/// This is the decision (satisfaction) version of the problem, following the
/// Garey & Johnson formulation (GT42).
///
/// # Representation
///
/// Each vertex is assigned a variable representing its position in the arrangement.
/// Variable i takes a value in {0, 1, ..., n-1}, and a valid configuration must be
/// a permutation (all positions are distinct) with total edge length at most K.
///
/// # Type Parameters
///
/// * `G` - The graph type (e.g., `SimpleGraph`)
///
/// # Example
///
/// ```
/// use problemreductions::models::graph::OptimalLinearArrangement;
/// use problemreductions::topology::SimpleGraph;
/// use problemreductions::{Problem, Solver, BruteForce};
///
/// // Path graph: 0-1-2-3 with bound 3
/// let graph = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]);
/// let problem = OptimalLinearArrangement::new(graph, 3);
///
/// let solver = BruteForce::new();
/// let solution = solver.find_satisfying(&problem);
/// assert!(solution.is_some());
/// ```
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(bound(deserialize = "G: serde::Deserialize<'de>"))]
pub struct OptimalLinearArrangement<G> {
/// The underlying graph.
graph: G,
/// Upper bound K on total edge length.
bound: usize,
}

impl<G: Graph> OptimalLinearArrangement<G> {
/// Create a new Optimal Linear Arrangement problem.
///
/// # Arguments
/// * `graph` - The undirected graph G = (V, E)
/// * `bound` - The upper bound K on total edge length
pub fn new(graph: G, bound: usize) -> Self {
Self { graph, bound }
}

/// Get a reference to the underlying graph.
pub fn graph(&self) -> &G {
&self.graph
}

/// Get the bound K.
pub fn bound(&self) -> usize {
self.bound
}

/// Get the number of vertices in the underlying graph.
pub fn num_vertices(&self) -> usize {
self.graph.num_vertices()
}

/// Get the number of edges in the underlying graph.
pub fn num_edges(&self) -> usize {
self.graph.num_edges()
}

/// Check if a configuration is a valid permutation with total edge length at most K.
pub fn is_valid_solution(&self, config: &[usize]) -> bool {
match self.total_edge_length(config) {
Some(length) => length <= self.bound,
None => false,
}
}

/// Check if a configuration forms a valid permutation of {0, ..., n-1}.
fn is_valid_permutation(&self, config: &[usize]) -> bool {
let n = self.graph.num_vertices();
if config.len() != n {
return false;
}
let mut seen = vec![false; n];
for &pos in config {
if pos >= n || seen[pos] {
return false;
}
seen[pos] = true;
}
true
}

/// Compute the total edge length for a given arrangement.
///
/// Returns `None` if the configuration is not a valid permutation.
pub fn total_edge_length(&self, config: &[usize]) -> Option<usize> {
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<G> Problem for OptimalLinearArrangement<G>
where
G: Graph + crate::variant::VariantParam,
{
const NAME: &'static str = "OptimalLinearArrangement";
type Metric = bool;

fn variant() -> Vec<(&'static str, &'static str)> {
crate::variant_params![G]
}

fn dims(&self) -> Vec<usize> {
let n = self.graph.num_vertices();
vec![n; n]
}

fn evaluate(&self, config: &[usize]) -> bool {
self.is_valid_solution(config)
}
}

impl<G: Graph + crate::variant::VariantParam> SatisfactionProblem for OptimalLinearArrangement<G> {}

crate::declare_variants! {
OptimalLinearArrangement<SimpleGraph> => "2^num_vertices",
}

#[cfg(test)]
#[path = "../../unit_tests/models/graph/optimal_linear_arrangement.rs"]
mod tests;
3 changes: 2 additions & 1 deletion src/models/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ pub use graph::{
BicliqueCover, GraphPartitioning, HamiltonianPath, IsomorphicSpanningTree, KColoring, MaxCut,
MaximalIS, MaximumClique, MaximumIndependentSet, MaximumMatching, MinimumDominatingSet,
MinimumFeedbackArcSet, MinimumFeedbackVertexSet, MinimumSumMulticenter, MinimumVertexCover,
PartitionIntoTriangles, RuralPostman, SpinGlass, SubgraphIsomorphism, TravelingSalesman,
OptimalLinearArrangement, PartitionIntoTriangles, RuralPostman, SpinGlass,
SubgraphIsomorphism, TravelingSalesman,
};
pub use misc::{
BinPacking, Factoring, FlowShopScheduling, Knapsack, LongestCommonSubsequence, PaintShop,
Expand Down
Loading
Loading