Skip to content

Commit 7f5e79d

Browse files
zazabapclaude
andcommitted
feat: add LongestCommonSubsequence model
Implement the k-string Longest Common Subsequence problem: - Model in src/models/misc/ with binary config over shortest string - 14 unit tests (creation, evaluation, brute force, serialization) - CLI dispatch and LCS alias - Registered in module tree and prelude Closes #108 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 63259ab commit 7f5e79d

7 files changed

Lines changed: 358 additions & 38 deletions

File tree

problemreductions-cli/src/dispatch.rs

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,20 @@
1+
use std::{any::Any, collections::BTreeMap, fmt, ops::Deref, path::Path};
2+
13
use anyhow::{bail, Context, Result};
2-
use problemreductions::models::algebraic::{ClosestVectorProblem, ILP};
3-
use problemreductions::models::misc::BinPacking;
4-
use problemreductions::prelude::*;
5-
use problemreductions::rules::{MinimizeSteps, ReductionGraph};
6-
use problemreductions::solvers::{BruteForce, ILPSolver, Solver};
7-
use problemreductions::topology::{KingsSubgraph, SimpleGraph, TriangularSubgraph, UnitDiskGraph};
8-
use problemreductions::types::ProblemSize;
9-
use problemreductions::variant::{K2, K3, KN};
4+
use problemreductions::{
5+
models::{
6+
algebraic::{ClosestVectorProblem, ILP},
7+
misc::{BinPacking, LongestCommonSubsequence},
8+
},
9+
prelude::*,
10+
rules::{MinimizeSteps, ReductionGraph},
11+
solvers::{BruteForce, ILPSolver, Solver},
12+
topology::{KingsSubgraph, SimpleGraph, TriangularSubgraph, UnitDiskGraph},
13+
types::ProblemSize,
14+
variant::{K2, K3, KN},
15+
};
1016
use serde::Serialize;
1117
use serde_json::Value;
12-
use std::any::Any;
13-
use std::collections::BTreeMap;
14-
use std::fmt;
15-
use std::ops::Deref;
16-
use std::path::Path;
1718

1819
use crate::problem_name::resolve_alias;
1920

@@ -244,6 +245,7 @@ pub fn load_problem(
244245
Some("f64") => deser_opt::<ClosestVectorProblem<f64>>(data),
245246
_ => deser_opt::<ClosestVectorProblem<i32>>(data),
246247
},
248+
"LongestCommonSubsequence" => deser_opt::<LongestCommonSubsequence>(data),
247249
_ => bail!("{}", crate::problem_name::unknown_problem_error(&canonical)),
248250
}
249251
}
@@ -303,6 +305,7 @@ pub fn serialize_any_problem(
303305
Some("f64") => try_ser::<ClosestVectorProblem<f64>>(any),
304306
_ => try_ser::<ClosestVectorProblem<i32>>(any),
305307
},
308+
"LongestCommonSubsequence" => try_ser::<LongestCommonSubsequence>(any),
306309
_ => bail!("{}", crate::problem_name::unknown_problem_error(&canonical)),
307310
}
308311
}

problemreductions-cli/src/problem_name.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
use std::collections::BTreeMap;
2-
use std::ffi::OsStr;
1+
use std::{collections::BTreeMap, ffi::OsStr};
32

43
/// A parsed problem specification: name + optional variant values.
54
#[derive(Debug, Clone)]
@@ -21,6 +20,7 @@ pub const ALIASES: &[(&str, &str)] = &[
2120
("TSP", "TravelingSalesman"),
2221
("BP", "BinPacking"),
2322
("CVP", "ClosestVectorProblem"),
23+
("LCS", "LongestCommonSubsequence"),
2424
];
2525

2626
/// Resolve a short alias to the canonical problem name.
@@ -51,6 +51,7 @@ pub fn resolve_alias(input: &str) -> String {
5151
"bicliquecover" => "BicliqueCover".to_string(),
5252
"bp" | "binpacking" => "BinPacking".to_string(),
5353
"cvp" | "closestvectorproblem" => "ClosestVectorProblem".to_string(),
54+
"lcs" | "longestcommonsubsequence" => "LongestCommonSubsequence".to_string(),
5455
_ => input.to_string(), // pass-through for exact names
5556
}
5657
}

src/lib.rs

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -36,41 +36,41 @@ pub mod variant;
3636
/// Prelude module for convenient imports.
3737
pub mod prelude {
3838
// Problem types
39-
pub use crate::models::algebraic::{BMF, QUBO};
40-
pub use crate::models::formula::{CNFClause, CircuitSAT, KSatisfiability, Satisfiability};
41-
pub use crate::models::graph::{BicliqueCover, SpinGlass};
42-
pub use crate::models::graph::{
43-
KColoring, MaxCut, MaximalIS, MaximumClique, MaximumIndependentSet, MaximumMatching,
44-
MinimumDominatingSet, MinimumVertexCover, TravelingSalesman,
45-
};
46-
pub use crate::models::misc::{BinPacking, Factoring, PaintShop};
47-
pub use crate::models::set::{MaximumSetPacking, MinimumSetCovering};
48-
49-
// Core traits
50-
pub use crate::rules::{ReduceTo, ReductionResult};
51-
pub use crate::solvers::{BruteForce, Solver};
52-
pub use crate::traits::{OptimizationProblem, Problem, SatisfactionProblem};
53-
5439
// Types
5540
pub use crate::error::{ProblemError, Result};
56-
pub use crate::types::{Direction, One, ProblemSize, SolutionSize, Unweighted};
41+
// Core traits
42+
pub use crate::rules::{ReduceTo, ReductionResult};
43+
pub use crate::{
44+
models::{
45+
algebraic::{BMF, QUBO},
46+
formula::{CNFClause, CircuitSAT, KSatisfiability, Satisfiability},
47+
graph::{
48+
BicliqueCover, KColoring, MaxCut, MaximalIS, MaximumClique, MaximumIndependentSet,
49+
MaximumMatching, MinimumDominatingSet, MinimumVertexCover, SpinGlass,
50+
TravelingSalesman,
51+
},
52+
misc::{BinPacking, Factoring, LongestCommonSubsequence, PaintShop},
53+
set::{MaximumSetPacking, MinimumSetCovering},
54+
},
55+
solvers::{BruteForce, Solver},
56+
traits::{OptimizationProblem, Problem, SatisfactionProblem},
57+
types::{Direction, One, ProblemSize, SolutionSize, Unweighted},
58+
};
5759
}
5860

5961
// Re-export commonly used items at crate root
6062
pub use error::{ProblemError, Result};
63+
// Re-export inventory so `declare_variants!` can use `$crate::inventory::submit!`
64+
pub use inventory;
65+
// Re-export proc macros for reduction registration and variant declaration
66+
pub use problemreductions_macros::{declare_variants, reduction};
6167
pub use registry::{ComplexityClass, ProblemInfo};
6268
pub use solvers::{BruteForce, Solver};
6369
pub use traits::{OptimizationProblem, Problem, SatisfactionProblem};
6470
pub use types::{
6571
Direction, NumericSize, One, ProblemSize, SolutionSize, Unweighted, WeightElement,
6672
};
6773

68-
// Re-export proc macros for reduction registration and variant declaration
69-
pub use problemreductions_macros::{declare_variants, reduction};
70-
71-
// Re-export inventory so `declare_variants!` can use `$crate::inventory::submit!`
72-
pub use inventory;
73-
7474
#[cfg(test)]
7575
#[path = "unit_tests/graph_models.rs"]
7676
mod test_graph_models;
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
//! Longest Common Subsequence problem implementation.
2+
//!
3+
//! Given k strings over an alphabet, find the longest string that is a
4+
//! subsequence of every input string. NP-hard for variable k (Maier, 1978).
5+
6+
use serde::{Deserialize, Serialize};
7+
8+
use crate::{
9+
registry::{FieldInfo, ProblemSchemaEntry},
10+
traits::{OptimizationProblem, Problem},
11+
types::{Direction, SolutionSize},
12+
};
13+
14+
inventory::submit! {
15+
ProblemSchemaEntry {
16+
name: "LongestCommonSubsequence",
17+
module_path: module_path!(),
18+
description: "Find the longest string that is a subsequence of every input string",
19+
fields: &[
20+
FieldInfo { name: "strings", type_name: "Vec<Vec<u8>>", description: "The input strings" },
21+
],
22+
}
23+
}
24+
25+
/// The Longest Common Subsequence problem.
26+
///
27+
/// Given `k` strings `s_1, ..., s_k` over an alphabet, find a longest
28+
/// string `w` that is a subsequence of every `s_i`.
29+
///
30+
/// A string `w` is a **subsequence** of `s` if `w` can be obtained by
31+
/// deleting zero or more characters from `s` without changing the order
32+
/// of the remaining characters.
33+
///
34+
/// # Representation
35+
///
36+
/// Configuration is binary selection over the characters of the shortest
37+
/// string. Each variable in `{0, 1}` indicates whether the corresponding
38+
/// character of the shortest string is included in the candidate subsequence.
39+
/// The candidate is valid if the resulting subsequence is also a subsequence
40+
/// of every other input string.
41+
///
42+
/// # Example
43+
///
44+
/// ```
45+
/// use problemreductions::{models::misc::LongestCommonSubsequence, BruteForce, Problem, Solver};
46+
///
47+
/// let problem = LongestCommonSubsequence::new(vec![
48+
/// vec![b'A', b'B', b'C', b'D', b'A', b'B'],
49+
/// vec![b'B', b'D', b'C', b'A', b'B', b'A'],
50+
/// vec![b'B', b'C', b'A', b'D', b'B', b'A'],
51+
/// ]);
52+
/// let solver = BruteForce::new();
53+
/// let solution = solver.find_best(&problem);
54+
/// assert!(solution.is_some());
55+
/// ```
56+
#[derive(Debug, Clone, Serialize, Deserialize)]
57+
pub struct LongestCommonSubsequence {
58+
/// The input strings.
59+
strings: Vec<Vec<u8>>,
60+
}
61+
62+
impl LongestCommonSubsequence {
63+
/// Create a new LCS problem from a list of strings.
64+
///
65+
/// # Panics
66+
///
67+
/// Panics if `strings` is empty.
68+
pub fn new(strings: Vec<Vec<u8>>) -> Self {
69+
assert!(!strings.is_empty(), "must have at least one string");
70+
Self { strings }
71+
}
72+
73+
/// Get the input strings.
74+
pub fn strings(&self) -> &[Vec<u8>] {
75+
&self.strings
76+
}
77+
78+
/// Get the number of input strings.
79+
pub fn num_strings(&self) -> usize {
80+
self.strings.len()
81+
}
82+
83+
/// Get the total length of all input strings.
84+
pub fn total_length(&self) -> usize {
85+
self.strings.iter().map(|s| s.len()).sum()
86+
}
87+
88+
/// Index of the shortest string.
89+
fn shortest_index(&self) -> usize {
90+
self.strings
91+
.iter()
92+
.enumerate()
93+
.min_by_key(|(_, s)| s.len())
94+
.map(|(i, _)| i)
95+
.unwrap_or(0)
96+
}
97+
98+
/// Length of the shortest string.
99+
fn shortest_len(&self) -> usize {
100+
self.strings.iter().map(|s| s.len()).min().unwrap_or(0)
101+
}
102+
}
103+
104+
impl Problem for LongestCommonSubsequence {
105+
const NAME: &'static str = "LongestCommonSubsequence";
106+
type Metric = SolutionSize<i32>;
107+
108+
fn variant() -> Vec<(&'static str, &'static str)> {
109+
crate::variant_params![]
110+
}
111+
112+
fn dims(&self) -> Vec<usize> {
113+
vec![2; self.shortest_len()]
114+
}
115+
116+
fn evaluate(&self, config: &[usize]) -> SolutionSize<i32> {
117+
let si = self.shortest_index();
118+
let shortest = &self.strings[si];
119+
if config.len() != shortest.len() {
120+
return SolutionSize::Invalid;
121+
}
122+
if config.iter().any(|&v| v > 1) {
123+
return SolutionSize::Invalid;
124+
}
125+
// Build the candidate subsequence from selected characters
126+
let candidate: Vec<u8> = config
127+
.iter()
128+
.enumerate()
129+
.filter(|(_, &v)| v == 1)
130+
.map(|(i, _)| shortest[i])
131+
.collect();
132+
// Check that candidate is a subsequence of every other string
133+
for (j, s) in self.strings.iter().enumerate() {
134+
if j == si {
135+
continue;
136+
}
137+
if !is_subsequence(&candidate, s) {
138+
return SolutionSize::Invalid;
139+
}
140+
}
141+
SolutionSize::Valid(candidate.len() as i32)
142+
}
143+
}
144+
145+
impl OptimizationProblem for LongestCommonSubsequence {
146+
type Value = i32;
147+
148+
fn direction(&self) -> Direction {
149+
Direction::Maximize
150+
}
151+
}
152+
153+
/// Check if `sub` is a subsequence of `full`.
154+
fn is_subsequence(sub: &[u8], full: &[u8]) -> bool {
155+
let mut it = full.iter();
156+
for &c in sub {
157+
if !it.any(|&x| x == c) {
158+
return false;
159+
}
160+
}
161+
true
162+
}
163+
164+
crate::declare_variants! {
165+
LongestCommonSubsequence => "2^total_length",
166+
}
167+
168+
#[cfg(test)]
169+
#[path = "../../unit_tests/models/misc/longest_common_subsequence.rs"]
170+
mod tests;

src/models/misc/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,15 @@
33
//! Problems with unique input structures that don't fit other categories:
44
//! - [`BinPacking`]: Bin Packing (minimize bins)
55
//! - [`Factoring`]: Integer factorization
6+
//! - [`LongestCommonSubsequence`]: Longest Common Subsequence (maximize common subsequence length)
67
//! - [`PaintShop`]: Minimize color switches in paint shop scheduling
78
89
mod bin_packing;
910
pub(crate) mod factoring;
11+
pub(crate) mod longest_common_subsequence;
1012
pub(crate) mod paintshop;
1113

1214
pub use bin_packing::BinPacking;
1315
pub use factoring::Factoring;
16+
pub use longest_common_subsequence::LongestCommonSubsequence;
1417
pub use paintshop::PaintShop;

src/models/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,5 @@ pub use graph::{
1515
BicliqueCover, KColoring, MaxCut, MaximalIS, MaximumClique, MaximumIndependentSet,
1616
MaximumMatching, MinimumDominatingSet, MinimumVertexCover, SpinGlass, TravelingSalesman,
1717
};
18-
pub use misc::{BinPacking, Factoring, PaintShop};
18+
pub use misc::{BinPacking, Factoring, LongestCommonSubsequence, PaintShop};
1919
pub use set::{MaximumSetPacking, MinimumSetCovering};

0 commit comments

Comments
 (0)