Skip to content
Merged
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
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,17 @@ make cli # builds target/release/pred

See the [Getting Started](https://codingthrust.github.io/problem-reductions/getting-started.html) guide for usage examples, the reduction workflow, and [CLI usage](https://codingthrust.github.io/problem-reductions/cli.html).

Try a model directly from the CLI:

```bash
# Show the Consecutive Block Minimization model (alias: CBM)
pred show CBM

# Create and solve a small CBM instance (currently with brute-force)
pred create CBM --matrix '[[true,false,true],[false,true,true]]' --bound 2 \
| pred solve - --solver brute-force
```

## MCP Server (AI Integration)

The `pred` CLI includes a built-in [MCP](https://modelcontextprotocol.io/) server for AI assistant integration (Claude Code, Cursor, Windsurf, OpenCode, etc.).
Expand Down
45 changes: 39 additions & 6 deletions docs/paper/reductions.typ
Original file line number Diff line number Diff line change
Expand Up @@ -136,12 +136,13 @@
"SchedulingWithIndividualDeadlines": [Scheduling With Individual Deadlines],
"StaffScheduling": [Staff Scheduling],
"MinimumTardinessSequencing": [Minimum Tardiness Sequencing],
"ConsecutiveBlockMinimization": [Consecutive Block Minimization],
"ConsecutiveOnesSubmatrix": [Consecutive Ones Submatrix],
"SequencingToMinimizeMaximumCumulativeCost": [Sequencing to Minimize Maximum Cumulative Cost],
"SequencingToMinimizeWeightedTardiness": [Sequencing to Minimize Weighted Tardiness],
"SequencingWithinIntervals": [Sequencing Within Intervals],
"SumOfSquaresPartition": [Sum of Squares Partition],
"TwoDimensionalConsecutiveSets": [2-Dimensional Consecutive Sets],
"SequencingWithinIntervals": [Sequencing Within Intervals],
"DirectedTwoCommodityIntegralFlow": [Directed Two-Commodity Integral Flow],
"ConjunctiveBooleanQuery": [Conjunctive Boolean Query],
"RectilinearPictureCompression": [Rectilinear Picture Compression],
Expand Down Expand Up @@ -1891,7 +1892,7 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76],
let n = x.instance.num_attributes
let deps = x.instance.dependencies
let m = deps.len()
let bound = x.instance.bound_k
let bound = x.instance.bound
let key-attrs = range(n).filter(i => x.optimal_config.at(i) == 1)
let fmt-set(s) = "${" + s.map(e => str(e)).join(", ") + "}$"
let fmt-fd(d) = fmt-set(d.at(0)) + " $arrow.r$ " + fmt-set(d.at(1))
Expand Down Expand Up @@ -2484,6 +2485,39 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76],
]
}

#{
let x = load-model-example("ConsecutiveBlockMinimization")
let mat = x.instance.matrix
let K = x.instance.bound
let n-rows = mat.len()
let n-cols = if n-rows > 0 { mat.at(0).len() } else { 0 }
let perm = x.optimal_config
// Count blocks under the satisfying permutation
let total-blocks = 0
for row in mat {
let in-block = false
for p in perm {
if row.at(p) {
if not in-block {
total-blocks += 1
in-block = true
}
} else {
in-block = false
}
}
}
[
#problem-def("ConsecutiveBlockMinimization")[
Given an $m times n$ binary matrix $A$ and a positive integer $K$, determine whether there exists a permutation of the columns of $A$ such that the resulting matrix has at most $K$ maximal blocks of consecutive 1-entries (summed over all rows). A _block_ is a maximal contiguous run of 1-entries within a single row.
][
Consecutive Block Minimization (SR17 in Garey & Johnson) arises in consecutive file organization for information retrieval systems, where records stored on a linear medium must be arranged so that each query's relevant records form a contiguous segment. Applications also include scheduling, production planning, the glass cutting industry, and data compression. NP-complete by reduction from Hamiltonian Path @kou1977. When $K$ equals the number of non-all-zero rows, the problem reduces to testing the _consecutive ones property_, solvable in polynomial time via PQ-trees @booth1975. A 1.5-approximation is known @haddadi2008. The best known exact algorithm runs in $O^*(n!)$ by brute-force enumeration of all column permutations.

*Example.* Let $A$ be the #n-rows$times$#n-cols matrix with rows #mat.enumerate().map(((i, row)) => [$r_#i = (#row.map(v => if v {$1$} else {$0$}).join($,$))$]).join(", ") and $K = #K$. The column permutation $pi = (#perm.map(p => str(p)).join(", "))$ yields #total-blocks total blocks, so #total-blocks $<= #K$ and the answer is YES.
]
]
}

#{
let x = load-model-example("PaintShop")
let n-cars = x.instance.num_cars
Expand Down Expand Up @@ -2765,7 +2799,7 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76],
let mat = x.instance.matrix
let m = mat.len()
let n = mat.at(0).len()
let K = x.instance.bound_k
let K = x.instance.bound
// Convert bool matrix to int for display
let M = mat.map(row => row.map(v => if v { 1 } else { 0 }))
[
Expand Down Expand Up @@ -3462,8 +3496,7 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76],
let nproc = x.instance.num_processors
let deadlines = x.instance.deadlines
let precs = x.instance.precedences
let sample = x.samples.at(0)
let start = sample.config
let start = x.optimal_config
let horizon = deadlines.fold(0, (acc, d) => if d > acc { d } else { acc })
let slot-groups = range(horizon).map(slot => range(ntasks).filter(t => start.at(t) == slot))
let tight-tasks = range(ntasks).filter(t => start.at(t) + 1 == deadlines.at(t))
Expand Down Expand Up @@ -3973,7 +4006,7 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76],
let A = x.instance.matrix
let m = A.len()
let n = A.at(0).len()
let K = x.instance.bound_k
let K = x.instance.bound
// Convert bool matrix to int for display
let A-int = A.map(row => row.map(v => if v { 1 } else { 0 }))
// Use the canonical witness {0, 1, 3}
Expand Down
11 changes: 11 additions & 0 deletions docs/paper/references.bib
Original file line number Diff line number Diff line change
Expand Up @@ -1034,6 +1034,17 @@ @article{kou1977
doi = {10.1137/0206005}
}

@article{haddadi2008,
author = {Salim Haddadi and Zohra Layouni},
title = {Consecutive block minimization is 1.5-approximable},
journal = {Information Processing Letters},
volume = {108},
number = {3},
pages = {161--163},
year = {2008},
doi = {10.1016/j.ipl.2008.05.003}
}

@article{lawler1972,
author = {Eugene L. Lawler},
title = {A Procedure for Computing the $K$ Best Solutions to Discrete Optimization Problems and Its Application to the Shortest Path Problem},
Expand Down
11 changes: 11 additions & 0 deletions docs/src/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,12 @@ pred create SteinerTree --graph 0-1,0-3,1-2,1-3,2-3,2-4,3-4 --edge-weights 2,5,2
# Create a Length-Bounded Disjoint Paths instance
pred create LengthBoundedDisjointPaths --graph 0-1,1-6,0-2,2-3,3-6,0-4,4-5,5-6 --source 0 --sink 6 --num-paths-required 2 --bound 3 -o lbdp.json

# Create a Consecutive Block Minimization instance (alias: CBM)
pred create CBM --matrix '[[true,false,true],[false,true,true]]' --bound 2 -o cbm.json

# CBM currently needs the brute-force solver
pred solve cbm.json --solver brute-force

# Or start from a canonical model example
pred create --example MIS/SimpleGraph/i32 -o example.json

Expand Down Expand Up @@ -342,6 +348,7 @@ pred create MIS --graph 0-1,1-2,2-3 -o problem.json
pred create MIS --graph 0-1,1-2,2-3 --weights 2,1,3,1 -o problem.json
pred create SAT --num-vars 3 --clauses "1,2;-1,3" -o sat.json
pred create QUBO --matrix "1,0.5;0.5,2" -o qubo.json
pred create CBM --matrix '[[true,false,true],[false,true,true]]' --bound 2 -o cbm.json
pred create KColoring --k 3 --graph 0-1,1-2,2-0 -o kcol.json
pred create KthBestSpanningTree --graph 0-1,0-2,1-2 --edge-weights 2,3,1 --k 1 --bound 3 -o kth.json
pred create SpinGlass --graph 0-1,1-2 -o sg.json
Expand All @@ -366,6 +373,10 @@ pred create StrongConnectivityAugmentation --arcs "0>1,1>2,2>0,3>4,4>3,2>3,4>5,5
For `LengthBoundedDisjointPaths`, the CLI flag `--bound` maps to the JSON field
`max_length`.

For `ConsecutiveBlockMinimization`, the `--matrix` flag expects a JSON 2D bool array such as
`'[[true,false,true],[false,true,true]]'`. The example above shows the accepted shape, and solving
CBM instances currently requires `--solver brute-force`.

For problem-specific create help, run `pred create <PROBLEM>` with no additional flags.
The generic `pred create --help` output lists all flags across all problem types.

Expand Down
5 changes: 4 additions & 1 deletion problemreductions-cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,7 @@ Flags by problem type:
BalancedCompleteBipartiteSubgraph --left, --right, --biedges, --k
BiconnectivityAugmentation --graph, --potential-edges, --budget [--num-vertices]
BMF --matrix (0/1), --rank
ConsecutiveBlockMinimization --matrix (JSON 2D bool), --bound-k
ConsecutiveOnesSubmatrix --matrix (0/1), --k
SteinerTree --graph, --edge-weights, --terminals
MultipleCopyFileAllocation --graph, --usage, --storage, --bound
Expand Down Expand Up @@ -357,7 +358,8 @@ pub struct CreateArgs {
/// Number of variables (for SAT/KSAT)
#[arg(long)]
pub num_vars: Option<usize>,
/// Matrix input (semicolon-separated rows; use `pred create <PROBLEM>` for problem-specific formats)
/// Matrix input. QUBO uses semicolon-separated numeric rows ("1,0.5;0.5,2");
/// ConsecutiveBlockMinimization uses a JSON 2D bool array ('[[true,false],[false,true]]')
#[arg(long)]
pub matrix: Option<String>,
/// Shared integer parameter (use `pred create <PROBLEM>` for the problem-specific meaning)
Expand Down Expand Up @@ -552,6 +554,7 @@ pub struct CreateArgs {
/// Alphabet size for LCS, SCS, StringToStringCorrection, or TwoDimensionalConsecutiveSets (optional; inferred from the input strings if omitted)
#[arg(long)]
pub alphabet_size: Option<usize>,

/// Number of attributes for AdditionalKey or MinimumCardinalityKey
#[arg(long)]
pub num_attributes: Option<usize>,
Expand Down
71 changes: 49 additions & 22 deletions problemreductions-cli/src/commands/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ use crate::problem_name::{
use crate::util;
use anyhow::{bail, Context, Result};
use problemreductions::export::{ModelExample, ProblemRef, ProblemSide, RuleExample};
use problemreductions::models::algebraic::{ClosestVectorProblem, ConsecutiveOnesSubmatrix, BMF};
use problemreductions::models::algebraic::{
ClosestVectorProblem, ConsecutiveBlockMinimization, ConsecutiveOnesSubmatrix, BMF,
};
use problemreductions::models::graph::{
GeneralizedHex, GraphPartitioning, HamiltonianCircuit, HamiltonianPath,
LengthBoundedDisjointPaths, MinimumCutIntoBoundedSets, MinimumMultiwayCut,
Expand Down Expand Up @@ -342,7 +344,7 @@ fn type_format_hint(type_name: &str, graph_type: Option<&str>) -> &'static str {
}
"Vec<Vec<usize>>" => "semicolon-separated sets: \"0,1;1,2;0,2\"",
"Vec<CNFClause>" => "semicolon-separated clauses: \"1,2;-1,3\"",
"Vec<Vec<bool>>" => "semicolon-separated binary rows: \"1,1,0;0,1,1\"",
"Vec<Vec<bool>>" => "JSON 2D bool array: '[[true,false],[false,true]]'",
"Vec<Vec<W>>" => "semicolon-separated rows: \"1,0.5;0.5,2\"",
"usize" | "W::Sum" => "integer",
"u64" => "integer",
Expand Down Expand Up @@ -431,7 +433,7 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str {
"AdditionalKey" => "--num-attributes 6 --dependencies \"0,1:2,3;2,3:4,5;4,5:0,1\" --relation-attrs 0,1,2,3,4,5 --known-keys \"0,1;2,3;4,5\"",
"SubgraphIsomorphism" => "--graph 0-1,1-2,2-0 --pattern 0-1",
"RectilinearPictureCompression" => {
"--matrix \"1,1,0,0;1,1,0,0;0,0,1,1;0,0,1,1\" --k 2"
"--matrix \"1,1,0,0;1,1,0,0;0,0,1,1;0,0,1,1\" --bound 2"
}
"SequencingToMinimizeWeightedTardiness" => {
"--sizes 3,4,2,5,3 --weights 2,3,1,4,2 --deadlines 5,8,4,15,10 --bound 13"
Expand All @@ -449,7 +451,7 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str {
"--strings \"010110;100101;001011\" --bound 3 --alphabet-size 2"
}
"MinimumCardinalityKey" => {
"--num-attributes 6 --dependencies \"0,1>2;0,2>3;1,3>4;2,4>5\" --k 2"
"--num-attributes 6 --dependencies \"0,1>2;0,2>3;1,3>4;2,4>5\" --bound 2"
}
"PrimeAttributeName" => {
"--universe 6 --deps \"0,1>2,3,4,5;2,3>0,1,4,5\" --query 3"
Expand All @@ -458,13 +460,16 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str {
"--alphabet-size 6 --sets \"0,1,2;3,4,5;1,3;2,4;0,5\""
}
"ShortestCommonSupersequence" => "--strings \"0,1,2;1,2,0\" --bound 4",
"SequencingToMinimizeMaximumCumulativeCost" => {
"--costs 2,-1,3,-2,1,-3 --precedence-pairs \"0>2,1>2,1>3,2>4,3>5,4>5\" --bound 4"
"ConsecutiveBlockMinimization" => {
"--matrix '[[true,false,true],[false,true,true]]' --bound 2"
}
"ConjunctiveQueryFoldability" => "(use --example ConjunctiveQueryFoldability)",
"ConjunctiveBooleanQuery" => {
"--domain-size 6 --relations \"2:0,3|1,3|2,4;3:0,1,5|1,2,5\" --conjuncts-spec \"0:v0,c3;0:v1,c3;1:v0,v1,c5\""
}
"ConjunctiveQueryFoldability" => "(use --example ConjunctiveQueryFoldability)",
"SequencingToMinimizeMaximumCumulativeCost" => {
"--costs 2,-1,3,-2,1,-3 --precedence-pairs \"0>2,1>2,1>3,2>4,3>5,4>5\" --bound 4"
}
"StringToStringCorrection" => {
"--source-string \"0,1,2,3,1,0\" --target-string \"0,1,3,2,1\" --bound 2"
}
Expand All @@ -489,12 +494,11 @@ fn help_flag_name(canonical: &str, field_name: &str) -> String {
return "num-processors/--m".to_string();
}
("LengthBoundedDisjointPaths", "max_length") => return "bound".to_string(),
("RectilinearPictureCompression", "bound_k") => return "k".to_string(),
("RectilinearPictureCompression", "bound") => return "bound".to_string(),
("PrimeAttributeName", "num_attributes") => return "universe".to_string(),
("PrimeAttributeName", "dependencies") => return "deps".to_string(),
("PrimeAttributeName", "query_attribute") => return "query".to_string(),
("MinimumCardinalityKey", "bound_k") => return "k".to_string(),
("ConsecutiveOnesSubmatrix", "bound_k") => return "k".to_string(),
("ConsecutiveOnesSubmatrix", "bound") => return "bound".to_string(),
("StaffScheduling", "shifts_per_schedule") => return "k".to_string(),
_ => {}
}
Expand Down Expand Up @@ -1774,12 +1778,12 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> {
"MinimumCardinalityKey" => {
let num_attributes = args.num_attributes.ok_or_else(|| {
anyhow::anyhow!(
"MinimumCardinalityKey requires --num-attributes, --dependencies, and --k\n\n\
Usage: pred create MinimumCardinalityKey --num-attributes 6 --dependencies \"0,1>2;0,2>3;1,3>4;2,4>5\" --k 2"
"MinimumCardinalityKey requires --num-attributes, --dependencies, and --bound\n\n\
Usage: pred create MinimumCardinalityKey --num-attributes 6 --dependencies \"0,1>2;0,2>3;1,3>4;2,4>5\" --bound 2"
)
})?;
let k = args.k.ok_or_else(|| {
anyhow::anyhow!("MinimumCardinalityKey requires --k (bound on key cardinality)")
let k = args.bound.ok_or_else(|| {
anyhow::anyhow!("MinimumCardinalityKey requires --bound (bound on key cardinality)")
})?;
let deps_str = args.dependencies.as_deref().ok_or_else(|| {
anyhow::anyhow!(
Expand Down Expand Up @@ -1853,32 +1857,55 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> {
(ser(BMF::new(matrix, rank))?, resolved_variant.clone())
}

// ConsecutiveBlockMinimization
"ConsecutiveBlockMinimization" => {
let usage = "Usage: pred create ConsecutiveBlockMinimization --matrix '[[true,false,true],[false,true,true]]' --bound 2";
let matrix_str = args.matrix.as_deref().ok_or_else(|| {
anyhow::anyhow!(
"ConsecutiveBlockMinimization requires --matrix as a JSON 2D bool array and --bound\n\n{usage}"
)
})?;
let bound = args.bound.ok_or_else(|| {
anyhow::anyhow!("ConsecutiveBlockMinimization requires --bound\n\n{usage}")
})?;
let matrix: Vec<Vec<bool>> = serde_json::from_str(matrix_str).map_err(|err| {
anyhow::anyhow!(
"ConsecutiveBlockMinimization requires --matrix as a JSON 2D bool array (e.g., '[[true,false,true],[false,true,true]]')\n\n{usage}\n\nFailed to parse --matrix: {err}"
)
})?;
(
ser(ConsecutiveBlockMinimization::try_new(matrix, bound)
.map_err(|err| anyhow::anyhow!("{err}\n\n{usage}"))?)?,
resolved_variant.clone(),
)
}

// RectilinearPictureCompression
"RectilinearPictureCompression" => {
let matrix = parse_bool_matrix(args)?;
let k = args.k.ok_or_else(|| {
let bound = args.bound.ok_or_else(|| {
anyhow::anyhow!(
"RectilinearPictureCompression requires --matrix and --k\n\n\
Usage: pred create RectilinearPictureCompression --matrix \"1,1,0,0;1,1,0,0;0,0,1,1;0,0,1,1\" --k 2"
"RectilinearPictureCompression requires --matrix and --bound\n\n\
Usage: pred create RectilinearPictureCompression --matrix \"1,1,0,0;1,1,0,0;0,0,1,1;0,0,1,1\" --bound 2"
)
})?;
(
ser(RectilinearPictureCompression::new(matrix, k))?,
ser(RectilinearPictureCompression::new(matrix, bound))?,
resolved_variant.clone(),
)
}

// ConsecutiveOnesSubmatrix
"ConsecutiveOnesSubmatrix" => {
let matrix = parse_bool_matrix(args)?;
let k = args.k.ok_or_else(|| {
let bound = args.bound.ok_or_else(|| {
anyhow::anyhow!(
"ConsecutiveOnesSubmatrix requires --matrix and --k\n\n\
Usage: pred create ConsecutiveOnesSubmatrix --matrix \"1,1,0,1;1,0,1,1;0,1,1,0\" --k 3"
"ConsecutiveOnesSubmatrix requires --matrix and --bound\n\n\
Usage: pred create ConsecutiveOnesSubmatrix --matrix \"1,1,0,1;1,0,1,1;0,1,1,0\" --bound 3"
)
})?;
(
ser(ConsecutiveOnesSubmatrix::new(matrix, k))?,
ser(ConsecutiveOnesSubmatrix::new(matrix, bound))?,
resolved_variant.clone(),
)
}
Expand Down
Loading
Loading