Skip to content
Merged
25 changes: 25 additions & 0 deletions docs/paper/reductions.typ
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
"BicliqueCover": [Biclique Cover],
"BinPacking": [Bin Packing],
"ClosestVectorProblem": [Closest Vector Problem],
"LongestCommonSubsequence": [Longest Common Subsequence],
"SubsetSum": [Subset Sum],
"MinimumFeedbackVertexSet": [Minimum Feedback Vertex Set],
)
Expand Down Expand Up @@ -980,6 +981,14 @@ Biclique Cover is equivalent to factoring the biadjacency matrix $M$ of the bipa
*Example.* Let $n = 4$ items with weights $(2, 3, 4, 5)$, values $(3, 4, 5, 7)$, and capacity $C = 7$. Selecting $S = {1, 2}$ (items with weights 3 and 4) gives total weight $3 + 4 = 7 lt.eq C$ and total value $4 + 5 = 9$. Selecting $S = {0, 3}$ (weights 2 and 5) gives weight $2 + 5 = 7 lt.eq C$ and value $3 + 7 = 10$, which is optimal.
]

#problem-def("LongestCommonSubsequence")[
Given $k$ strings $s_1, dots, s_k$ over a finite alphabet $Sigma$, find a longest string $w$ that is a subsequence of every $s_i$. A string $w$ is a _subsequence_ of $s$ if $w$ can be obtained by deleting zero or more characters from $s$ without changing the order of the remaining characters.
][
The LCS problem is polynomial-time solvable for $k = 2$ strings via dynamic programming in $O(n_1 n_2)$ time (Wagner & Fischer, 1974), but NP-hard for $k gt.eq 3$ strings @maier1978. It is a foundational problem in bioinformatics (sequence alignment), version control (diff algorithms), and data compression. The problem is listed as SR10 in Garey & Johnson @garey1979.

*Example.* Let $s_1 = $ `ABAC` and $s_2 = $ `BACA` over $Sigma = {A, B, C}$. The longest common subsequence has length 3, e.g., `BAC`: positions 1, 2, 3 of $s_1$ match positions 0, 1, 2 of $s_2$.
]

#problem-def("SubsetSum")[
Given a finite set $A = {a_0, dots, a_(n-1)}$ with sizes $s(a_i) in ZZ^+$ and a target $B in ZZ^+$, determine whether there exists a subset $A' subset.eq A$ such that $sum_(a in A') s(a) = B$.
][
Expand Down Expand Up @@ -1702,6 +1711,22 @@ The following reductions to Integer Linear Programming are straightforward formu
_Solution extraction._ For each position $k$, find vertex $v$ with $x_(v,k) = 1$ to recover the tour permutation; then select edges between consecutive positions.
]

#reduction-rule("LongestCommonSubsequence", "ILP")[
The match-pair ILP formulation @blum2021 encodes subsequence alignment as a binary optimization. For two strings $s_1$ (length $n_1$) and $s_2$ (length $n_2$), each position pair $(j_1, j_2)$ where $s_1[j_1] = s_2[j_2]$ yields a binary variable. Constraints enforce one-to-one matching and order preservation (no crossings). The objective maximizes the number of matched pairs.
][
_Construction._ Given strings $s_1$ and $s_2$:

_Variables:_ Binary $m_(j_1, j_2) in {0, 1}$ for each $(j_1, j_2)$ with $s_1[j_1] = s_2[j_2]$. Interpretation: $m_(j_1, j_2) = 1$ iff position $j_1$ of $s_1$ is matched to position $j_2$ of $s_2$.

_Constraints:_ (1) Each position in $s_1$ matched at most once: $sum_(j_2 : (j_1, j_2) in M) m_(j_1, j_2) lt.eq 1$ for all $j_1$. (2) Each position in $s_2$ matched at most once: $sum_(j_1 : (j_1, j_2) in M) m_(j_1, j_2) lt.eq 1$ for all $j_2$. (3) No crossings: for $(j_1, j_2), (j'_1, j'_2) in M$ with $j_1 < j'_1$ and $j_2 > j'_2$: $m_(j_1, j_2) + m_(j'_1, j'_2) lt.eq 1$.

_Objective:_ Maximize $sum_((j_1, j_2) in M) m_(j_1, j_2)$.

_Correctness._ ($arrow.r.double$) A common subsequence of length $ell$ defines $ell$ matched pairs that are order-preserving (no crossings) and one-to-one, yielding a feasible ILP solution with objective $ell$. ($arrow.l.double$) An ILP solution with objective $ell$ defines $ell$ matched pairs; constraints (1)--(2) ensure one-to-one matching, and constraint (3) ensures order preservation, so the matched characters form a common subsequence of length $ell$.

_Solution extraction._ Collect pairs $(j_1, j_2)$ with $m_(j_1, j_2) = 1$, sort by $j_1$, and read the characters.
]

== Unit Disk Mapping

#reduction-rule("MaximumIndependentSet", "KingsSubgraph")[
Expand Down
21 changes: 21 additions & 0 deletions docs/paper/references.bib
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,27 @@ @article{ibarra1975
doi = {10.1145/321906.321909}
}

@article{maier1978,
author = {David Maier},
title = {The Complexity of Some Problems on Subsequences and Supersequences},
journal = {Journal of the ACM},
volume = {25},
number = {2},
pages = {322--336},
year = {1978},
doi = {10.1145/322063.322075}
}

@article{blum2021,
author = {Christian Blum and Maria J. Blesa and Borja Calvo},
title = {{ILP}-based reduced variable neighborhood search for the longest common subsequence problem},
journal = {Computers \& Operations Research},
volume = {125},
pages = {105089},
year = {2021},
doi = {10.1016/j.cor.2020.105089}
}

@book{sipser2012,
author = {Michael Sipser},
title = {Introduction to the Theory of Computation},
Expand Down
109 changes: 109 additions & 0 deletions examples/reduction_longestcommonsubsequence_to_ilp.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
// # LongestCommonSubsequence to ILP Reduction
//
// ## Mathematical Formulation
// Uses the match-pair formulation (Blum et al., 2021).
// For each position pair (j1, j2) where s1[j1] == s2[j2], a binary variable m_{j1,j2}.
// Constraints:
// (1) Each s1 position matched at most once
// (2) Each s2 position matched at most once
// (3) Order preservation: no crossings among matched pairs
// Objective: maximize total matched pairs.
//
// ## This Example
// - Instance: s1 = "ABAC", s2 = "BACA"
// - 6 match pairs, LCS = "BAC" (length 3)
//
// ## Output
// Exports `docs/paper/examples/longestcommonsubsequence_to_ilp.json`.

use problemreductions::export::*;
use problemreductions::models::algebraic::ILP;
use problemreductions::prelude::*;
use problemreductions::solvers::ILPSolver;

pub fn run() {
// 1. Create LCS instance: s1 = "ABAC", s2 = "BACA"
let problem = LongestCommonSubsequence::new(vec![
vec![b'A', b'B', b'A', b'C'],
vec![b'B', b'A', b'C', b'A'],
]);

// 2. Reduce to ILP
let reduction = ReduceTo::<ILP<bool>>::reduce_to(&problem);
let ilp = reduction.target_problem();

// 3. Print transformation
println!("\n=== Problem Transformation ===");
println!(
"Source: LCS with {} strings, total length {}",
problem.num_strings(),
problem.total_length()
);
println!(
"Target: ILP with {} variables, {} constraints",
ilp.num_vars,
ilp.constraints.len()
);

// 4. Solve ILP
let solver = ILPSolver::new();
let ilp_solution = solver
.solve(ilp)
.expect("ILP should be feasible for ABAC/BACA");
println!("\n=== Solution ===");
println!("ILP solution: {:?}", &ilp_solution);

// 5. Extract LCS solution
let extracted = reduction.extract_solution(&ilp_solution);
println!("Source LCS config: {:?}", extracted);

// 6. Verify
let metric = problem.evaluate(&extracted);
assert!(metric.is_valid());
let lcs_length = metric.unwrap();
println!("LCS length: {}", lcs_length);
assert_eq!(lcs_length, 3);
println!("\nReduction verified successfully");

// 7. Collect solutions and export JSON
let solutions = vec![SolutionPair {
source_config: extracted,
target_config: ilp_solution,
}];

let source_variant = variant_to_map(LongestCommonSubsequence::variant());
let target_variant = variant_to_map(ILP::<bool>::variant());
let overhead =
lookup_overhead("LongestCommonSubsequence", &source_variant, "ILP", &target_variant)
.expect("LCS -> ILP overhead not found");

let data = ReductionData {
source: ProblemSide {
problem: LongestCommonSubsequence::NAME.to_string(),
variant: source_variant,
instance: serde_json::json!({
"strings": [
[65, 66, 65, 67],
[66, 65, 67, 65],
],
}),
},
target: ProblemSide {
problem: ILP::<bool>::NAME.to_string(),
variant: target_variant,
instance: serde_json::json!({
"num_vars": ilp.num_vars,
"num_constraints": ilp.constraints.len(),
}),
},
overhead: overhead_to_json(&overhead),
};

let results = ResultData { solutions };
let name = "longestcommonsubsequence_to_ilp";
write_example(name, &data, &results);
}

fn main() {
run()
}
4 changes: 4 additions & 0 deletions problemreductions-cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,7 @@ Flags by problem type:
BicliqueCover --left, --right, --biedges, --k
BMF --matrix (0/1), --rank
CVP --basis, --target-vec [--bounds]
LCS --strings
FVS --arcs [--weights] [--num-vertices]
ILP, CircuitSAT (via reduction only)

Expand Down Expand Up @@ -329,6 +330,9 @@ pub struct CreateArgs {
/// Variable bounds for CVP as "lower,upper" (e.g., "-10,10") [default: -10,10]
#[arg(long, allow_hyphen_values = true)]
pub bounds: Option<String>,
/// Input strings for LCS (semicolon-separated, e.g., "ABAC;BACA")
#[arg(long)]
pub strings: Option<String>,
/// Directed arcs for directed graph problems (e.g., 0>1,1>2,2>0)
#[arg(long)]
pub arcs: Option<String>,
Expand Down
21 changes: 20 additions & 1 deletion problemreductions-cli/src/commands/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use crate::util;
use anyhow::{bail, Context, Result};
use problemreductions::models::algebraic::{ClosestVectorProblem, BMF};
use problemreductions::models::graph::GraphPartitioning;
use problemreductions::models::misc::{BinPacking, PaintShop};
use problemreductions::models::misc::{BinPacking, LongestCommonSubsequence, PaintShop};
use problemreductions::prelude::*;
use problemreductions::registry::collect_schemas;
use problemreductions::topology::{
Expand Down Expand Up @@ -47,6 +47,7 @@ fn all_data_flags_empty(args: &CreateArgs) -> bool {
&& args.basis.is_none()
&& args.target_vec.is_none()
&& args.bounds.is_none()
&& args.strings.is_none()
&& args.arcs.is_none()
}

Expand Down Expand Up @@ -424,6 +425,24 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> {
(ser(BMF::new(matrix, rank))?, resolved_variant.clone())
}

// LongestCommonSubsequence
"LongestCommonSubsequence" => {
let strings_str = args.strings.as_deref().ok_or_else(|| {
anyhow::anyhow!(
"LCS requires --strings\n\n\
Usage: pred create LCS --strings \"ABAC;BACA\""
)
})?;
let strings: Vec<Vec<u8>> = strings_str
.split(';')
.map(|s| s.trim().as_bytes().to_vec())
.collect();
(
ser(LongestCommonSubsequence::new(strings))?,
resolved_variant.clone(),
)
}

// ClosestVectorProblem
"ClosestVectorProblem" => {
let basis_str = args.basis.as_deref().ok_or_else(|| {
Expand Down
4 changes: 3 additions & 1 deletion problemreductions-cli/src/dispatch.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use anyhow::{bail, Context, Result};
use problemreductions::models::algebraic::{ClosestVectorProblem, ILP};
use problemreductions::models::misc::{BinPacking, Knapsack, SubsetSum};
use problemreductions::models::misc::{BinPacking, Knapsack, LongestCommonSubsequence, SubsetSum};
use problemreductions::prelude::*;
use problemreductions::rules::{MinimizeSteps, ReductionGraph};
use problemreductions::solvers::{BruteForce, ILPSolver, Solver};
Expand Down Expand Up @@ -246,6 +246,7 @@ pub fn load_problem(
_ => deser_opt::<ClosestVectorProblem<i32>>(data),
},
"Knapsack" => deser_opt::<Knapsack>(data),
"LongestCommonSubsequence" => deser_opt::<LongestCommonSubsequence>(data),
"MinimumFeedbackVertexSet" => deser_opt::<MinimumFeedbackVertexSet<i32>>(data),
"SubsetSum" => deser_sat::<SubsetSum>(data),
_ => bail!("{}", crate::problem_name::unknown_problem_error(&canonical)),
Expand Down Expand Up @@ -309,6 +310,7 @@ pub fn serialize_any_problem(
_ => try_ser::<ClosestVectorProblem<i32>>(any),
},
"Knapsack" => try_ser::<Knapsack>(any),
"LongestCommonSubsequence" => try_ser::<LongestCommonSubsequence>(any),
"MinimumFeedbackVertexSet" => try_ser::<MinimumFeedbackVertexSet<i32>>(any),
"SubsetSum" => try_ser::<SubsetSum>(any),
_ => bail!("{}", crate::problem_name::unknown_problem_error(&canonical)),
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 @@ -20,6 +20,7 @@ pub const ALIASES: &[(&str, &str)] = &[
("KSAT", "KSatisfiability"),
("TSP", "TravelingSalesman"),
("CVP", "ClosestVectorProblem"),
("LCS", "LongestCommonSubsequence"),
("MaxMatching", "MaximumMatching"),
("FVS", "MinimumFeedbackVertexSet"),
];
Expand Down Expand Up @@ -54,6 +55,7 @@ pub fn resolve_alias(input: &str) -> String {
"binpacking" => "BinPacking".to_string(),
"cvp" | "closestvectorproblem" => "ClosestVectorProblem".to_string(),
"knapsack" => "Knapsack".to_string(),
"lcs" | "longestcommonsubsequence" => "LongestCommonSubsequence".to_string(),
"fvs" | "minimumfeedbackvertexset" => "MinimumFeedbackVertexSet".to_string(),
"subsetsum" => "SubsetSum".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 @@ -45,7 +45,7 @@ pub mod prelude {
KColoring, MaxCut, MaximalIS, MaximumClique, MaximumIndependentSet, MaximumMatching,
MinimumDominatingSet, MinimumFeedbackVertexSet, MinimumVertexCover, TravelingSalesman,
};
pub use crate::models::misc::{BinPacking, Factoring, Knapsack, PaintShop, SubsetSum};
pub use crate::models::misc::{BinPacking, Factoring, Knapsack, LongestCommonSubsequence, PaintShop, SubsetSum};
pub use crate::models::set::{MaximumSetPacking, MinimumSetCovering};

// Core traits
Expand Down
Loading
Loading