Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions docs/paper/reductions.typ
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
"SubsetSum": [Subset Sum],
"MinimumFeedbackArcSet": [Minimum Feedback Arc Set],
"MinimumFeedbackVertexSet": [Minimum Feedback Vertex Set],
"QuadraticAssignment": [Quadratic Assignment],
"ShortestCommonSupersequence": [Shortest Common Supersequence],
"MinimumSumMulticenter": [Minimum Sum Multicenter],
"SubgraphIsomorphism": [Subgraph Isomorphism],
Expand Down Expand Up @@ -802,6 +803,56 @@ Integer Linear Programming is a universal modeling framework: virtually every NP
) <fig:ilp-example>
]

#problem-def("QuadraticAssignment")[
Given $n$ facilities and $m$ locations ($n <= m$), a flow matrix $C in ZZ^(n times n)$ representing flows between facilities, and a distance matrix $D in ZZ^(m times m)$ representing distances between locations, find an injective assignment $f: {1, dots, n} -> {1, dots, m}$ that minimizes
$ sum_(i != j) C_(i j) dot D_(f(i), f(j)). $
][
The Quadratic Assignment Problem was introduced by Koopmans and Beckmann (1957) to model the optimal placement of economic activities (facilities) across geographic locations, minimizing total transportation cost weighted by inter-facility flows. It is NP-hard, as shown by Sahni and Gonzalez (1976) via reduction from the Hamiltonian Circuit problem. QAP is widely regarded as one of the hardest combinatorial optimization problems: even moderate instances ($n > 20$) challenge state-of-the-art exact solvers. Best exact approaches use branch-and-bound with Gilmore--Lawler bounds or cutting-plane methods; the best known general algorithm runs in $O^*(n!)$ by exhaustive enumeration of all permutations#footnote[No algorithm significantly improving on brute-force permutation enumeration is known for general QAP.].

Applications include facility layout planning, keyboard and control panel design, scheduling, VLSI placement, and hospital floor planning. As a special case, when $D$ is a distance matrix on a line (i.e., $D_(k l) = |k - l|$), QAP reduces to the Optimal Linear Arrangement problem.

*Example.* Consider $n = m = 4$ with flow matrix $C$ and distance matrix $D$:
$ C = mat(0, 3, 0, 2; 3, 0, 0, 1; 0, 0, 0, 4; 2, 1, 4, 0), quad D = mat(0, 1, 2, 3; 1, 0, 1, 2; 2, 1, 0, 1; 3, 2, 1, 0). $
The identity assignment $f(i) = i$ gives cost $sum_(i != j) C_(i j) dot D_(i, j) = 3 dot 1 + 2 dot 3 + 3 dot 1 + 1 dot 2 + 4 dot 1 + 2 dot 3 + 1 dot 2 + 4 dot 1 = 3 + 6 + 3 + 2 + 4 + 6 + 2 + 4 = 30$. However, the assignment $f = (1, 2, 4, 3)$ — swapping facilities 3 and 4 — gives cost $3 dot 1 + 2 dot 2 + 3 dot 1 + 1 dot 1 + 4 dot 1 + 2 dot 2 + 1 dot 1 + 4 dot 1 = 3 + 4 + 3 + 1 + 4 + 4 + 1 + 4 = 24$. The optimal assignment is $f^* = (3, 4, 1, 2)$ with cost 22: it places the heavily interacting facilities 3 and 4 (flow 4) at adjacent locations.

#figure(
canvas(length: 1cm, {
import draw: *
// Facility column (left)
let fac-x = 0
let loc-x = 5
let ys = (3, 2, 1, 0)
// Draw facility nodes
for i in range(4) {
circle((fac-x, ys.at(i)), radius: 0.3, fill: graph-colors.at(0), stroke: 0.8pt + graph-colors.at(0), name: "f" + str(i))
content("f" + str(i), text(fill: white, 8pt)[$F_#(i+1)$])
}
// Draw location nodes
for j in range(4) {
circle((loc-x, ys.at(j)), radius: 0.3, fill: graph-colors.at(1), stroke: 0.8pt + graph-colors.at(1), name: "l" + str(j))
content("l" + str(j), text(fill: white, 8pt)[$L_#(j+1)$])
}
// Column labels
content((fac-x, 3.7), text(9pt, weight: "bold")[Facilities])
content((loc-x, 3.7), text(9pt, weight: "bold")[Locations])
// Optimal assignment f* = (3, 4, 1, 2): F1→L3, F2→L4, F3→L1, F4→L2
let assignments = ((0, 2), (1, 3), (2, 0), (3, 1))
for (fi, li) in assignments {
line("f" + str(fi) + ".east", "l" + str(li) + ".west",
mark: (end: "straight"), stroke: 1.2pt + luma(80))
}
// Annotate key flow: F3↔F4 have flow 4
on-layer(-1, {
rect((-0.55, -0.55), (0.55, 1.55),
fill: graph-colors.at(0).transparentize(92%),
stroke: (dash: "dashed", paint: graph-colors.at(0).transparentize(50%), thickness: 0.6pt))
})
content((fac-x, -0.9), text(6pt, fill: luma(100))[flow$(F_3, F_4) = 4$])
}),
caption: [Optimal assignment $f^* = (3, 4, 1, 2)$ for the $4 times 4$ QAP instance. Facilities (blue, left) are assigned to locations (red, right) by arrows. Facilities $F_3$ and $F_4$ (highest flow $= 4$) are assigned to adjacent locations $L_1$ and $L_2$ (distance $= 1$). Total cost $= 22$.],
) <fig:qap-example>
]

#problem-def("ClosestVectorProblem")[
Given a lattice basis $bold(B) in RR^(m times n)$ (columns $bold(b)_1, dots, bold(b)_n in RR^m$ spanning lattice $cal(L)(bold(B)) = {bold(B) bold(x) : bold(x) in ZZ^n}$) and target $bold(t) in RR^m$, find $bold(x) in ZZ^n$ minimizing $norm(bold(B) bold(x) - bold(t))_2$.
][
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 @@ -462,6 +462,22 @@
}
]
},
{
"name": "QuadraticAssignment",
"description": "Minimize total cost of assigning facilities to locations",
"fields": [
{
"name": "cost_matrix",
"type_name": "Vec<Vec<i64>>",
"description": "Flow/cost matrix between facilities"
},
{
"name": "distance_matrix",
"type_name": "Vec<Vec<i64>>",
"description": "Distance matrix between locations"
}
]
},
Comment on lines +465 to +480
{
"name": "RuralPostman",
"description": "Find a circuit covering required edges with total length at most B (Rural Postman Problem)",
Expand Down
4 changes: 4 additions & 0 deletions problemreductions-cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@ Flags by problem type:
LCS --strings
FAS --arcs [--weights] [--num-vertices]
FVS --arcs [--weights] [--num-vertices]
QAP --matrix (cost), --distance-matrix
FlowShopScheduling --task-lengths, --deadline [--num-processors]
SCS --strings, --bound [--alphabet-size]
ILP, CircuitSAT (via reduction only)
Expand Down Expand Up @@ -356,6 +357,9 @@ pub struct CreateArgs {
/// Directed arcs for directed graph problems (e.g., 0>1,1>2,2>0)
#[arg(long)]
pub arcs: Option<String>,
/// Distance matrix for QuadraticAssignment (semicolon-separated rows, e.g., "0,1,2;1,0,1;2,1,0")
#[arg(long)]
pub distance_matrix: Option<String>,
/// Task lengths for FlowShopScheduling (semicolon-separated rows: "3,4,2;2,3,5;4,1,3")
#[arg(long)]
pub task_lengths: Option<String>,
Expand Down
81 changes: 81 additions & 0 deletions problemreductions-cli/src/commands/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ fn all_data_flags_empty(args: &CreateArgs) -> bool {
&& args.pattern.is_none()
&& args.strings.is_none()
&& args.arcs.is_none()
&& args.distance_matrix.is_none()
&& args.task_lengths.is_none()
&& args.deadline.is_none()
&& args.num_processors.is_none()
Expand Down Expand Up @@ -101,6 +102,7 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str {
"Satisfiability" => "--num-vars 3 --clauses \"1,2;-1,3\"",
"KSatisfiability" => "--num-vars 3 --clauses \"1,2,3;-1,2,-3\" --k 3",
"QUBO" => "--matrix \"1,0.5;0.5,2\"",
"QuadraticAssignment" => "--matrix \"0,5;5,0\" --distance-matrix \"0,1;1,0\"",
"SpinGlass" => "--graph 0-1,1-2 --couplings 1,1",
"KColoring" => "--graph 0-1,1-2,2-0 --k 3",
"MinimumSumMulticenter" => {
Expand Down Expand Up @@ -375,6 +377,54 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> {
util::ser_ksat(num_vars, clauses, k)?
}

// QuadraticAssignment
"QuadraticAssignment" => {
let cost_str = args.matrix.as_deref().ok_or_else(|| {
anyhow::anyhow!(
"QuadraticAssignment requires --matrix (cost) and --distance-matrix\n\n\
Usage: pred create QAP --matrix \"0,5;5,0\" --distance-matrix \"0,1;1,0\""
)
})?;
let dist_str = args.distance_matrix.as_deref().ok_or_else(|| {
anyhow::anyhow!(
"QuadraticAssignment requires --distance-matrix\n\n\
Usage: pred create QAP --matrix \"0,5;5,0\" --distance-matrix \"0,1;1,0\""
)
})?;
let cost_matrix = parse_i64_matrix(cost_str).context("Invalid cost matrix")?;
let distance_matrix = parse_i64_matrix(dist_str).context("Invalid distance matrix")?;
let n = cost_matrix.len();
for (i, row) in cost_matrix.iter().enumerate() {
if row.len() != n {
bail!(
"cost matrix must be square: row {i} has {} columns, expected {n}",
row.len()
);
}
}
let m = distance_matrix.len();
for (i, row) in distance_matrix.iter().enumerate() {
if row.len() != m {
bail!(
"distance matrix must be square: row {i} has {} columns, expected {m}",
row.len()
);
}
}
if n > m {
bail!("num_facilities ({n}) must be <= num_locations ({m})");
}
(
ser(
problemreductions::models::algebraic::QuadraticAssignment::new(
cost_matrix,
distance_matrix,
),
)?,
resolved_variant.clone(),
)
}
Comment on lines +380 to +426

// QUBO
"QUBO" => {
let matrix = parse_matrix(args)?;
Expand Down Expand Up @@ -1209,6 +1259,37 @@ fn parse_matrix(args: &CreateArgs) -> Result<Vec<Vec<f64>>> {
.collect()
}

/// Parse a semicolon-separated matrix of i64 values.
/// E.g., "0,5;5,0"
fn parse_i64_matrix(s: &str) -> Result<Vec<Vec<i64>>> {
let matrix: Vec<Vec<i64>> = s
.split(';')
.enumerate()
.map(|(row_idx, row)| {
row.trim()
.split(',')
.enumerate()
.map(|(col_idx, v)| {
v.trim().parse::<i64>().map_err(|e| {
anyhow::anyhow!("Invalid value at row {row_idx}, col {col_idx}: {e}")
})
})
.collect()
})
.collect::<Result<_>>()?;
if let Some(first_len) = matrix.first().map(|r| r.len()) {
for (i, row) in matrix.iter().enumerate() {
if row.len() != first_len {
bail!(
"Ragged matrix: row {i} has {} columns, expected {first_len}",
row.len()
);
}
}
}
Ok(matrix)
}

/// Parse `--arcs` as directed arc pairs and build a `DirectedGraph`.
///
/// Returns `(graph, num_arcs)`. Infers vertex count from arc endpoints
Expand Down
4 changes: 3 additions & 1 deletion problemreductions-cli/src/dispatch.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use anyhow::{bail, Context, Result};
use problemreductions::models::algebraic::{ClosestVectorProblem, ILP};
use problemreductions::models::algebraic::{ClosestVectorProblem, QuadraticAssignment, ILP};
use problemreductions::models::misc::{
BinPacking, FlowShopScheduling, Knapsack, LongestCommonSubsequence,
ShortestCommonSupersequence, SubsetSum,
Expand Down Expand Up @@ -229,6 +229,7 @@ pub fn load_problem(
},
"MaximumSetPacking" => deser_opt::<MaximumSetPacking<i32>>(data),
"MinimumSetCovering" => deser_opt::<MinimumSetCovering<i32>>(data),
"QuadraticAssignment" => deser_opt::<QuadraticAssignment>(data),
"QUBO" => deser_opt::<QUBO<f64>>(data),
"SpinGlass" => match variant.get("weight").map(|s| s.as_str()) {
Some("f64") => deser_opt::<SpinGlass<SimpleGraph, f64>>(data),
Expand Down Expand Up @@ -304,6 +305,7 @@ pub fn serialize_any_problem(
_ => try_ser::<MaximumSetPacking<i32>>(any),
},
"MinimumSetCovering" => try_ser::<MinimumSetCovering<i32>>(any),
"QuadraticAssignment" => try_ser::<QuadraticAssignment>(any),
"QUBO" => try_ser::<QUBO<f64>>(any),
"SpinGlass" => match variant.get("weight").map(|s| s.as_str()) {
Some("f64") => try_ser::<SpinGlass<SimpleGraph, f64>>(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 @@ -24,6 +24,7 @@ pub const ALIASES: &[(&str, &str)] = &[
("LCS", "LongestCommonSubsequence"),
("MaxMatching", "MaximumMatching"),
("FVS", "MinimumFeedbackVertexSet"),
("QAP", "QuadraticAssignment"),
("SCS", "ShortestCommonSupersequence"),
("FAS", "MinimumFeedbackArcSet"),
("pmedian", "MinimumSumMulticenter"),
Expand Down Expand Up @@ -69,6 +70,7 @@ pub fn resolve_alias(input: &str) -> String {
"fas" | "minimumfeedbackarcset" => "MinimumFeedbackArcSet".to_string(),
"minimumsummulticenter" | "pmedian" => "MinimumSumMulticenter".to_string(),
"subsetsum" => "SubsetSum".to_string(),
"quadraticassignment" | "qap" => "QuadraticAssignment".to_string(),
"scs" | "shortestcommonsupersequence" => "ShortestCommonSupersequence".to_string(),
"hamiltonianpath" => "HamiltonianPath".to_string(),
_ => input.to_string(), // pass-through for exact names
Expand Down
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ pub mod variant;
/// Prelude module for convenient imports.
pub mod prelude {
// Problem types
pub use crate::models::algebraic::{BMF, QUBO};
pub use crate::models::algebraic::{QuadraticAssignment, BMF, QUBO};
pub use crate::models::formula::{CNFClause, CircuitSAT, KSatisfiability, Satisfiability};
pub use crate::models::graph::{
BicliqueCover, GraphPartitioning, HamiltonianPath, IsomorphicSpanningTree, SpinGlass,
Expand Down
3 changes: 3 additions & 0 deletions src/models/algebraic/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,16 @@
//! - [`ILP`]: Integer Linear Programming
//! - [`ClosestVectorProblem`]: Closest Vector Problem (minimize lattice distance)
//! - [`BMF`]: Boolean Matrix Factorization
//! - [`QuadraticAssignment`]: Quadratic Assignment Problem

pub(crate) mod bmf;
mod closest_vector_problem;
mod ilp;
mod quadratic_assignment;
mod qubo;

pub use bmf::BMF;
pub use closest_vector_problem::{ClosestVectorProblem, VarBounds};
pub use ilp::{Comparison, LinearConstraint, ObjectiveSense, VariableDomain, ILP};
pub use quadratic_assignment::QuadraticAssignment;
pub use qubo::QUBO;
Loading
Loading