From fbc34859ea65e45cab0719df46a25e70238c5956 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Mon, 16 Mar 2026 23:57:43 +0800 Subject: [PATCH 1/8] Add plan for #443: RectilinearPictureCompression --- ...6-03-16-rectilinear-picture-compression.md | 223 ++++++++++++++++++ 1 file changed, 223 insertions(+) create mode 100644 docs/plans/2026-03-16-rectilinear-picture-compression.md diff --git a/docs/plans/2026-03-16-rectilinear-picture-compression.md b/docs/plans/2026-03-16-rectilinear-picture-compression.md new file mode 100644 index 000000000..01ebd449a --- /dev/null +++ b/docs/plans/2026-03-16-rectilinear-picture-compression.md @@ -0,0 +1,223 @@ +# Plan: Add RectilinearPictureCompression Model + +Fixes #443 + +## Information Checklist + +| # | Item | Value | +|---|------|-------| +| 1 | Problem name | `RectilinearPictureCompression` | +| 2 | Mathematical definition | Given an m×n binary matrix M and positive integer K, is there a collection of K or fewer axis-aligned all-1 rectangles that covers precisely the 1-entries of M? | +| 3 | Problem type | Satisfaction (`Metric = bool`) | +| 4 | Type parameters | None | +| 5 | Struct fields | `matrix: Vec>`, `bound_k: usize` | +| 6 | Configuration space | `vec![2; R]` where R = number of maximal all-1 rectangles in the matrix | +| 7 | Feasibility check | Union of selected rectangles covers exactly all 1-entries, and count ≤ K | +| 8 | Objective function | N/A (satisfaction) — returns `true` iff feasible | +| 9 | Best known exact algorithm | Brute-force: O(2^(num_rows * num_cols)) — no significantly better algorithm known (Masek, 1978) | +| 10 | Solving strategy | BruteForce works (enumerate subsets of maximal all-1 rectangles) | +| 11 | Category | `misc` (binary matrix input — unique structure) | +| 12 | Expected outcome | Instance 1 (6×6, K=3): YES — 3 rectangles cover all 1-entries. Instance 2 (same, K=2): NO | + +Associated rule: #458 [Rule] 3SAT to Rectilinear Picture Compression (not an orphan). + +## Batch 1: Implementation (Steps 1–5.5) + +All steps in this batch are independent and can be parallelized within a single subagent. + +### Step 1: Category & file +- Category: `misc/` +- File: `src/models/misc/rectilinear_picture_compression.rs` + +### Step 1.5: Size getters +- `num_rows()` → `self.matrix.len()` +- `num_cols()` → `self.matrix.first().map_or(0, |r| r.len())` +- `bound_k()` → `self.bound_k` + +### Step 2: Implement the model + +Create `src/models/misc/rectilinear_picture_compression.rs`: + +**Struct:** +```rust +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RectilinearPictureCompression { + matrix: Vec>, + bound_k: usize, +} +``` + +**Constructor & getters:** +- `new(matrix, bound_k)` — validate all rows have equal length +- `matrix()`, `bound_k()`, `num_rows()`, `num_cols()` + +**Core helper — `maximal_rectangles(&self) -> Vec<(usize, usize, usize, usize)>`:** + +Enumerate all maximal all-1 sub-rectangles `(r1, r2, c1, c2)` where `r1..=r2` are row bounds and `c1..=c2` are column bounds. Algorithm: +1. Enumerate all all-1 rectangles by iterating over starting positions `(r1, c1)` and extending rows while narrowing the column range +2. Collect into a set +3. Filter to keep only maximal ones (no proper superset in the set) +4. Sort lexicographically for deterministic ordering + +Since candidates are all-1 by construction, they can never cover a 0-entry. This simplifies evaluate(). + +**Problem trait:** +```rust +impl Problem for RectilinearPictureCompression { + const NAME: &'static str = "RectilinearPictureCompression"; + type Metric = bool; + fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] } + fn dims(&self) -> Vec { vec![2; self.maximal_rectangles().len()] } + fn evaluate(&self, config: &[usize]) -> bool { + let candidates = self.maximal_rectangles(); + if config.len() != candidates.len() { return false; } + // Collect selected rectangles (config[i] == 1) + // Check: count of selected ≤ bound_k + // Check: union of selected covers ALL 1-entries in matrix + // (No need to check 0-coverage — candidates are all-1 by construction) + } +} +impl SatisfactionProblem for RectilinearPictureCompression {} +``` + +**ProblemSchemaEntry:** +```rust +inventory::submit! { + ProblemSchemaEntry { + name: "RectilinearPictureCompression", + display_name: "Rectilinear Picture Compression", + aliases: &[], + dimensions: &[], + module_path: module_path!(), + description: "Can a binary matrix be covered by K or fewer axis-aligned all-1 rectangles?", + fields: &[ + FieldInfo { name: "matrix", type_name: "Vec>", description: "Binary matrix" }, + FieldInfo { name: "bound_k", type_name: "usize", description: "Maximum number of rectangles" }, + ], + } +} +``` + +### Step 2.5: Variant complexity + +```rust +crate::declare_variants! { + default sat RectilinearPictureCompression => "2^(num_rows * num_cols)", +} +``` + +Conservative bound: number of maximal all-1 rectangles R ≤ num_rows * num_cols in worst case. + +### Step 3: Register the model + +1. `src/models/misc/mod.rs` — add `pub(crate) mod rectilinear_picture_compression;` and `pub use rectilinear_picture_compression::RectilinearPictureCompression;`. Also add to `canonical_model_example_specs()` chain. +2. `src/models/mod.rs` — add `RectilinearPictureCompression` to the misc re-export line. + +### Step 4: CLI discovery + +`problemreductions-cli/src/problem_name.rs`: +- Add `"rectilinearpicturecompression" => "RectilinearPictureCompression"` to `resolve_alias()` +- No short alias (none established in literature) + +### Step 4.5: CLI creation support + +`problemreductions-cli/src/commands/create.rs`: +- Add match arm for `"RectilinearPictureCompression"` using existing `parse_bool_matrix(args)` and `args.k` (reuse `--k` flag for bound_k) +- Add to `example_command_line()`: + ``` + "RectilinearPictureCompression" => "--matrix '1,1,0;0,1,1' --k 2", + ``` + +`problemreductions-cli/src/cli.rs`: +- Add `RectilinearPictureCompression` to the help table under "Flags by problem type": + ``` + RectilinearPictureCompression --matrix, --k + ``` + +### Step 4.6: Canonical model example + +In the model file, add (feature-gated under `example-db`): +```rust +#[cfg(feature = "example-db")] +pub(crate) fn canonical_model_example_specs() -> Vec { + vec![crate::example_db::specs::ModelExampleSpec { + id: "rectilinear_picture_compression", + build: || { + // 4x4 matrix with two disjoint all-1 blocks (small enough for brute force) + let matrix = vec![ + vec![true, true, false, false], + vec![true, true, false, false], + vec![false, false, true, true], + vec![false, false, true, true], + ]; + let problem = RectilinearPictureCompression::new(matrix, 2); + crate::example_db::specs::satisfaction_example( + problem, + vec![], // samples filled by brute force + ) + }, + }] +} +``` + +### Step 5: Unit tests + +Create `src/unit_tests/models/misc/rectilinear_picture_compression.rs`. + +Link from model file: `#[cfg(test)] #[path = "../../unit_tests/models/misc/rectilinear_picture_compression.rs"] mod tests;` + +Required tests: +- `test_rectilinear_picture_compression_basic` — construct 6×6 instance from issue, verify `num_rows()`, `num_cols()`, `bound_k()`, `dims()` length, NAME, variant() +- `test_rectilinear_picture_compression_evaluation` — verify satisfying config (3 correct rectangles selected) returns true; verify unsatisfying configs (wrong selection, too many) return false +- `test_rectilinear_picture_compression_no_solution` — same matrix with bound_k=2, verify no satisfying config exists +- `test_rectilinear_picture_compression_serialization` — serde JSON roundtrip +- `test_rectilinear_picture_compression_solver` — BruteForce::find_satisfying() returns Some for K=3, None for K=2 +- `test_rectilinear_picture_compression_paper_example` — use the paper example instance, verify expected outcome, verify find_all_satisfying() count +- `test_rectilinear_picture_compression_edge_cases` — empty matrix, all-zeros matrix, single cell + +### Step 5.5: trait_consistency + +`src/unit_tests/trait_consistency.rs`: +- Add `check_problem_trait(RectilinearPictureCompression::new(...))` call in `test_all_problems_implement_trait_correctly` + +## Batch 2: Paper Entry (Step 6) + +Depends on Batch 1 completion (needs `make paper` exports). + +### Step 6: Document in paper + +`docs/paper/reductions.typ`: + +**6a. Display name:** +```typst +"RectilinearPictureCompression": [Rectilinear Picture Compression], +``` + +**6b. Problem definition:** +```typst +#problem-def("RectilinearPictureCompression")[ + Given an $m times n$ binary matrix $M$ and a positive 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 $(a, b, c, d)$ with $a <= b$ and $c <= d$ covers + entries $M_(i j)$ for $a <= i <= b$ and $c <= j <= d$, and every + covered entry must satisfy $M_(i j) = 1$. +][ + ... body ... +] +``` + +**6c. Body:** +- Background: classical NP-complete problem (Garey & Johnson A4 SR25). Applications in image compression, DNA array synthesis, IC manufacturing, ACL minimization. NP-completeness via 3SAT (Masek, 1978). +- Best known algorithms: no algorithm significantly improving on brute-force is known; cite Masek 1978 and Applegate et al. 2007. +- Example: CeTZ diagram showing a small binary matrix (e.g., 4×4 with two blocks) with rectangles highlighted in color. Show the covering solution. +- Evaluation: show that selected rectangles cover all 1-entries. + +**6d. Build:** `make paper` — must compile without errors. + +## Reference files +- Satisfaction model pattern: `src/models/misc/subset_sum.rs` +- Model tests: `src/unit_tests/models/misc/subset_sum.rs` +- CLI create: `problemreductions-cli/src/commands/create.rs` (see BMF for bool matrix pattern) +- Example specs: `src/example_db/specs.rs` (satisfaction_example helper) +- Trait consistency: `src/unit_tests/trait_consistency.rs` From 24a5f31e8a37e07df512a7ce357078e9403961c6 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Tue, 17 Mar 2026 00:13:46 +0800 Subject: [PATCH 2/8] Add RectilinearPictureCompression satisfaction problem model Implement the Rectilinear Picture Compression problem: given an m x n binary matrix M and bound K, determine if at most K axis-aligned all-1 rectangles can cover precisely the 1-entries of M. Includes model file, unit tests, CLI create support, canonical example, trait consistency check, and regenerated fixtures. Co-Authored-By: Claude Opus 4.6 --- docs/src/reductions/problem_schemas.json | 16 ++ docs/src/reductions/reduction_graph.json | 39 +-- problemreductions-cli/src/cli.rs | 1 + problemreductions-cli/src/commands/create.rs | 20 +- src/example_db/fixtures/examples.json | 1 + src/models/misc/mod.rs | 4 + .../misc/rectilinear_picture_compression.rs | 255 ++++++++++++++++++ src/models/mod.rs | 3 +- .../misc/rectilinear_picture_compression.rs | 229 ++++++++++++++++ src/unit_tests/trait_consistency.rs | 4 + 10 files changed, 554 insertions(+), 18 deletions(-) create mode 100644 src/models/misc/rectilinear_picture_compression.rs create mode 100644 src/unit_tests/models/misc/rectilinear_picture_compression.rs diff --git a/docs/src/reductions/problem_schemas.json b/docs/src/reductions/problem_schemas.json index a69a12756..ee0f0257b 100644 --- a/docs/src/reductions/problem_schemas.json +++ b/docs/src/reductions/problem_schemas.json @@ -629,6 +629,22 @@ } ] }, + { + "name": "RectilinearPictureCompression", + "description": "Cover all 1-entries of a binary matrix with at most K axis-aligned all-1 rectangles", + "fields": [ + { + "name": "matrix", + "type_name": "Vec>", + "description": "m x n binary matrix" + }, + { + "name": "bound_k", + "type_name": "usize", + "description": "Maximum number of rectangles allowed" + } + ] + }, { "name": "RuralPostman", "description": "Find a circuit covering required edges with total length at most B (Rural Postman Problem)", diff --git a/docs/src/reductions/reduction_graph.json b/docs/src/reductions/reduction_graph.json index 5429b350f..b9dde74b8 100644 --- a/docs/src/reductions/reduction_graph.json +++ b/docs/src/reductions/reduction_graph.json @@ -463,6 +463,13 @@ "doc_path": "models/algebraic/struct.QUBO.html", "complexity": "2^num_vars" }, + { + "name": "RectilinearPictureCompression", + "variant": {}, + "category": "misc", + "doc_path": "models/misc/struct.RectilinearPictureCompression.html", + "complexity": "2^(num_rows * num_cols)" + }, { "name": "RuralPostman", "variant": { @@ -599,7 +606,7 @@ }, { "source": 4, - "target": 57, + "target": 58, "overhead": [ { "field": "num_spins", @@ -763,7 +770,7 @@ }, { "source": 22, - "target": 61, + "target": 62, "overhead": [ { "field": "num_elements", @@ -774,7 +781,7 @@ }, { "source": 23, - "target": 53, + "target": 54, "overhead": [ { "field": "num_clauses", @@ -819,7 +826,7 @@ }, { "source": 27, - "target": 57, + "target": 58, "overhead": [ { "field": "num_spins", @@ -1265,7 +1272,7 @@ }, { "source": 51, - "target": 56, + "target": 57, "overhead": [ { "field": "num_spins", @@ -1275,7 +1282,7 @@ "doc_path": "rules/spinglass_qubo/index.html" }, { - "source": 53, + "source": 54, "target": 4, "overhead": [ { @@ -1290,7 +1297,7 @@ "doc_path": "rules/sat_circuitsat/index.html" }, { - "source": 53, + "source": 54, "target": 17, "overhead": [ { @@ -1305,7 +1312,7 @@ "doc_path": "rules/sat_coloring/index.html" }, { - "source": 53, + "source": 54, "target": 22, "overhead": [ { @@ -1320,7 +1327,7 @@ "doc_path": "rules/sat_ksat/index.html" }, { - "source": 53, + "source": 54, "target": 32, "overhead": [ { @@ -1335,7 +1342,7 @@ "doc_path": "rules/sat_maximumindependentset/index.html" }, { - "source": 53, + "source": 54, "target": 41, "overhead": [ { @@ -1350,7 +1357,7 @@ "doc_path": "rules/sat_minimumdominatingset/index.html" }, { - "source": 56, + "source": 57, "target": 51, "overhead": [ { @@ -1361,7 +1368,7 @@ "doc_path": "rules/spinglass_qubo/index.html" }, { - "source": 57, + "source": 58, "target": 27, "overhead": [ { @@ -1376,8 +1383,8 @@ "doc_path": "rules/spinglass_maxcut/index.html" }, { - "source": 57, - "target": 56, + "source": 58, + "target": 57, "overhead": [ { "field": "num_spins", @@ -1391,7 +1398,7 @@ "doc_path": "rules/spinglass_casts/index.html" }, { - "source": 62, + "source": 63, "target": 13, "overhead": [ { @@ -1406,7 +1413,7 @@ "doc_path": "rules/travelingsalesman_ilp/index.html" }, { - "source": 62, + "source": 63, "target": 51, "overhead": [ { diff --git a/problemreductions-cli/src/cli.rs b/problemreductions-cli/src/cli.rs index 32d58428c..d1cf0c9e9 100644 --- a/problemreductions-cli/src/cli.rs +++ b/problemreductions-cli/src/cli.rs @@ -246,6 +246,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) diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index dc1aaca64..5fef64aee 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -11,7 +11,7 @@ use problemreductions::models::graph::{ }; use problemreductions::models::misc::{ BinPacking, FlowShopScheduling, LongestCommonSubsequence, MinimumTardinessSequencing, - PaintShop, ShortestCommonSupersequence, SubsetSum, + PaintShop, RectilinearPictureCompression, ShortestCommonSupersequence, SubsetSum, }; use problemreductions::prelude::*; use problemreductions::registry::collect_schemas; @@ -292,6 +292,9 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str { "--graph 0-1,1-2,2-3,3-0 --edge-weights 1,1,1,1 --required-edges 0,2 --bound 4" } "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", @@ -988,6 +991,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(|| { diff --git a/src/example_db/fixtures/examples.json b/src/example_db/fixtures/examples.json index 39501490c..459068c2e 100644 --- a/src/example_db/fixtures/examples.json +++ b/src/example_db/fixtures/examples.json @@ -29,6 +29,7 @@ {"problem":"PaintShop","variant":{},"instance":{"car_labels":["A","B","C"],"is_first":[true,true,false,true,false,false],"num_cars":3,"sequence_indices":[0,1,0,2,1,2]},"samples":[{"config":[0,0,1],"metric":{"Valid":2}}],"optimal":[{"config":[0,0,1],"metric":{"Valid":2}},{"config":[0,1,1],"metric":{"Valid":2}},{"config":[1,0,0],"metric":{"Valid":2}},{"config":[1,1,0],"metric":{"Valid":2}}]}, {"problem":"PartitionIntoTriangles","variant":{"graph":"SimpleGraph"},"instance":{"graph":{"inner":{"edge_property":"undirected","edges":[[0,1,null],[0,2,null],[1,2,null],[3,4,null],[3,5,null],[4,5,null],[0,3,null]],"node_holes":[],"nodes":[null,null,null,null,null,null]}}},"samples":[{"config":[0,0,0,1,1,1],"metric":true}],"optimal":[{"config":[0,0,0,1,1,1],"metric":true},{"config":[1,1,1,0,0,0],"metric":true}]}, {"problem":"QUBO","variant":{"weight":"f64"},"instance":{"matrix":[[-1.0,2.0,0.0],[0.0,-1.0,2.0],[0.0,0.0,-1.0]],"num_vars":3},"samples":[{"config":[1,0,1],"metric":{"Valid":-2.0}}],"optimal":[{"config":[1,0,1],"metric":{"Valid":-2.0}}]}, + {"problem":"RectilinearPictureCompression","variant":{},"instance":{"bound_k":2,"matrix":[[true,true,false,false],[true,true,false,false],[false,false,true,true],[false,false,true,true]]},"samples":[{"config":[1,1],"metric":true},{"config":[0,0],"metric":false}],"optimal":[{"config":[1,1],"metric":true}]}, {"problem":"Satisfiability","variant":{},"instance":{"clauses":[{"literals":[1,2]},{"literals":[-1,3]},{"literals":[-2,-3]}],"num_vars":3},"samples":[{"config":[1,0,1],"metric":true}],"optimal":[{"config":[0,1,0],"metric":true},{"config":[1,0,1],"metric":true}]}, {"problem":"SetBasis","variant":{},"instance":{"collection":[[0,1],[1,2],[0,2],[0,1,2]],"k":3,"universe_size":4},"samples":[{"config":[1,0,0,0,0,1,0,0,0,0,1,0],"metric":true}],"optimal":[{"config":[0,0,1,0,0,1,0,0,1,0,0,0],"metric":true},{"config":[0,0,1,0,1,0,0,0,0,1,0,0],"metric":true},{"config":[0,1,0,0,0,0,1,0,1,0,0,0],"metric":true},{"config":[0,1,0,0,1,0,0,0,0,0,1,0],"metric":true},{"config":[0,1,1,0,1,0,1,0,1,1,0,0],"metric":true},{"config":[0,1,1,0,1,1,0,0,1,0,1,0],"metric":true},{"config":[1,0,0,0,0,0,1,0,0,1,0,0],"metric":true},{"config":[1,0,0,0,0,1,0,0,0,0,1,0],"metric":true},{"config":[1,0,1,0,0,1,1,0,1,1,0,0],"metric":true},{"config":[1,0,1,0,1,1,0,0,0,1,1,0],"metric":true},{"config":[1,1,0,0,0,1,1,0,1,0,1,0],"metric":true},{"config":[1,1,0,0,1,0,1,0,0,1,1,0],"metric":true}]}, {"problem":"ShortestCommonSupersequence","variant":{},"instance":{"alphabet_size":3,"bound":4,"strings":[[0,1,2],[1,0,2]]},"samples":[{"config":[1,0,1,2],"metric":true}],"optimal":[{"config":[0,1,0,2],"metric":true},{"config":[1,0,1,2],"metric":true}]}, diff --git a/src/models/misc/mod.rs b/src/models/misc/mod.rs index cc96aa83e..14380db2a 100644 --- a/src/models/misc/mod.rs +++ b/src/models/misc/mod.rs @@ -8,6 +8,7 @@ //! - [`LongestCommonSubsequence`]: Longest Common Subsequence //! - [`MinimumTardinessSequencing`]: Minimize tardy tasks in single-machine scheduling //! - [`PaintShop`]: Minimize color switches in paint shop scheduling +//! - [`RectilinearPictureCompression`]: Cover 1-entries with bounded rectangles //! - [`ShortestCommonSupersequence`]: Find a common supersequence of bounded length //! - [`SubsetSum`]: Find a subset summing to exactly a target value @@ -18,6 +19,7 @@ mod knapsack; mod longest_common_subsequence; mod minimum_tardiness_sequencing; pub(crate) mod paintshop; +mod rectilinear_picture_compression; pub(crate) mod shortest_common_supersequence; mod subset_sum; @@ -28,6 +30,7 @@ pub use knapsack::Knapsack; pub use longest_common_subsequence::LongestCommonSubsequence; pub use minimum_tardiness_sequencing::MinimumTardinessSequencing; pub use paintshop::PaintShop; +pub use rectilinear_picture_compression::RectilinearPictureCompression; pub use shortest_common_supersequence::ShortestCommonSupersequence; pub use subset_sum::SubsetSum; @@ -36,6 +39,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec>", description: "m x n binary matrix" }, + FieldInfo { name: "bound_k", type_name: "usize", description: "Maximum number of rectangles allowed" }, + ], + } +} + +/// The Rectilinear Picture Compression problem. +/// +/// Given an m x n binary matrix M and a positive integer K, determine whether +/// there exists a collection of at most K axis-aligned all-1 rectangles that +/// covers precisely the 1-entries of M. +/// +/// # Representation +/// +/// The configuration space consists of the maximal all-1 rectangles in the +/// matrix. Each variable is binary: 1 if the rectangle is selected, 0 otherwise. +/// The problem is satisfiable iff the selected rectangles number at most K and +/// their union covers all 1-entries. +/// +/// # Example +/// +/// ``` +/// use problemreductions::models::misc::RectilinearPictureCompression; +/// use problemreductions::{Problem, Solver, BruteForce}; +/// +/// let matrix = vec![ +/// vec![true, true, false, false], +/// vec![true, true, false, false], +/// vec![false, false, true, true], +/// vec![false, false, true, true], +/// ]; +/// let problem = RectilinearPictureCompression::new(matrix, 2); +/// let solver = BruteForce::new(); +/// let solution = solver.find_satisfying(&problem); +/// assert!(solution.is_some()); +/// ``` +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RectilinearPictureCompression { + matrix: Vec>, + bound_k: usize, +} + +impl RectilinearPictureCompression { + /// Create a new RectilinearPictureCompression instance. + /// + /// # Panics + /// + /// Panics if `matrix` is empty or has inconsistent row lengths. + pub fn new(matrix: Vec>, bound_k: usize) -> Self { + assert!(!matrix.is_empty(), "Matrix must not be empty"); + let cols = matrix[0].len(); + assert!(cols > 0, "Matrix must have at least one column"); + assert!( + matrix.iter().all(|row| row.len() == cols), + "All rows must have the same length" + ); + Self { matrix, bound_k } + } + + /// Returns the number of rows in the matrix. + pub fn num_rows(&self) -> usize { + self.matrix.len() + } + + /// Returns the number of columns in the matrix. + pub fn num_cols(&self) -> usize { + self.matrix[0].len() + } + + /// Returns the bound K. + pub fn bound_k(&self) -> usize { + self.bound_k + } + + /// Returns a reference to the binary matrix. + pub fn matrix(&self) -> &Vec> { + &self.matrix + } + + /// Enumerate all maximal all-1 sub-rectangles in the matrix. + /// + /// A rectangle is represented as `(r1, c1, r2, c2)` covering rows + /// `r1..=r2` and columns `c1..=c2`. A rectangle is maximal if no + /// proper superset rectangle in the candidate set is also all-1. + /// + /// The result is sorted lexicographically and deduplicated. + pub fn maximal_rectangles(&self) -> Vec<(usize, usize, usize, usize)> { + let m = self.num_rows(); + let n = self.num_cols(); + + // Step 1: Enumerate all all-1 rectangles by extending from each (r1, c1). + let mut candidates = Vec::new(); + for r1 in 0..m { + for c1 in 0..n { + if !self.matrix[r1][c1] { + continue; + } + // Find the rightmost column from c1 that is all-1 in row r1. + let mut c_max = n; + for c in c1..n { + if !self.matrix[r1][c] { + c_max = c; + break; + } + } + // Extend downward row by row, narrowing column range. + let mut c_end = c_max; // exclusive upper bound on columns + for r2 in r1..m { + // Narrow c_end based on row r2. + let mut new_c_end = c1; + for c in c1..c_end { + if self.matrix[r2][c] { + new_c_end = c + 1; + } else { + break; + } + } + if new_c_end <= c1 { + break; + } + c_end = new_c_end; + candidates.push((r1, c1, r2, c_end - 1)); + } + } + } + + // Step 2: Remove duplicates. + candidates.sort(); + candidates.dedup(); + + // Step 3: Filter to keep only maximal rectangles. + // A rectangle A is dominated by rectangle B if B contains A as a proper subset. + let mut maximal = Vec::new(); + for &(r1, c1, r2, c2) in &candidates { + let is_dominated = candidates.iter().any(|&(sr1, sc1, sr2, sc2)| { + sr1 <= r1 + && sc1 <= c1 + && sr2 >= r2 + && sc2 >= c2 + && (sr1, sc1, sr2, sc2) != (r1, c1, r2, c2) + }); + if !is_dominated { + maximal.push((r1, c1, r2, c2)); + } + } + + maximal + } +} + +impl Problem for RectilinearPictureCompression { + const NAME: &'static str = "RectilinearPictureCompression"; + type Metric = bool; + + fn variant() -> Vec<(&'static str, &'static str)> { + crate::variant_params![] + } + + fn dims(&self) -> Vec { + let r = self.maximal_rectangles().len(); + vec![2; r] + } + + fn evaluate(&self, config: &[usize]) -> bool { + let rects = self.maximal_rectangles(); + if config.len() != rects.len() { + return false; + } + if config.iter().any(|&v| v >= 2) { + return false; + } + + // Count selected rectangles. + let selected_count: usize = config.iter().sum(); + if selected_count > self.bound_k { + return false; + } + + // Check that all 1-entries are covered. + let m = self.num_rows(); + let n = self.num_cols(); + let mut covered = vec![vec![false; n]; m]; + for (i, &x) in config.iter().enumerate() { + if x == 1 { + let (r1, c1, r2, c2) = rects[i]; + for row in &mut covered[r1..=r2] { + for cell in &mut row[c1..=c2] { + *cell = true; + } + } + } + } + + for (row_m, row_c) in self.matrix.iter().zip(covered.iter()) { + for (&entry, &cov) in row_m.iter().zip(row_c.iter()) { + if entry && !cov { + return false; + } + } + } + + true + } +} + +impl SatisfactionProblem for RectilinearPictureCompression {} + +crate::declare_variants! { + default sat RectilinearPictureCompression => "2^(num_rows * num_cols)", +} + +#[cfg(feature = "example-db")] +pub(crate) fn canonical_model_example_specs() -> Vec { + vec![crate::example_db::specs::ModelExampleSpec { + id: "rectilinear_picture_compression", + build: || { + let matrix = vec![ + vec![true, true, false, false], + vec![true, true, false, false], + vec![false, false, true, true], + vec![false, false, true, true], + ]; + let problem = RectilinearPictureCompression::new(matrix, 2); + // Config: select both maximal rectangles (the two 2x2 blocks). + // The maximal rectangles for this matrix are exactly: + // (0,0,1,1) and (2,2,3,3), so config [1,1] selects both. + crate::example_db::specs::satisfaction_example(problem, vec![vec![1, 1], vec![0, 0]]) + }, + }] +} + +#[cfg(test)] +#[path = "../../unit_tests/models/misc/rectilinear_picture_compression.rs"] +mod tests; diff --git a/src/models/mod.rs b/src/models/mod.rs index 94614519a..796cd77d8 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -21,6 +21,7 @@ pub use graph::{ }; pub use misc::{ BinPacking, Factoring, FlowShopScheduling, Knapsack, LongestCommonSubsequence, - MinimumTardinessSequencing, PaintShop, ShortestCommonSupersequence, SubsetSum, + MinimumTardinessSequencing, PaintShop, RectilinearPictureCompression, + ShortestCommonSupersequence, SubsetSum, }; pub use set::{ExactCoverBy3Sets, MaximumSetPacking, MinimumSetCovering, SetBasis}; diff --git a/src/unit_tests/models/misc/rectilinear_picture_compression.rs b/src/unit_tests/models/misc/rectilinear_picture_compression.rs new file mode 100644 index 000000000..e3a03edcb --- /dev/null +++ b/src/unit_tests/models/misc/rectilinear_picture_compression.rs @@ -0,0 +1,229 @@ +use super::*; +use crate::solvers::{BruteForce, Solver}; +use crate::traits::Problem; + +fn two_block_matrix() -> Vec> { + vec![ + vec![true, true, false, false], + vec![true, true, false, false], + vec![false, false, true, true], + vec![false, false, true, true], + ] +} + +fn issue_matrix() -> Vec> { + // 6x6 matrix from the issue description + vec![ + vec![true, true, true, false, false, false], + vec![true, true, true, false, false, false], + vec![false, false, true, true, true, false], + vec![false, false, true, true, true, false], + vec![false, false, false, false, true, true], + vec![false, false, false, false, true, true], + ] +} + +#[test] +fn test_rectilinear_picture_compression_basic() { + let problem = RectilinearPictureCompression::new(two_block_matrix(), 2); + assert_eq!(problem.num_rows(), 4); + assert_eq!(problem.num_cols(), 4); + assert_eq!(problem.bound_k(), 2); + assert_eq!( + ::NAME, + "RectilinearPictureCompression" + ); + assert_eq!( + ::variant(), + vec![] + ); +} + +#[test] +fn test_rectilinear_picture_compression_maximal_rectangles_two_blocks() { + let problem = RectilinearPictureCompression::new(two_block_matrix(), 2); + let rects = problem.maximal_rectangles(); + // Two disjoint 2x2 blocks: (0,0,1,1) and (2,2,3,3) + assert_eq!(rects, vec![(0, 0, 1, 1), (2, 2, 3, 3)]); +} + +#[test] +fn test_rectilinear_picture_compression_dims() { + let problem = RectilinearPictureCompression::new(two_block_matrix(), 2); + // 2 maximal rectangles -> 2 binary variables + assert_eq!(problem.dims(), vec![2, 2]); +} + +#[test] +fn test_rectilinear_picture_compression_evaluate_satisfying() { + let problem = RectilinearPictureCompression::new(two_block_matrix(), 2); + // Select both maximal rectangles + assert!(problem.evaluate(&[1, 1])); +} + +#[test] +fn test_rectilinear_picture_compression_evaluate_unsatisfying_not_all_covered() { + let problem = RectilinearPictureCompression::new(two_block_matrix(), 2); + // Select only first rectangle - second block uncovered + assert!(!problem.evaluate(&[1, 0])); + // Select only second rectangle - first block uncovered + assert!(!problem.evaluate(&[0, 1])); + // Select none + assert!(!problem.evaluate(&[0, 0])); +} + +#[test] +fn test_rectilinear_picture_compression_evaluate_bound_exceeded() { + let problem = RectilinearPictureCompression::new(two_block_matrix(), 1); + // Both selected but bound is 1 + assert!(!problem.evaluate(&[1, 1])); +} + +#[test] +fn test_rectilinear_picture_compression_evaluate_wrong_config_length() { + let problem = RectilinearPictureCompression::new(two_block_matrix(), 2); + assert!(!problem.evaluate(&[1])); + assert!(!problem.evaluate(&[1, 1, 0])); +} + +#[test] +fn test_rectilinear_picture_compression_evaluate_invalid_variable_value() { + let problem = RectilinearPictureCompression::new(two_block_matrix(), 2); + assert!(!problem.evaluate(&[2, 0])); +} + +#[test] +fn test_rectilinear_picture_compression_issue_matrix_satisfiable() { + let problem = RectilinearPictureCompression::new(issue_matrix(), 3); + let solver = BruteForce::new(); + let solution = solver.find_satisfying(&problem); + assert!(solution.is_some()); + let sol = solution.unwrap(); + assert!(problem.evaluate(&sol)); +} + +#[test] +fn test_rectilinear_picture_compression_issue_matrix_unsatisfiable() { + let problem = RectilinearPictureCompression::new(issue_matrix(), 2); + let solver = BruteForce::new(); + let solution = solver.find_satisfying(&problem); + assert!(solution.is_none()); +} + +#[test] +fn test_rectilinear_picture_compression_brute_force() { + let problem = RectilinearPictureCompression::new(two_block_matrix(), 2); + let solver = BruteForce::new(); + let solution = solver + .find_satisfying(&problem) + .expect("should find a solution"); + assert!(problem.evaluate(&solution)); +} + +#[test] +fn test_rectilinear_picture_compression_brute_force_all() { + let problem = RectilinearPictureCompression::new(two_block_matrix(), 2); + let solver = BruteForce::new(); + let solutions = solver.find_all_satisfying(&problem); + assert!(!solutions.is_empty()); + for sol in &solutions { + assert!(problem.evaluate(sol)); + } +} + +#[test] +fn test_rectilinear_picture_compression_serialization() { + let problem = RectilinearPictureCompression::new(two_block_matrix(), 2); + let json = serde_json::to_value(&problem).unwrap(); + assert_eq!( + json, + serde_json::json!({ + "matrix": [ + [true, true, false, false], + [true, true, false, false], + [false, false, true, true], + [false, false, true, true], + ], + "bound_k": 2, + }) + ); + let restored: RectilinearPictureCompression = serde_json::from_value(json).unwrap(); + assert_eq!(restored.num_rows(), problem.num_rows()); + assert_eq!(restored.num_cols(), problem.num_cols()); + assert_eq!(restored.bound_k(), problem.bound_k()); + assert_eq!(restored.matrix(), problem.matrix()); +} + +#[test] +fn test_rectilinear_picture_compression_single_cell() { + // Single 1-entry matrix + let matrix = vec![vec![true]]; + let problem = RectilinearPictureCompression::new(matrix, 1); + let rects = problem.maximal_rectangles(); + assert_eq!(rects, vec![(0, 0, 0, 0)]); + assert_eq!(problem.dims(), vec![2]); + assert!(problem.evaluate(&[1])); + assert!(!problem.evaluate(&[0])); +} + +#[test] +fn test_rectilinear_picture_compression_all_zeros() { + // Matrix with no 1-entries: no maximal rectangles, always satisfiable + let matrix = vec![vec![false, false], vec![false, false]]; + let problem = RectilinearPictureCompression::new(matrix, 0); + let rects = problem.maximal_rectangles(); + assert!(rects.is_empty()); + assert_eq!(problem.dims(), Vec::::new()); + // Empty config satisfies (no 1-entries to cover) + assert!(problem.evaluate(&[])); +} + +#[test] +fn test_rectilinear_picture_compression_full_matrix() { + // 2x2 all-ones matrix: one maximal rectangle covers everything + let matrix = vec![vec![true, true], vec![true, true]]; + let problem = RectilinearPictureCompression::new(matrix, 1); + let rects = problem.maximal_rectangles(); + assert_eq!(rects, vec![(0, 0, 1, 1)]); + assert!(problem.evaluate(&[1])); + assert!(!problem.evaluate(&[0])); +} + +#[test] +fn test_rectilinear_picture_compression_overlapping_rectangles() { + // L-shaped region: requires multiple rectangles, some may overlap + let matrix = vec![vec![true, true], vec![true, false]]; + let problem = RectilinearPictureCompression::new(matrix, 2); + let rects = problem.maximal_rectangles(); + // Maximal rectangles: (0,0,1,0) vertical bar, (0,0,0,1) horizontal bar + assert!(rects.contains(&(0, 0, 1, 0))); + assert!(rects.contains(&(0, 0, 0, 1))); + let solver = BruteForce::new(); + let solution = solver.find_satisfying(&problem).unwrap(); + assert!(problem.evaluate(&solution)); +} + +#[test] +fn test_rectilinear_picture_compression_matrix_getter() { + let matrix = two_block_matrix(); + let problem = RectilinearPictureCompression::new(matrix.clone(), 2); + assert_eq!(problem.matrix(), &matrix); +} + +#[test] +#[should_panic(expected = "empty")] +fn test_rectilinear_picture_compression_empty_matrix_panics() { + RectilinearPictureCompression::new(vec![], 1); +} + +#[test] +#[should_panic(expected = "column")] +fn test_rectilinear_picture_compression_empty_row_panics() { + RectilinearPictureCompression::new(vec![vec![]], 1); +} + +#[test] +#[should_panic(expected = "same length")] +fn test_rectilinear_picture_compression_inconsistent_rows_panics() { + RectilinearPictureCompression::new(vec![vec![true, false], vec![true]], 1); +} diff --git a/src/unit_tests/trait_consistency.rs b/src/unit_tests/trait_consistency.rs index ef0f09f67..f678c2439 100644 --- a/src/unit_tests/trait_consistency.rs +++ b/src/unit_tests/trait_consistency.rs @@ -166,6 +166,10 @@ fn test_all_problems_implement_trait_correctly() { ), "IsomorphicSpanningTree", ); + check_problem_trait( + &RectilinearPictureCompression::new(vec![vec![true, false], vec![false, true]], 2), + "RectilinearPictureCompression", + ); check_problem_trait( &ShortestCommonSupersequence::new(2, vec![vec![0, 1], vec![1, 0]], 3), "ShortestCommonSupersequence", From 23188f0f86376e88a4b231b8382e7e403d01dd46 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Tue, 17 Mar 2026 00:23:54 +0800 Subject: [PATCH 3/8] Add RectilinearPictureCompression problem-def entry to paper Co-Authored-By: Claude Opus 4.6 --- docs/paper/reductions.typ | 55 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index 76cddb448..6b77856af 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -109,6 +109,7 @@ "FlowShopScheduling": [Flow Shop Scheduling], "MinimumTardinessSequencing": [Minimum Tardiness Sequencing], "DirectedTwoCommodityIntegralFlow": [Directed Two-Commodity Integral Flow], + "RectilinearPictureCompression": [Rectilinear Picture Compression], ) // Definition label: "def:" — each definition block must have a matching label @@ -1826,6 +1827,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 positive 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. The best known exact algorithm runs in $O^*(2^(m n))$ by brute-force enumeration over all possible rectangle selections#footnote[No algorithm improving on brute-force is known for the general case of Rectilinear Picture Compression.]. + + *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 are $R_1 = (0, 1, 0, 1)$ covering the top-left $2 times 2$ block and $R_2 = (2, 3, 2, 3)$ covering the bottom-right $2 times 2$ block. 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$.], + ) + ] + ] +} + #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$. ][ From f936eee9ce462d7f25fb8989090c2f0c2bd336c9 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Tue, 17 Mar 2026 00:34:07 +0800 Subject: [PATCH 4/8] Review fixes: precompute maximal rectangles, return slice from matrix(), add solution count assertion --- .../misc/rectilinear_picture_compression.rs | 52 ++++++++++++++----- .../misc/rectilinear_picture_compression.rs | 3 +- 2 files changed, 42 insertions(+), 13 deletions(-) diff --git a/src/models/misc/rectilinear_picture_compression.rs b/src/models/misc/rectilinear_picture_compression.rs index c898db6c1..abe6aaff2 100644 --- a/src/models/misc/rectilinear_picture_compression.rs +++ b/src/models/misc/rectilinear_picture_compression.rs @@ -8,6 +8,7 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry}; use crate::traits::{Problem, SatisfactionProblem}; +use serde::de::Deserializer; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -55,10 +56,27 @@ inventory::submit! { /// let solution = solver.find_satisfying(&problem); /// assert!(solution.is_some()); /// ``` -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize)] pub struct RectilinearPictureCompression { matrix: Vec>, bound_k: usize, + #[serde(skip)] + maximal_rects: Vec<(usize, usize, usize, usize)>, +} + +impl<'de> Deserialize<'de> for RectilinearPictureCompression { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + #[derive(Deserialize)] + struct Inner { + matrix: Vec>, + bound_k: usize, + } + let inner = Inner::deserialize(deserializer)?; + Ok(Self::new(inner.matrix, inner.bound_k)) + } } impl RectilinearPictureCompression { @@ -75,7 +93,13 @@ impl RectilinearPictureCompression { matrix.iter().all(|row| row.len() == cols), "All rows must have the same length" ); - Self { matrix, bound_k } + let mut instance = Self { + matrix, + bound_k, + maximal_rects: Vec::new(), + }; + instance.maximal_rects = instance.compute_maximal_rectangles(); + instance } /// Returns the number of rows in the matrix. @@ -94,18 +118,23 @@ impl RectilinearPictureCompression { } /// Returns a reference to the binary matrix. - pub fn matrix(&self) -> &Vec> { + pub fn matrix(&self) -> &[Vec] { &self.matrix } - /// Enumerate all maximal all-1 sub-rectangles in the matrix. + /// Returns the precomputed maximal all-1 sub-rectangles. /// - /// A rectangle is represented as `(r1, c1, r2, c2)` covering rows - /// `r1..=r2` and columns `c1..=c2`. A rectangle is maximal if no - /// proper superset rectangle in the candidate set is also all-1. + /// Each rectangle is `(r1, c1, r2, c2)` covering rows `r1..=r2` and + /// columns `c1..=c2`. + pub fn maximal_rectangles(&self) -> &[(usize, usize, usize, usize)] { + &self.maximal_rects + } + + /// Enumerate all maximal all-1 sub-rectangles in the matrix. /// - /// The result is sorted lexicographically and deduplicated. - pub fn maximal_rectangles(&self) -> Vec<(usize, usize, usize, usize)> { + /// A rectangle is maximal if no proper superset rectangle in the + /// candidate set is also all-1. The result is sorted lexicographically. + fn compute_maximal_rectangles(&self) -> Vec<(usize, usize, usize, usize)> { let m = self.num_rows(); let n = self.num_cols(); @@ -178,12 +207,11 @@ impl Problem for RectilinearPictureCompression { } fn dims(&self) -> Vec { - let r = self.maximal_rectangles().len(); - vec![2; r] + vec![2; self.maximal_rects.len()] } fn evaluate(&self, config: &[usize]) -> bool { - let rects = self.maximal_rectangles(); + let rects = &self.maximal_rects; if config.len() != rects.len() { return false; } diff --git a/src/unit_tests/models/misc/rectilinear_picture_compression.rs b/src/unit_tests/models/misc/rectilinear_picture_compression.rs index e3a03edcb..3b7d38bf6 100644 --- a/src/unit_tests/models/misc/rectilinear_picture_compression.rs +++ b/src/unit_tests/models/misc/rectilinear_picture_compression.rs @@ -125,7 +125,8 @@ fn test_rectilinear_picture_compression_brute_force_all() { let problem = RectilinearPictureCompression::new(two_block_matrix(), 2); let solver = BruteForce::new(); let solutions = solver.find_all_satisfying(&problem); - assert!(!solutions.is_empty()); + // Two disjoint 2x2 blocks with K=2: exactly one satisfying config [1,1]. + assert_eq!(solutions.len(), 1); for sol in &solutions { assert!(problem.evaluate(sol)); } From a7010ffabbcaf9c867cd7480c6492c785290736d Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Tue, 17 Mar 2026 00:34:14 +0800 Subject: [PATCH 5/8] chore: remove plan file after implementation --- ...6-03-16-rectilinear-picture-compression.md | 223 ------------------ 1 file changed, 223 deletions(-) delete mode 100644 docs/plans/2026-03-16-rectilinear-picture-compression.md diff --git a/docs/plans/2026-03-16-rectilinear-picture-compression.md b/docs/plans/2026-03-16-rectilinear-picture-compression.md deleted file mode 100644 index 01ebd449a..000000000 --- a/docs/plans/2026-03-16-rectilinear-picture-compression.md +++ /dev/null @@ -1,223 +0,0 @@ -# Plan: Add RectilinearPictureCompression Model - -Fixes #443 - -## Information Checklist - -| # | Item | Value | -|---|------|-------| -| 1 | Problem name | `RectilinearPictureCompression` | -| 2 | Mathematical definition | Given an m×n binary matrix M and positive integer K, is there a collection of K or fewer axis-aligned all-1 rectangles that covers precisely the 1-entries of M? | -| 3 | Problem type | Satisfaction (`Metric = bool`) | -| 4 | Type parameters | None | -| 5 | Struct fields | `matrix: Vec>`, `bound_k: usize` | -| 6 | Configuration space | `vec![2; R]` where R = number of maximal all-1 rectangles in the matrix | -| 7 | Feasibility check | Union of selected rectangles covers exactly all 1-entries, and count ≤ K | -| 8 | Objective function | N/A (satisfaction) — returns `true` iff feasible | -| 9 | Best known exact algorithm | Brute-force: O(2^(num_rows * num_cols)) — no significantly better algorithm known (Masek, 1978) | -| 10 | Solving strategy | BruteForce works (enumerate subsets of maximal all-1 rectangles) | -| 11 | Category | `misc` (binary matrix input — unique structure) | -| 12 | Expected outcome | Instance 1 (6×6, K=3): YES — 3 rectangles cover all 1-entries. Instance 2 (same, K=2): NO | - -Associated rule: #458 [Rule] 3SAT to Rectilinear Picture Compression (not an orphan). - -## Batch 1: Implementation (Steps 1–5.5) - -All steps in this batch are independent and can be parallelized within a single subagent. - -### Step 1: Category & file -- Category: `misc/` -- File: `src/models/misc/rectilinear_picture_compression.rs` - -### Step 1.5: Size getters -- `num_rows()` → `self.matrix.len()` -- `num_cols()` → `self.matrix.first().map_or(0, |r| r.len())` -- `bound_k()` → `self.bound_k` - -### Step 2: Implement the model - -Create `src/models/misc/rectilinear_picture_compression.rs`: - -**Struct:** -```rust -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct RectilinearPictureCompression { - matrix: Vec>, - bound_k: usize, -} -``` - -**Constructor & getters:** -- `new(matrix, bound_k)` — validate all rows have equal length -- `matrix()`, `bound_k()`, `num_rows()`, `num_cols()` - -**Core helper — `maximal_rectangles(&self) -> Vec<(usize, usize, usize, usize)>`:** - -Enumerate all maximal all-1 sub-rectangles `(r1, r2, c1, c2)` where `r1..=r2` are row bounds and `c1..=c2` are column bounds. Algorithm: -1. Enumerate all all-1 rectangles by iterating over starting positions `(r1, c1)` and extending rows while narrowing the column range -2. Collect into a set -3. Filter to keep only maximal ones (no proper superset in the set) -4. Sort lexicographically for deterministic ordering - -Since candidates are all-1 by construction, they can never cover a 0-entry. This simplifies evaluate(). - -**Problem trait:** -```rust -impl Problem for RectilinearPictureCompression { - const NAME: &'static str = "RectilinearPictureCompression"; - type Metric = bool; - fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] } - fn dims(&self) -> Vec { vec![2; self.maximal_rectangles().len()] } - fn evaluate(&self, config: &[usize]) -> bool { - let candidates = self.maximal_rectangles(); - if config.len() != candidates.len() { return false; } - // Collect selected rectangles (config[i] == 1) - // Check: count of selected ≤ bound_k - // Check: union of selected covers ALL 1-entries in matrix - // (No need to check 0-coverage — candidates are all-1 by construction) - } -} -impl SatisfactionProblem for RectilinearPictureCompression {} -``` - -**ProblemSchemaEntry:** -```rust -inventory::submit! { - ProblemSchemaEntry { - name: "RectilinearPictureCompression", - display_name: "Rectilinear Picture Compression", - aliases: &[], - dimensions: &[], - module_path: module_path!(), - description: "Can a binary matrix be covered by K or fewer axis-aligned all-1 rectangles?", - fields: &[ - FieldInfo { name: "matrix", type_name: "Vec>", description: "Binary matrix" }, - FieldInfo { name: "bound_k", type_name: "usize", description: "Maximum number of rectangles" }, - ], - } -} -``` - -### Step 2.5: Variant complexity - -```rust -crate::declare_variants! { - default sat RectilinearPictureCompression => "2^(num_rows * num_cols)", -} -``` - -Conservative bound: number of maximal all-1 rectangles R ≤ num_rows * num_cols in worst case. - -### Step 3: Register the model - -1. `src/models/misc/mod.rs` — add `pub(crate) mod rectilinear_picture_compression;` and `pub use rectilinear_picture_compression::RectilinearPictureCompression;`. Also add to `canonical_model_example_specs()` chain. -2. `src/models/mod.rs` — add `RectilinearPictureCompression` to the misc re-export line. - -### Step 4: CLI discovery - -`problemreductions-cli/src/problem_name.rs`: -- Add `"rectilinearpicturecompression" => "RectilinearPictureCompression"` to `resolve_alias()` -- No short alias (none established in literature) - -### Step 4.5: CLI creation support - -`problemreductions-cli/src/commands/create.rs`: -- Add match arm for `"RectilinearPictureCompression"` using existing `parse_bool_matrix(args)` and `args.k` (reuse `--k` flag for bound_k) -- Add to `example_command_line()`: - ``` - "RectilinearPictureCompression" => "--matrix '1,1,0;0,1,1' --k 2", - ``` - -`problemreductions-cli/src/cli.rs`: -- Add `RectilinearPictureCompression` to the help table under "Flags by problem type": - ``` - RectilinearPictureCompression --matrix, --k - ``` - -### Step 4.6: Canonical model example - -In the model file, add (feature-gated under `example-db`): -```rust -#[cfg(feature = "example-db")] -pub(crate) fn canonical_model_example_specs() -> Vec { - vec![crate::example_db::specs::ModelExampleSpec { - id: "rectilinear_picture_compression", - build: || { - // 4x4 matrix with two disjoint all-1 blocks (small enough for brute force) - let matrix = vec![ - vec![true, true, false, false], - vec![true, true, false, false], - vec![false, false, true, true], - vec![false, false, true, true], - ]; - let problem = RectilinearPictureCompression::new(matrix, 2); - crate::example_db::specs::satisfaction_example( - problem, - vec![], // samples filled by brute force - ) - }, - }] -} -``` - -### Step 5: Unit tests - -Create `src/unit_tests/models/misc/rectilinear_picture_compression.rs`. - -Link from model file: `#[cfg(test)] #[path = "../../unit_tests/models/misc/rectilinear_picture_compression.rs"] mod tests;` - -Required tests: -- `test_rectilinear_picture_compression_basic` — construct 6×6 instance from issue, verify `num_rows()`, `num_cols()`, `bound_k()`, `dims()` length, NAME, variant() -- `test_rectilinear_picture_compression_evaluation` — verify satisfying config (3 correct rectangles selected) returns true; verify unsatisfying configs (wrong selection, too many) return false -- `test_rectilinear_picture_compression_no_solution` — same matrix with bound_k=2, verify no satisfying config exists -- `test_rectilinear_picture_compression_serialization` — serde JSON roundtrip -- `test_rectilinear_picture_compression_solver` — BruteForce::find_satisfying() returns Some for K=3, None for K=2 -- `test_rectilinear_picture_compression_paper_example` — use the paper example instance, verify expected outcome, verify find_all_satisfying() count -- `test_rectilinear_picture_compression_edge_cases` — empty matrix, all-zeros matrix, single cell - -### Step 5.5: trait_consistency - -`src/unit_tests/trait_consistency.rs`: -- Add `check_problem_trait(RectilinearPictureCompression::new(...))` call in `test_all_problems_implement_trait_correctly` - -## Batch 2: Paper Entry (Step 6) - -Depends on Batch 1 completion (needs `make paper` exports). - -### Step 6: Document in paper - -`docs/paper/reductions.typ`: - -**6a. Display name:** -```typst -"RectilinearPictureCompression": [Rectilinear Picture Compression], -``` - -**6b. Problem definition:** -```typst -#problem-def("RectilinearPictureCompression")[ - Given an $m times n$ binary matrix $M$ and a positive 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 $(a, b, c, d)$ with $a <= b$ and $c <= d$ covers - entries $M_(i j)$ for $a <= i <= b$ and $c <= j <= d$, and every - covered entry must satisfy $M_(i j) = 1$. -][ - ... body ... -] -``` - -**6c. Body:** -- Background: classical NP-complete problem (Garey & Johnson A4 SR25). Applications in image compression, DNA array synthesis, IC manufacturing, ACL minimization. NP-completeness via 3SAT (Masek, 1978). -- Best known algorithms: no algorithm significantly improving on brute-force is known; cite Masek 1978 and Applegate et al. 2007. -- Example: CeTZ diagram showing a small binary matrix (e.g., 4×4 with two blocks) with rectangles highlighted in color. Show the covering solution. -- Evaluation: show that selected rectangles cover all 1-entries. - -**6d. Build:** `make paper` — must compile without errors. - -## Reference files -- Satisfaction model pattern: `src/models/misc/subset_sum.rs` -- Model tests: `src/unit_tests/models/misc/subset_sum.rs` -- CLI create: `problemreductions-cli/src/commands/create.rs` (see BMF for bool matrix pattern) -- Example specs: `src/example_db/specs.rs` (satisfaction_example helper) -- Trait consistency: `src/unit_tests/trait_consistency.rs` From 1d6a88e25f0175ff15a5a0d5b592b388eb243902 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Tue, 17 Mar 2026 03:45:07 +0800 Subject: [PATCH 6/8] fix: address PR #677 review comments - allow RectilinearPictureCompression through the crate prelude and add a regression test - replace quadratic maximal-rectangle filtering with prefix-sum extension checks - align Rectilinear documentation with bound_k = 0 support and rephrase the paper complexity note --- docs/paper/reductions.typ | 4 +- src/lib.rs | 7 +- .../misc/rectilinear_picture_compression.rs | 82 +++++++++++++------ src/unit_tests/prelude.rs | 7 ++ 4 files changed, 72 insertions(+), 28 deletions(-) create mode 100644 src/unit_tests/prelude.rs diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index a3de77f51..ef049a4a8 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -1890,14 +1890,14 @@ NP-completeness was established by Garey, Johnson, and Stockmeyer @gareyJohnsonS 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 positive integer $K$, + 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. The best known exact algorithm runs in $O^*(2^(m n))$ by brute-force enumeration over all possible rectangle selections#footnote[No algorithm improving on brute-force is known for the general case of Rectilinear Picture Compression.]. + 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 are $R_1 = (0, 1, 0, 1)$ covering the top-left $2 times 2$ block and $R_2 = (2, 3, 2, 3)$ covering the bottom-right $2 times 2$ block. 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. diff --git a/src/lib.rs b/src/lib.rs index 42bc6c927..fc12ea214 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -58,8 +58,8 @@ pub mod prelude { }; pub use crate::models::misc::{ BinPacking, Factoring, FlowShopScheduling, Knapsack, LongestCommonSubsequence, - MinimumTardinessSequencing, PaintShop, SequencingWithinIntervals, - ShortestCommonSupersequence, SubsetSum, + MinimumTardinessSequencing, PaintShop, RectilinearPictureCompression, + SequencingWithinIntervals, ShortestCommonSupersequence, SubsetSum, }; pub use crate::models::set::{ ExactCoverBy3Sets, MaximumSetPacking, MinimumSetCovering, SetBasis, @@ -97,6 +97,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)] diff --git a/src/models/misc/rectilinear_picture_compression.rs b/src/models/misc/rectilinear_picture_compression.rs index abe6aaff2..cc5e73e21 100644 --- a/src/models/misc/rectilinear_picture_compression.rs +++ b/src/models/misc/rectilinear_picture_compression.rs @@ -1,6 +1,6 @@ //! Rectilinear Picture Compression problem implementation. //! -//! Given an m x n binary matrix M and a positive integer K, determine whether +//! Given an m x n binary matrix M and a nonnegative integer K, determine whether //! there exists a collection of at most K axis-aligned all-1 rectangles that //! covers precisely the 1-entries of M. Each rectangle (r1, c1, r2, c2) with //! r1 <= r2, c1 <= c2 covers entries M[i][j] for r1 <= i <= r2, c1 <= j <= c2, @@ -11,6 +11,8 @@ use crate::traits::{Problem, SatisfactionProblem}; use serde::de::Deserializer; use serde::{Deserialize, Serialize}; +type Rectangle = (usize, usize, usize, usize); + inventory::submit! { ProblemSchemaEntry { name: "RectilinearPictureCompression", @@ -28,7 +30,7 @@ inventory::submit! { /// The Rectilinear Picture Compression problem. /// -/// Given an m x n binary matrix M and a positive integer K, determine whether +/// Given an m x n binary matrix M and a nonnegative integer K, determine whether /// there exists a collection of at most K axis-aligned all-1 rectangles that /// covers precisely the 1-entries of M. /// @@ -61,7 +63,7 @@ pub struct RectilinearPictureCompression { matrix: Vec>, bound_k: usize, #[serde(skip)] - maximal_rects: Vec<(usize, usize, usize, usize)>, + maximal_rects: Vec, } impl<'de> Deserialize<'de> for RectilinearPictureCompression { @@ -126,19 +128,50 @@ impl RectilinearPictureCompression { /// /// Each rectangle is `(r1, c1, r2, c2)` covering rows `r1..=r2` and /// columns `c1..=c2`. - pub fn maximal_rectangles(&self) -> &[(usize, usize, usize, usize)] { + pub fn maximal_rectangles(&self) -> &[Rectangle] { &self.maximal_rects } + fn build_prefix_sum(&self) -> Vec> { + let m = self.num_rows(); + let n = self.num_cols(); + let mut prefix_sum = vec![vec![0; n + 1]; m + 1]; + + for r in 0..m { + let mut row_sum = 0; + for c in 0..n { + row_sum += usize::from(self.matrix[r][c]); + prefix_sum[r + 1][c + 1] = prefix_sum[r][c + 1] + row_sum; + } + } + + prefix_sum + } + + fn range_is_all_ones( + prefix_sum: &[Vec], + r1: usize, + c1: usize, + r2: usize, + c2: usize, + ) -> bool { + let area = (r2 - r1 + 1) * (c2 - c1 + 1); + let sum = prefix_sum[r2 + 1][c2 + 1] + prefix_sum[r1][c1] + - prefix_sum[r1][c2 + 1] + - prefix_sum[r2 + 1][c1]; + sum == area + } + /// Enumerate all maximal all-1 sub-rectangles in the matrix. /// - /// A rectangle is maximal if no proper superset rectangle in the - /// candidate set is also all-1. The result is sorted lexicographically. - fn compute_maximal_rectangles(&self) -> Vec<(usize, usize, usize, usize)> { + /// A rectangle is maximal if it cannot be extended one step left, right, + /// up, or down while remaining all-1. The result is sorted lexicographically. + fn compute_maximal_rectangles(&self) -> Vec { let m = self.num_rows(); let n = self.num_cols(); - // Step 1: Enumerate all all-1 rectangles by extending from each (r1, c1). + // Step 1: Enumerate right-maximal candidate rectangles by fixing + // (r1, c1, r2) and taking the widest all-1 prefix common to rows r1..=r2. let mut candidates = Vec::new(); for r1 in 0..m { for c1 in 0..n { @@ -178,23 +211,24 @@ impl RectilinearPictureCompression { candidates.sort(); candidates.dedup(); - // Step 3: Filter to keep only maximal rectangles. - // A rectangle A is dominated by rectangle B if B contains A as a proper subset. - let mut maximal = Vec::new(); - for &(r1, c1, r2, c2) in &candidates { - let is_dominated = candidates.iter().any(|&(sr1, sc1, sr2, sc2)| { - sr1 <= r1 - && sc1 <= c1 - && sr2 >= r2 - && sc2 >= c2 - && (sr1, sc1, sr2, sc2) != (r1, c1, r2, c2) - }); - if !is_dominated { - maximal.push((r1, c1, r2, c2)); - } - } + // Step 3: Filter to keep only rectangles that cannot be extended in + // any cardinal direction. A 2D prefix sum makes each extension check O(1). + let prefix_sum = self.build_prefix_sum(); + candidates + .into_iter() + .filter(|&(r1, c1, r2, c2)| { + let can_extend_left = + c1 > 0 && Self::range_is_all_ones(&prefix_sum, r1, c1 - 1, r2, c1 - 1); + let can_extend_right = + c2 + 1 < n && Self::range_is_all_ones(&prefix_sum, r1, c2 + 1, r2, c2 + 1); + let can_extend_up = + r1 > 0 && Self::range_is_all_ones(&prefix_sum, r1 - 1, c1, r1 - 1, c2); + let can_extend_down = + r2 + 1 < m && Self::range_is_all_ones(&prefix_sum, r2 + 1, c1, r2 + 1, c2); - maximal + !(can_extend_left || can_extend_right || can_extend_up || can_extend_down) + }) + .collect() } } diff --git a/src/unit_tests/prelude.rs b/src/unit_tests/prelude.rs new file mode 100644 index 000000000..fd89ef5c6 --- /dev/null +++ b/src/unit_tests/prelude.rs @@ -0,0 +1,7 @@ +use crate::prelude::*; + +#[test] +fn test_prelude_exports_rectilinear_picture_compression() { + let problem = RectilinearPictureCompression::new(vec![vec![true]], 1); + assert_eq!(problem.bound_k(), 1); +} From 645e7a1d2bfacb9021e6652d0efee9e3009fbb5d Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Tue, 17 Mar 2026 04:08:56 +0800 Subject: [PATCH 7/8] fix: tighten review follow-ups for PR 677 --- docs/paper/reductions.typ | 2 +- docs/src/cli.md | 2 + problemreductions-cli/src/cli.rs | 4 +- problemreductions-cli/src/commands/create.rs | 28 ++-- problemreductions-cli/src/commands/inspect.rs | 9 +- problemreductions-cli/src/dispatch.rs | 32 ++++- problemreductions-cli/tests/cli_tests.rs | 129 ++++++++++++++++++ 7 files changed, 183 insertions(+), 23 deletions(-) diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index ef049a4a8..da5b48441 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -1899,7 +1899,7 @@ NP-completeness was established by Garey, Johnson, and Stockmeyer @gareyJohnsonS ][ 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 are $R_1 = (0, 1, 0, 1)$ covering the top-left $2 times 2$ block and $R_2 = (2, 3, 2, 3)$ covering the bottom-right $2 times 2$ block. 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. + *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( { diff --git a/docs/src/cli.md b/docs/src/cli.md index 5c595e905..915bc257c 100644 --- a/docs/src/cli.md +++ b/docs/src/cli.md @@ -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 diff --git a/problemreductions-cli/src/cli.rs b/problemreductions-cli/src/cli.rs index c199bd1be..6743a9af1 100644 --- a/problemreductions-cli/src/cli.rs +++ b/problemreductions-cli/src/cli.rs @@ -322,10 +322,10 @@ pub struct CreateArgs { /// Number of variables (for SAT/KSAT) #[arg(long)] pub num_vars: Option, - /// Matrix for QUBO (semicolon-separated rows, e.g., "1,0.5;0.5,2") + /// Matrix input (semicolon-separated rows; use `pred create ` for problem-specific formats) #[arg(long)] pub matrix: Option, - /// Number of colors for KColoring + /// Shared integer parameter (use `pred create ` for the problem-specific meaning) #[arg(long)] pub k: Option, /// Generate a random instance (graph-based problems only) diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index eb2be0ca4..1e1e29af2 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -234,7 +234,6 @@ fn type_format_hint(type_name: &str, graph_type: Option<&str>) -> &'static str { "Vec>" => "semicolon-separated groups: \"0,1;2,3\"", "usize" => "integer", "u64" => "integer", - "Vec" => "comma-separated integers: 0,0,5", "i64" => "integer", "BigUint" => "nonnegative decimal integer", "Vec" => "comma-separated nonnegative decimal integers: 3,7,1,8", @@ -309,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) @@ -371,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 { @@ -410,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 { @@ -2071,7 +2064,7 @@ fn parse_bool_matrix(args: &CreateArgs) -> Result>> { .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> = matrix_str .split(';') .map(|row| { row.trim() @@ -2086,7 +2079,16 @@ fn parse_bool_matrix(args: &CreateArgs) -> Result>> { }) .collect() }) - .collect() + .collect::>()?; + + 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. diff --git a/problemreductions-cli/src/commands/inspect.rs b/problemreductions-cli/src/commands/inspect.rs index 3a5d37cad..5155c6581 100644 --- a/problemreductions-cli/src/commands/inspect.rs +++ b/problemreductions-cli/src/commands/inspect.rs @@ -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); @@ -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, }); diff --git a/problemreductions-cli/src/dispatch.rs b/problemreductions-cli/src/dispatch.rs index 938d8643c..0ba13977a 100644 --- a/problemreductions-cli/src/dispatch.rs +++ b/problemreductions-cli/src/dispatch.rs @@ -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 { + fn best_ilp_path(&self) -> Option { 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"); @@ -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 { + 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"))?; diff --git a/problemreductions-cli/tests/cli_tests.rs b/problemreductions-cli/tests/cli_tests.rs index 9c35fd001..c481bebc3 100644 --- a/problemreductions-cli/tests/cli_tests.rs +++ b/problemreductions-cli/tests/cli_tests.rs @@ -951,6 +951,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() @@ -2355,6 +2396,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_kcoloring_missing_k() { let output = pred() From 37d75922649bb2c4abe48abd80fb7c574da3539c Mon Sep 17 00:00:00 2001 From: zazabap Date: Thu, 19 Mar 2026 07:15:28 +0000 Subject: [PATCH 8/8] fix: update staff scheduling test to match parse_bool_rows validation The parse_bool_rows refactor catches ragged rows before validate_staff_scheduling_args, producing a different error message. Update the assertion to accept either message. Co-Authored-By: Claude Opus 4.6 (1M context) --- problemreductions-cli/src/commands/create.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index 669331daa..6ac146c85 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -3721,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 {