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
55 changes: 55 additions & 0 deletions docs/paper/reductions.typ
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@
"SumOfSquaresPartition": [Sum of Squares Partition],
"SequencingWithinIntervals": [Sequencing Within Intervals],
"DirectedTwoCommodityIntegralFlow": [Directed Two-Commodity Integral Flow],
"RectilinearPictureCompression": [Rectilinear Picture Compression],
"StringToStringCorrection": [String-to-String Correction],
)

Expand Down Expand Up @@ -2476,6 +2477,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 @@ -346,6 +346,8 @@ 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
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 MinimumMultiwayCut --graph 0-1,1-2,2-3,3-0 --terminals 0,2 --edge-weights 3,1,2,4 -o mmc.json
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
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 @@ -264,6 +264,7 @@ Flags by problem type:
FlowShopScheduling --task-lengths, --deadline [--num-processors]
StaffScheduling --schedules, --requirements, --num-workers, --k
MinimumTardinessSequencing --n, --deadlines [--precedence-pairs]
RectilinearPictureCompression --matrix (0/1), --k
SCS --strings, --bound [--alphabet-size]
StringToStringCorrection --source-string, --target-string, --bound [--alphabet-size]
D2CIF --arcs, --capacities, --source-1, --sink-1, --source-2, --sink-2, --requirement-1, --requirement-2
Expand Down Expand Up @@ -342,10 +343,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
45 changes: 39 additions & 6 deletions problemreductions-cli/src/commands/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ use problemreductions::models::graph::{
};
use problemreductions::models::misc::{
BinPacking, FlowShopScheduling, LongestCommonSubsequence, MinimumTardinessSequencing,
MultiprocessorScheduling, PaintShop, SequencingWithReleaseTimesAndDeadlines,
SequencingWithinIntervals, ShortestCommonSupersequence, StringToStringCorrection, SubsetSum,
SumOfSquaresPartition,
MultiprocessorScheduling, PaintShop, RectilinearPictureCompression,
SequencingWithReleaseTimesAndDeadlines, SequencingWithinIntervals, ShortestCommonSupersequence,
StringToStringCorrection, SubsetSum, SumOfSquaresPartition,
};
use problemreductions::models::BiconnectivityAugmentation;
use problemreductions::prelude::*;
Expand Down Expand Up @@ -335,6 +335,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",
"SumOfSquaresPartition" => "--sizes 5,3,8,2,7,1 --num-groups 3 --bound 240",
"ComparativeContainment" => {
Expand Down Expand Up @@ -370,6 +373,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(),
("PrimeAttributeName", "num_attributes") => return "universe".to_string(),
("PrimeAttributeName", "dependencies") => return "deps".to_string(),
("PrimeAttributeName", "query_attribute") => return "query".to_string(),
Expand Down Expand Up @@ -1468,6 +1473,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(),
)
}

// ConsecutiveOnesSubmatrix
"ConsecutiveOnesSubmatrix" => {
let matrix = parse_bool_matrix(args)?;
Expand Down Expand Up @@ -2942,7 +2962,7 @@ fn parse_schedules(args: &CreateArgs, usage: &str) -> Result<Vec<Vec<bool>>> {
}

fn parse_bool_rows(rows_str: &str) -> Result<Vec<Vec<bool>>> {
rows_str
let matrix: Vec<Vec<bool>> = rows_str
.split(';')
.map(|row| {
row.trim()
Expand All @@ -2956,7 +2976,16 @@ fn parse_bool_rows(rows_str: &str) -> 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)
}

fn parse_requirements(args: &CreateArgs, usage: &str) -> Result<Vec<u64>> {
Expand Down Expand Up @@ -3692,7 +3721,11 @@ mod tests {
let result = std::panic::catch_unwind(|| create(&args, &out));
assert!(result.is_ok(), "create should return an error, not panic");
let err = result.unwrap().unwrap_err().to_string();
assert!(err.contains("schedule 1 has 6 periods, expected 7"));
// parse_bool_rows catches ragged rows before validate_staff_scheduling_args
assert!(
err.contains("All rows") || err.contains("schedule 1 has 6 periods, expected 7"),
"expected row-length validation error, got: {err}"
);
}

fn empty_args() -> CreateArgs {
Expand Down
129 changes: 129 additions & 0 deletions problemreductions-cli/tests/cli_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1084,6 +1084,47 @@ fn test_solve_d2cif_default_solver_suggests_bruteforce() {
std::fs::remove_file(&output_file).ok();
}

#[test]
fn test_inspect_rectilinear_picture_compression_lists_bruteforce_only() {
let output_file = std::env::temp_dir().join("pred_test_inspect_rpc.json");
let create_output = pred()
.args([
"-o",
output_file.to_str().unwrap(),
"create",
"RectilinearPictureCompression",
"--matrix",
"1,1;1,1",
"--k",
"1",
])
.output()
.unwrap();
assert!(
create_output.status.success(),
"stderr: {}",
String::from_utf8_lossy(&create_output.stderr)
);

let inspect_output = pred()
.args(["inspect", output_file.to_str().unwrap()])
.output()
.unwrap();
assert!(
inspect_output.status.success(),
"stderr: {}",
String::from_utf8_lossy(&inspect_output.stderr)
);
let stdout = String::from_utf8(inspect_output.stdout).unwrap();
let json: serde_json::Value = serde_json::from_str(&stdout).unwrap();
assert!(
json["solvers"] == serde_json::json!(["brute-force"]),
"inspect should list only usable solvers, got: {json}"
);

std::fs::remove_file(&output_file).ok();
}

#[test]
fn test_create_x3c_rejects_duplicate_subset_elements() {
let output = pred()
Expand Down Expand Up @@ -2860,6 +2901,94 @@ fn test_create_set_basis_no_flags_uses_actual_cli_flag_names() {
);
}

#[test]
fn test_create_rectilinear_picture_compression_help_uses_k_flag() {
let output = pred()
.args(["create", "RectilinearPictureCompression"])
.output()
.unwrap();
assert!(!output.status.success());
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("--matrix"),
"expected '--matrix' in help output, got: {stderr}"
);
assert!(
stderr.contains("--k"),
"expected '--k' in help output, got: {stderr}"
);
assert!(
!stderr.contains("--bound-k"),
"help should advertise the actual CLI flag name, got: {stderr}"
);
}

#[test]
fn test_create_rectilinear_picture_compression_rejects_ragged_matrix() {
let output = pred()
.args([
"create",
"RectilinearPictureCompression",
"--matrix",
"1,0;1",
"--k",
"1",
])
.output()
.unwrap();
assert!(!output.status.success());
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("All rows in --matrix must have the same length"),
"expected rectangular-matrix validation error, got: {stderr}"
);
assert!(
!stderr.contains("panicked at"),
"ragged matrix should not crash the CLI, got: {stderr}"
);
}

#[test]
fn test_create_help_uses_generic_matrix_and_k_descriptions() {
let output = pred().args(["create", "--help"]).output().unwrap();
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("Matrix input"),
"expected generic matrix help, got: {stdout}"
);
assert!(
stdout.contains("Shared integer parameter"),
"expected generic k help, got: {stdout}"
);
assert!(
!stdout.contains("Matrix for QUBO"),
"create --help should not imply --matrix is QUBO-only, got: {stdout}"
);
assert!(
!stdout.contains("Number of colors for KColoring"),
"create --help should not imply --k is KColoring-only, got: {stdout}"
);
}

#[test]
fn test_create_length_bounded_disjoint_paths_help_uses_bound_flag() {
let output = pred()
.args(["create", "LengthBoundedDisjointPaths"])
.output()
.unwrap();
assert!(!output.status.success());
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("--bound"),
"expected '--bound' in help output, got: {stderr}"
);
assert!(
!stderr.contains("--max-length"),
"help should advertise the actual CLI flag name, got: {stderr}"
);
}

#[test]
fn test_create_consecutive_ones_submatrix_no_flags_uses_actual_cli_help() {
let output = pred()
Expand Down
9 changes: 6 additions & 3 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,9 @@ pub mod prelude {
pub use crate::models::misc::{
BinPacking, Factoring, FlowShopScheduling, Knapsack, LongestCommonSubsequence,
MinimumTardinessSequencing, MultiprocessorScheduling, PaintShop,
SequencingWithReleaseTimesAndDeadlines, SequencingWithinIntervals,
ShortestCommonSupersequence, StaffScheduling, StringToStringCorrection, SubsetSum,
SumOfSquaresPartition,
RectilinearPictureCompression, SequencingWithReleaseTimesAndDeadlines,
SequencingWithinIntervals, ShortestCommonSupersequence, StaffScheduling,
StringToStringCorrection, SubsetSum, SumOfSquaresPartition,
};
pub use crate::models::set::{
ComparativeContainment, ConsecutiveSets, ExactCoverBy3Sets, MaximumSetPacking,
Expand Down Expand Up @@ -102,6 +102,9 @@ pub use inventory;
#[path = "unit_tests/graph_models.rs"]
mod test_graph_models;
#[cfg(test)]
#[path = "unit_tests/prelude.rs"]
mod test_prelude;
#[cfg(test)]
#[path = "unit_tests/property.rs"]
mod test_property;
#[cfg(test)]
Expand Down
4 changes: 4 additions & 0 deletions src/models/misc/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
//! - [`MinimumTardinessSequencing`]: Minimize tardy tasks in single-machine scheduling
//! - [`PaintShop`]: Minimize color switches in paint shop scheduling
//! - [`PrecedenceConstrainedScheduling`]: Schedule unit tasks on processors by deadline
//! - [`RectilinearPictureCompression`]: Cover 1-entries with bounded rectangles
//! - [`SequencingWithReleaseTimesAndDeadlines`]: Single-machine scheduling feasibility
//! - [`SequencingWithinIntervals`]: Schedule tasks within time windows
//! - [`ShortestCommonSupersequence`]: Find a common supersequence of bounded length
Expand All @@ -26,6 +27,7 @@ mod minimum_tardiness_sequencing;
mod multiprocessor_scheduling;
pub(crate) mod paintshop;
mod precedence_constrained_scheduling;
mod rectilinear_picture_compression;
mod sequencing_with_release_times_and_deadlines;
mod sequencing_within_intervals;
pub(crate) mod shortest_common_supersequence;
Expand All @@ -43,6 +45,7 @@ pub use minimum_tardiness_sequencing::MinimumTardinessSequencing;
pub use multiprocessor_scheduling::MultiprocessorScheduling;
pub use paintshop::PaintShop;
pub use precedence_constrained_scheduling::PrecedenceConstrainedScheduling;
pub use rectilinear_picture_compression::RectilinearPictureCompression;
pub use sequencing_with_release_times_and_deadlines::SequencingWithReleaseTimesAndDeadlines;
pub use sequencing_within_intervals::SequencingWithinIntervals;
pub use shortest_common_supersequence::ShortestCommonSupersequence;
Expand All @@ -58,6 +61,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::M
specs.extend(longest_common_subsequence::canonical_model_example_specs());
specs.extend(multiprocessor_scheduling::canonical_model_example_specs());
specs.extend(paintshop::canonical_model_example_specs());
specs.extend(rectilinear_picture_compression::canonical_model_example_specs());
specs.extend(sequencing_within_intervals::canonical_model_example_specs());
specs.extend(staff_scheduling::canonical_model_example_specs());
specs.extend(shortest_common_supersequence::canonical_model_example_specs());
Expand Down
Loading
Loading