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
55 changes: 55 additions & 0 deletions docs/paper/reductions.typ
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@
"MinimumTardinessSequencing": [Minimum Tardiness Sequencing],
"SequencingWithinIntervals": [Sequencing Within Intervals],
"DirectedTwoCommodityIntegralFlow": [Directed Two-Commodity Integral Flow],
"RectilinearPictureCompression": [Rectilinear Picture Compression],
)

// Definition label: "def:<ProblemName>" — each definition block must have a matching label
Expand Down Expand Up @@ -1879,6 +1880,60 @@ NP-completeness was established by Garey, Johnson, and Stockmeyer @gareyJohnsonS
*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.
]

#{
let x = load-model-example("RectilinearPictureCompression")
let mat = x.instance.matrix
let m = mat.len()
let n = mat.at(0).len()
let K = x.instance.bound_k
// Convert bool matrix to int for display
let M = mat.map(row => row.map(v => if v { 1 } else { 0 }))
[
#problem-def("RectilinearPictureCompression")[
Given an $m times n$ binary matrix $M$ and a nonnegative integer $K$,
determine whether there exists a collection of at most $K$
axis-aligned rectangles that covers precisely the 1-entries of $M$.
Each rectangle is a quadruple $(a, b, c, d)$ with $a lt.eq b$ and $c lt.eq d$,
covering entries $M_(i j)$ for $a lt.eq i lt.eq b$ and $c lt.eq j lt.eq d$,
where every covered entry must satisfy $M_(i j) = 1$.
][
Rectilinear Picture Compression is a classical NP-complete problem from Garey & Johnson (A4 SR25, p.~232) @garey1979. It arises naturally in image compression, DNA microarray design, integrated circuit manufacturing, and access control list minimization. NP-completeness was established by Masek (1978) via transformation from 3SAT. A straightforward exact baseline, including the brute-force solver in this crate, enumerates subsets of the maximal all-1 rectangles. If an instance has $R$ such rectangles, this gives an $O^*(2^R)$ exact search, so the worst-case behavior remains exponential in the instance size.

*Example.* Let $M = mat(#M.map(row => row.map(v => str(v)).join(", ")).join("; "))$ (a $#m times #n$ matrix) and $K = #K$. The two maximal all-1 rectangles cover rows $0..1$, columns $0..1$ and rows $2..3$, columns $2..3$. Selecting both gives $|{R_1, R_2}| = 2 lt.eq K = #K$ and their union covers all eight 1-entries, so the answer is YES.

#figure(
{
let cell-size = 0.5
let blue = graph-colors.at(0)
let teal = rgb("#76b7b2")
// Rectangle covers: R1 covers rows 0..1, cols 0..1; R2 covers rows 2..3, cols 2..3
let rect-color(r, c) = {
if r <= 1 and c <= 1 { blue.transparentize(40%) }
else if r >= 2 and c >= 2 { teal.transparentize(40%) }
else { white }
}
align(center, grid(
columns: n,
column-gutter: 0pt,
row-gutter: 0pt,
..range(m).map(r =>
range(n).map(c => {
let val = M.at(r).at(c)
let fill = if val == 1 { rect-color(r, c) } else { white }
box(width: cell-size * 1cm, height: cell-size * 1cm,
fill: fill, stroke: 0.4pt + luma(180),
align(center + horizon, text(8pt, weight: if val == 1 { "bold" } else { "regular" },
if val == 1 { "1" } else { "0" })))
})
).flatten(),
))
},
caption: [Rectilinear Picture Compression: matrix $M$ covered by two rectangles $R_1$ (blue, top-left $2 times 2$) and $R_2$ (teal, bottom-right $2 times 2$), with $|{R_1, R_2}| = 2 lt.eq K = #K$.],
) <fig:rpc>
]
]
}

#problem-def("RuralPostman")[
Given an undirected graph $G = (V, E)$ with edge lengths $l: E -> ZZ_(gt.eq 0)$, a subset $E' subset.eq E$ of required edges, and a bound $B in ZZ^+$, determine whether there exists a circuit (closed walk) in $G$ that traverses every edge in $E'$ and has total length at most $B$.
][
Expand Down
2 changes: 2 additions & 0 deletions docs/src/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,8 @@ pred create QUBO --matrix "1,0.5;0.5,2" -o qubo.json
pred create KColoring --k 3 --graph 0-1,1-2,2-0 -o kcol.json
pred create SpinGlass --graph 0-1,1-2 -o sg.json
pred create MaxCut --graph 0-1,1-2,2-0 -o maxcut.json
pred create RectilinearPictureCompression --matrix "1,1,0,0;1,1,0,0;0,0,1,1;0,0,1,1" --k 2 -o rpc.json
pred solve rpc.json --solver brute-force
pred create SteinerTree --graph 0-1,0-3,1-2,1-3,2-3,2-4,3-4 --edge-weights 2,5,2,1,5,6,1 --terminals 0,2,4 -o steiner.json
pred create UndirectedTwoCommodityIntegralFlow --graph 0-2,1-2,2-3 --capacities 1,1,2 --source-1 0 --sink-1 3 --source-2 1 --sink-2 3 --requirement-1 1 --requirement-2 1 -o utcif.json
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
Expand Down
5 changes: 3 additions & 2 deletions problemreductions-cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,7 @@ Flags by problem type:
FVS --arcs [--weights] [--num-vertices]
FlowShopScheduling --task-lengths, --deadline [--num-processors]
MinimumTardinessSequencing --n, --deadlines [--precedence-pairs]
RectilinearPictureCompression --matrix (0/1), --k
SCS --strings, --bound [--alphabet-size]
D2CIF --arcs, --capacities, --source-1, --sink-1, --source-2, --sink-2, --requirement-1, --requirement-2
ILP, CircuitSAT (via reduction only)
Expand Down Expand Up @@ -321,10 +322,10 @@ pub struct CreateArgs {
/// Number of variables (for SAT/KSAT)
#[arg(long)]
pub num_vars: Option<usize>,
/// Matrix for QUBO (semicolon-separated rows, e.g., "1,0.5;0.5,2")
/// Matrix input (semicolon-separated rows; use `pred create <PROBLEM>` for problem-specific formats)
#[arg(long)]
pub matrix: Option<String>,
/// Number of colors for KColoring
/// Shared integer parameter (use `pred create <PROBLEM>` for the problem-specific meaning)
#[arg(long)]
pub k: Option<usize>,
/// Generate a random instance (graph-based problems only)
Expand Down
49 changes: 35 additions & 14 deletions problemreductions-cli/src/commands/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ use problemreductions::models::graph::{
};
use problemreductions::models::misc::{
BinPacking, FlowShopScheduling, LongestCommonSubsequence, MinimumTardinessSequencing,
PaintShop, SequencingWithinIntervals, ShortestCommonSupersequence, SubsetSum,
PaintShop, RectilinearPictureCompression, SequencingWithinIntervals,
ShortestCommonSupersequence, SubsetSum,
};
use problemreductions::prelude::*;
use problemreductions::registry::collect_schemas;
Expand Down Expand Up @@ -233,7 +234,6 @@ fn type_format_hint(type_name: &str, graph_type: Option<&str>) -> &'static str {
"Vec<Vec<usize>>" => "semicolon-separated groups: \"0,1;2,3\"",
"usize" => "integer",
"u64" => "integer",
"Vec<u64>" => "comma-separated integers: 0,0,5",
"i64" => "integer",
"BigUint" => "nonnegative decimal integer",
"Vec<BigUint>" => "comma-separated nonnegative decimal integers: 3,7,1,8",
Expand Down Expand Up @@ -293,6 +293,9 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str {
"--arcs \"0>1,0>2,1>3,2>3,1>4,3>5,4>5,2>4\" --weights 3,2,4,1,2,3,1,3 --partition \"0,1;2,3;4,7;5,6\" --bound 10"
}
"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"
}
"SubsetSum" => "--sizes 3,7,1,8,2,4 --target 11",
"SetBasis" => "--universe 4 --sets \"0,1;1,2;0,2;0,1,2\" --k 3",
"ShortestCommonSupersequence" => "--strings \"0,1,2;1,2,0\" --bound 4",
Expand All @@ -305,6 +308,8 @@ fn help_flag_name(canonical: &str, field_name: &str) -> String {
match (canonical, field_name) {
("BoundedComponentSpanningForest", "max_components") => return "k".to_string(),
("BoundedComponentSpanningForest", "max_weight") => return "bound".to_string(),
("LengthBoundedDisjointPaths", "max_length") => return "bound".to_string(),
("RectilinearPictureCompression", "bound_k") => return "k".to_string(),
_ => {}
}
// General field-name overrides (previously in cli_flag_name)
Expand Down Expand Up @@ -367,12 +372,7 @@ fn print_problem_help(canonical: &str, graph_type: Option<&str>) -> Result<()> {
eprintln!(" --{:<16} {} ({})", flag_name, field.description, hint);
} else {
let hint = help_flag_hint(canonical, &field.name, &field.type_name, graph_type);
eprintln!(
" --{:<16} {} ({})",
help_flag_name(canonical, &field.name),
field.description,
hint
);
eprintln!(" --{:<16} {} ({})", flag_name, field.description, hint);
}
}
} else {
Expand Down Expand Up @@ -406,10 +406,7 @@ fn problem_help_flag_name(
if field_type == "DirectedGraph" {
return "arcs".to_string();
}
if canonical == "LengthBoundedDisjointPaths" && field_name == "max_length" {
return "bound".to_string();
}
field_name.replace('_', "-")
help_flag_name(canonical, field_name)
}

fn lbdp_validation_error(message: &str, usage: Option<&str>) -> anyhow::Error {
Expand Down Expand Up @@ -1107,6 +1104,21 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> {
(ser(BMF::new(matrix, rank))?, resolved_variant.clone())
}

// RectilinearPictureCompression
"RectilinearPictureCompression" => {
let matrix = parse_bool_matrix(args)?;
let k = args.k.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"
)
})?;
(
ser(RectilinearPictureCompression::new(matrix, k))?,
resolved_variant.clone(),
)
}

// LongestCommonSubsequence
"LongestCommonSubsequence" => {
let strings_str = args.strings.as_deref().ok_or_else(|| {
Expand Down Expand Up @@ -2052,7 +2064,7 @@ fn parse_bool_matrix(args: &CreateArgs) -> Result<Vec<Vec<bool>>> {
.matrix
.as_deref()
.ok_or_else(|| anyhow::anyhow!("This problem requires --matrix (e.g., \"1,0;0,1;1,1\")"))?;
matrix_str
let matrix: Vec<Vec<bool>> = matrix_str
.split(';')
.map(|row| {
row.trim()
Expand All @@ -2067,7 +2079,16 @@ fn parse_bool_matrix(args: &CreateArgs) -> Result<Vec<Vec<bool>>> {
})
.collect()
})
.collect()
.collect::<Result<_>>()?;

if let Some(expected_width) = matrix.first().map(Vec::len) {
anyhow::ensure!(
matrix.iter().all(|row| row.len() == expected_width),
"All rows in --matrix must have the same length"
);
}

Ok(matrix)
}

/// Parse `--matrix` as semicolon-separated rows of comma-separated f64 values.
Expand Down
9 changes: 7 additions & 2 deletions problemreductions-cli/src/commands/inspect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,12 @@ fn inspect_problem(pj: &ProblemJson, out: &OutputConfig) -> Result<()> {
text.push_str(&format!("Variables: {}\n", problem.num_variables_dyn()));

// Solvers
text.push_str("Solvers: ilp (default), brute-force\n");
let solvers = problem.available_solvers();
if solvers.first() == Some(&"ilp") {
text.push_str("Solvers: ilp (default), brute-force\n");
} else {
text.push_str("Solvers: brute-force\n");
}

// Reductions
let outgoing = graph.outgoing_reductions(name);
Expand All @@ -56,7 +61,7 @@ fn inspect_problem(pj: &ProblemJson, out: &OutputConfig) -> Result<()> {
"variant": variant,
"size_fields": size_fields,
"num_variables": problem.num_variables_dyn(),
"solvers": ["ilp", "brute-force"],
"solvers": solvers,
"reduces_to": targets,
});

Expand Down
32 changes: 27 additions & 5 deletions problemreductions-cli/src/dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,12 @@ impl LoadedProblem {
Ok(SolveResult { config, evaluation })
}

/// Solve using the ILP solver. If the problem is not ILP, auto-reduce to ILP first.
pub fn solve_with_ilp(&self) -> Result<SolveResult> {
fn best_ilp_path(&self) -> Option<problemreductions::rules::ReductionPath> {
let name = self.problem_name();
if name == "ILP" {
return solve_ilp(self.as_any());
return Some(problemreductions::rules::ReductionPath { steps: Vec::new() });
}

// Auto-reduce to ILP, solve, and map solution back
let source_variant = self.variant_map();
let graph = ReductionGraph::new();
let ilp_variants = graph.variants_for("ILP");
Expand All @@ -79,13 +77,37 @@ impl LoadedProblem {
}
}

let reduction_path = best_path.ok_or_else(|| {
best_path
}

pub fn supports_ilp_solver(&self) -> bool {
self.best_ilp_path().is_some()
}

pub fn available_solvers(&self) -> Vec<&'static str> {
if self.supports_ilp_solver() {
vec!["ilp", "brute-force"]
} else {
vec!["brute-force"]
}
}

/// Solve using the ILP solver. If the problem is not ILP, auto-reduce to ILP first.
pub fn solve_with_ilp(&self) -> Result<SolveResult> {
let name = self.problem_name();
if name == "ILP" {
return solve_ilp(self.as_any());
}

// Auto-reduce to ILP, solve, and map solution back
let reduction_path = self.best_ilp_path().ok_or_else(|| {
anyhow::anyhow!(
"No reduction path from {} to ILP. Try `--solver brute-force`, or reduce to a problem that supports ILP.",
name
)
})?;

let graph = ReductionGraph::new();
let chain = graph
.reduce_along_path(&reduction_path, self.as_any())
.ok_or_else(|| anyhow::anyhow!("Failed to execute reduction chain to ILP"))?;
Expand Down
Loading
Loading