From 6cc27307076cd86ffcc6bb4aaba190f736ce1ea9 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Mon, 16 Mar 2026 20:44:09 +0800 Subject: [PATCH 1/6] Add plan for #420: ConsecutiveBlockMinimization model --- ...26-03-16-consecutive-block-minimization.md | 167 ++++++++++++++++++ 1 file changed, 167 insertions(+) create mode 100644 docs/plans/2026-03-16-consecutive-block-minimization.md diff --git a/docs/plans/2026-03-16-consecutive-block-minimization.md b/docs/plans/2026-03-16-consecutive-block-minimization.md new file mode 100644 index 00000000..5108c839 --- /dev/null +++ b/docs/plans/2026-03-16-consecutive-block-minimization.md @@ -0,0 +1,167 @@ +# Plan: Add ConsecutiveBlockMinimization Model (#420) + +## Summary + +Add the Consecutive Block Minimization satisfaction problem (Garey & Johnson SR17) to the `algebraic/` category. Given an m×n binary matrix A and positive integer K, decide whether there exists a column permutation yielding at most K maximal blocks of consecutive 1's across all rows. + +## Issue Details + +- **Problem**: ConsecutiveBlockMinimization +- **Type**: SatisfactionProblem (Metric = bool) +- **Category**: algebraic/ (matrix input) +- **No type parameters** (no graph type, no weight type) +- **NP-complete**: Kou (1977), reduction from Hamiltonian Path +- **Complexity**: O(n^n × m × n) brute-force + +## Batch 1: Implementation (Steps 1-5.5) + +### Step 1: Category + +Place in `src/models/algebraic/` alongside BMF, QUBO, ILP, CVP. + +### Step 1.5: Size Getters + +Getter methods needed (from complexity expression and overhead expressions): +- `num_rows()` → m (matrix.len()) +- `num_cols()` → n (matrix[0].len()) +- `bound_k()` → K +- `num_variables()` → num_cols (override default) + +### Step 2: Model File + +Create `src/models/algebraic/consecutive_block_minimization.rs`: + +**Schema entry** (inventory::submit!): +- name: "ConsecutiveBlockMinimization" +- display_name: "ConsecutiveBlockMinimization" +- aliases: &["CBM"] +- dimensions: &[] (no type params) +- fields: matrix (Vec>), bound_k (usize) + +**Struct**: +```rust +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ConsecutiveBlockMinimization { + matrix: Vec>, + num_rows: usize, + num_cols: usize, + bound_k: usize, +} +``` + +**Constructor** `new(matrix, bound_k)`: +- Validate all rows have same length +- Store derived num_rows, num_cols + +**Accessors**: `matrix()`, `num_rows()`, `num_cols()`, `bound_k()` + +**Helper** `count_blocks(config)`: +- Interpret config as column permutation (config[i] = position of column i) +- Validate it's a valid permutation (all distinct, in 0..num_cols) +- For each row, count maximal runs of consecutive 1's after reordering +- Return Option: None if invalid permutation, Some(total) otherwise + +**Problem impl**: +- `NAME = "ConsecutiveBlockMinimization"` +- `Metric = bool` +- `dims() = vec![num_cols; num_cols]` +- `evaluate(config)`: call count_blocks, return false if invalid permutation, otherwise total <= bound_k +- `variant() = crate::variant_params![]` +- Override `num_variables()` → num_cols + +**SatisfactionProblem**: empty marker impl + +### Step 2.5: declare_variants! + +```rust +crate::declare_variants! { + default sat ConsecutiveBlockMinimization => "num_cols^num_cols * num_rows * num_cols", +} +``` + +### Step 3: Register in Modules + +1. `src/models/algebraic/mod.rs`: + - Add `pub(crate) mod consecutive_block_minimization;` + - Add `pub use consecutive_block_minimization::ConsecutiveBlockMinimization;` + - Update module doc comment + - Add to `canonical_model_example_specs()` aggregator + +2. `src/models/mod.rs`: + - Re-export `ConsecutiveBlockMinimization` from `algebraic` + +### Step 4: CLI Registration + +1. **Aliases**: Already handled via `declare_variants!` + ProblemSchemaEntry aliases field ("CBM") + +2. **`problemreductions-cli/src/commands/create.rs`**: + - Add match arm for "ConsecutiveBlockMinimization" + - Parse `--matrix` (JSON 2D bool array) and `--bound-k` (integer) + - Add to `after_help` flag table + - Add to `all_data_flags_empty` + - Add to `example_for` with a small example instance + +3. **`problemreductions-cli/src/cli.rs`**: + - Add `--matrix` flag: `Option` for JSON-encoded matrix + - Add `--bound-k` flag: `Option` + +### Step 4.6: Example-DB + +Add `canonical_model_example_specs()` in the model file: +- Use the YES instance from the issue (Instance 2: path graph adjacency matrix, K=6) +- Or use a simpler small instance that has a satisfying permutation +- Use `satisfaction_example(problem, vec![valid_config])` + +### Step 5: Unit Tests + +Create `src/unit_tests/models/algebraic/consecutive_block_minimization.rs`: + +- `test_consecutive_block_minimization_creation`: constructor, getters +- `test_consecutive_block_minimization_evaluation`: test with known YES and NO configs +- `test_consecutive_block_minimization_invalid_permutation`: non-permutation configs return false +- `test_consecutive_block_minimization_serialization`: serde round-trip +- `test_consecutive_block_minimization_solver`: BruteForce::find_satisfying on small instance +- `test_consecutive_block_minimization_paper_example`: test with the issue's example instances +- `test_consecutive_block_minimization_empty_matrix`: edge case + +Add `#[path]` link in model file: +```rust +#[cfg(test)] +#[path = "../../unit_tests/models/algebraic/consecutive_block_minimization.rs"] +mod tests; +``` + +### Step 5.5: Trait Consistency + +Add entry in `src/unit_tests/trait_consistency.rs`: +```rust +check_problem_trait( + &ConsecutiveBlockMinimization::new(matrix, bound_k), + "ConsecutiveBlockMinimization", +); +``` + +## Batch 2: Paper Entry (Step 6) + +### Step 6: Paper Documentation + +In `docs/paper/reductions.typ`: + +1. Add to `display-name` dict: + ```typst + "ConsecutiveBlockMinimization": [Consecutive Block Minimization], + ``` + +2. Add `problem-def` block after related algebraic problems: + - Formal definition: m×n binary matrix, positive integer K, column permutation, blocks + - Background: GJ SR17, information retrieval, scheduling, glass cutting + - Reference to Kou (1977), Booth (1975), Haddadi & Layouni (2008) + - Connection to consecutive ones property (PQ-trees) + +## Verification + +After each batch, run: +```bash +make check # fmt + clippy + test +make paper # build paper +``` From 5c70fffa186092fdae2673e37704232b39befbeb Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Mon, 16 Mar 2026 21:08:09 +0800 Subject: [PATCH 2/6] Implement #420: Add ConsecutiveBlockMinimization model MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add the Consecutive Block Minimization satisfaction problem (Garey & Johnson SR17) to the algebraic/ category. Given an m×n binary matrix and bound K, decide whether a column permutation exists yielding at most K maximal blocks of consecutive 1's across all rows. - Model with SatisfactionProblem trait, dims=[n;n] permutation space - CLI create with --matrix (JSON) and --bound-k flags - Unit tests: creation, evaluation, count_blocks, brute_force, serialization, invalid permutation, empty matrix - Paper entry with citations (Kou 1977, Booth 1975, Haddadi 2008) - Example DB fixture with 2×3 matrix instance Co-Authored-By: Claude Opus 4.6 --- docs/paper/reductions.typ | 35 +++ docs/paper/references.bib | 29 +++ docs/src/reductions/problem_schemas.json | 16 ++ docs/src/reductions/reduction_graph.json | 246 ++++++++++-------- problemreductions-cli/src/cli.rs | 5 + problemreductions-cli/src/commands/create.rs | 30 ++- src/example_db/fixtures/examples.json | 1 + .../consecutive_block_minimization.rs | 207 +++++++++++++++ src/models/algebraic/mod.rs | 4 + src/models/mod.rs | 2 +- .../consecutive_block_minimization.rs | 96 +++++++ src/unit_tests/trait_consistency.rs | 4 + 12 files changed, 559 insertions(+), 116 deletions(-) create mode 100644 src/models/algebraic/consecutive_block_minimization.rs create mode 100644 src/unit_tests/models/algebraic/consecutive_block_minimization.rs diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index ce08d760..5751a28c 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -105,6 +105,7 @@ "PartitionIntoTriangles": [Partition Into Triangles], "FlowShopScheduling": [Flow Shop Scheduling], "MinimumTardinessSequencing": [Minimum Tardiness Sequencing], + "ConsecutiveBlockMinimization": [Consecutive Block Minimization], ) // Definition label: "def:" — each definition block must have a matching label @@ -1496,6 +1497,40 @@ NP-completeness was established by Garey, Johnson, and Stockmeyer @gareyJohnsonS ] } +#{ + let x = load-model-example("ConsecutiveBlockMinimization") + let mat = x.instance.matrix + let K = x.instance.bound_k + let n-rows = mat.len() + let n-cols = if n-rows > 0 { mat.at(0).len() } else { 0 } + let sol = x.optimal.at(0) + let perm = sol.config + // Count blocks under the satisfying permutation + let total-blocks = 0 + for row in mat { + let in-block = false + for p in perm { + if row.at(p) { + if not in-block { + total-blocks += 1 + in-block = true + } + } else { + in-block = false + } + } + } + [ + #problem-def("ConsecutiveBlockMinimization")[ + Given an $m times n$ binary matrix $A$ and a positive integer $K$, determine whether there exists a permutation of the columns of $A$ such that the resulting matrix has at most $K$ maximal blocks of consecutive 1-entries (summed over all rows). A _block_ is a maximal contiguous run of 1-entries within a single row. + ][ + Consecutive Block Minimization (SR17 in Garey & Johnson) arises in consecutive file organization for information retrieval systems, where records stored on a linear medium must be arranged so that each query's relevant records form a contiguous segment. Applications also include scheduling, production planning, the glass cutting industry, and data compression. NP-complete by reduction from Hamiltonian Path @kou1977. When $K$ equals the number of non-all-zero rows, the problem reduces to testing the _consecutive ones property_, solvable in polynomial time via PQ-trees @booth1975. A 1.5-approximation is known @haddadi2008. The best known exact algorithm runs in $O^*(n!)$ by brute-force enumeration of all column permutations. + + *Example.* Let $A$ be the #n-rows$times$#n-cols matrix with rows #mat.enumerate().map(((i, row)) => [$r_#i = (#row.map(v => if v {$1$} else {$0$}).join($,$))$]).join(", ") and $K = #K$. The column permutation $pi = (#perm.map(p => str(p)).join(", "))$ yields #total-blocks total blocks, so #total-blocks $<= #K$ and the answer is YES. + ] + ] +} + #{ let x = load-model-example("PaintShop") let n-cars = x.instance.num_cars diff --git a/docs/paper/references.bib b/docs/paper/references.bib index 7186ea64..f6c87bc9 100644 --- a/docs/paper/references.bib +++ b/docs/paper/references.bib @@ -672,3 +672,32 @@ @article{papadimitriou1982 year = {1982}, doi = {10.1145/322307.322309} } + +@article{kou1977, + author = {Lawrence T. Kou}, + title = {Polynomial Complete Consecutive Information Retrieval Problems}, + journal = {SIAM Journal on Computing}, + volume = {6}, + number = {1}, + pages = {67--75}, + year = {1977}, + doi = {10.1137/0206005} +} + +@phdthesis{booth1975, + author = {Kellogg S. Booth}, + title = {PQ Tree Algorithms}, + school = {University of California, Berkeley}, + year = {1975} +} + +@article{haddadi2008, + author = {Salim Haddadi and Zohra Layouni}, + title = {Consecutive block minimization is 1.5-approximable}, + journal = {Information Processing Letters}, + volume = {108}, + number = {3}, + pages = {161--163}, + year = {2008}, + doi = {10.1016/j.ipl.2008.05.003} +} diff --git a/docs/src/reductions/problem_schemas.json b/docs/src/reductions/problem_schemas.json index 5949a528..0bf2cc3e 100644 --- a/docs/src/reductions/problem_schemas.json +++ b/docs/src/reductions/problem_schemas.json @@ -89,6 +89,22 @@ } ] }, + { + "name": "ConsecutiveBlockMinimization", + "description": "Permute columns of a binary matrix to have at most K consecutive blocks of 1s", + "fields": [ + { + "name": "matrix", + "type_name": "Vec>", + "description": "Binary matrix A (m x n)" + }, + { + "name": "bound_k", + "type_name": "usize", + "description": "Upper bound K on total consecutive blocks" + } + ] + }, { "name": "ExactCoverBy3Sets", "description": "Determine if a collection of 3-element subsets contains an exact cover", diff --git a/docs/src/reductions/reduction_graph.json b/docs/src/reductions/reduction_graph.json index cd80f3f0..fa539800 100644 --- a/docs/src/reductions/reduction_graph.json +++ b/docs/src/reductions/reduction_graph.json @@ -1,3 +1,14 @@ +{ + "nodes": [ + { + "name": "BMF", + "variant": {}, + "category": "algebraic", + "doc_path": "models/algebraic/struct.BMF.html", + "complexity": "2^(rows * rank + r +Exported to: docs/src/reductions/reduction_graph.json + +JSON content: { "nodes": [ { @@ -57,6 +68,13 @@ "doc_path": "models/algebraic/struct.ClosestVectorProblem.html", "complexity": "2^num_basis_vectors" }, + { + "name": "ConsecutiveBlockMinimization", + "variant": {}, + "category": "algebraic", + "doc_path": "models/algebraic/struct.ConsecutiveBlockMinimization.html", + "complexity": "factorial(num_cols) * num_rows * num_cols" + }, { "name": "ExactCoverBy3Sets", "variant": {}, @@ -539,7 +557,7 @@ "edges": [ { "source": 3, - "target": 12, + "target": 13, "overhead": [ { "field": "num_vars", @@ -554,7 +572,7 @@ }, { "source": 4, - "target": 12, + "target": 13, "overhead": [ { "field": "num_vars", @@ -569,7 +587,7 @@ }, { "source": 4, - "target": 54, + "target": 55, "overhead": [ { "field": "num_spins", @@ -583,7 +601,7 @@ "doc_path": "rules/circuit_spinglass/index.html" }, { - "source": 8, + "source": 9, "target": 4, "overhead": [ { @@ -598,8 +616,8 @@ "doc_path": "rules/factoring_circuit/index.html" }, { - "source": 8, - "target": 13, + "source": 9, + "target": 14, "overhead": [ { "field": "num_vars", @@ -613,8 +631,8 @@ "doc_path": "rules/factoring_ilp/index.html" }, { - "source": 12, - "target": 13, + "source": 13, + "target": 14, "overhead": [ { "field": "num_vars", @@ -628,8 +646,8 @@ "doc_path": "rules/ilp_bool_ilp_i32/index.html" }, { - "source": 12, - "target": 49, + "source": 13, + "target": 50, "overhead": [ { "field": "num_vars", @@ -639,8 +657,8 @@ "doc_path": "rules/ilp_qubo/index.html" }, { - "source": 16, - "target": 19, + "source": 17, + "target": 20, "overhead": [ { "field": "num_vertices", @@ -654,8 +672,8 @@ "doc_path": "rules/kcoloring_casts/index.html" }, { - "source": 19, - "target": 12, + "source": 20, + "target": 13, "overhead": [ { "field": "num_vars", @@ -669,8 +687,8 @@ "doc_path": "rules/coloring_ilp/index.html" }, { - "source": 19, - "target": 49, + "source": 20, + "target": 50, "overhead": [ { "field": "num_vars", @@ -680,8 +698,8 @@ "doc_path": "rules/coloring_qubo/index.html" }, { - "source": 20, - "target": 22, + "source": 21, + "target": 23, "overhead": [ { "field": "num_vars", @@ -695,8 +713,8 @@ "doc_path": "rules/ksatisfiability_casts/index.html" }, { - "source": 20, - "target": 49, + "source": 21, + "target": 50, "overhead": [ { "field": "num_vars", @@ -706,8 +724,8 @@ "doc_path": "rules/ksatisfiability_qubo/index.html" }, { - "source": 21, - "target": 22, + "source": 22, + "target": 23, "overhead": [ { "field": "num_vars", @@ -721,8 +739,8 @@ "doc_path": "rules/ksatisfiability_casts/index.html" }, { - "source": 21, - "target": 49, + "source": 22, + "target": 50, "overhead": [ { "field": "num_vars", @@ -732,8 +750,8 @@ "doc_path": "rules/ksatisfiability_qubo/index.html" }, { - "source": 21, - "target": 58, + "source": 22, + "target": 59, "overhead": [ { "field": "num_elements", @@ -743,8 +761,8 @@ "doc_path": "rules/ksatisfiability_subsetsum/index.html" }, { - "source": 22, - "target": 51, + "source": 23, + "target": 52, "overhead": [ { "field": "num_clauses", @@ -762,8 +780,8 @@ "doc_path": "rules/sat_ksat/index.html" }, { - "source": 23, - "target": 49, + "source": 24, + "target": 50, "overhead": [ { "field": "num_vars", @@ -773,8 +791,8 @@ "doc_path": "rules/knapsack_qubo/index.html" }, { - "source": 24, - "target": 12, + "source": 25, + "target": 13, "overhead": [ { "field": "num_vars", @@ -788,8 +806,8 @@ "doc_path": "rules/longestcommonsubsequence_ilp/index.html" }, { - "source": 25, - "target": 54, + "source": 26, + "target": 55, "overhead": [ { "field": "num_spins", @@ -803,8 +821,8 @@ "doc_path": "rules/spinglass_maxcut/index.html" }, { - "source": 27, - "target": 12, + "source": 28, + "target": 13, "overhead": [ { "field": "num_vars", @@ -818,8 +836,8 @@ "doc_path": "rules/maximumclique_ilp/index.html" }, { - "source": 27, - "target": 31, + "source": 28, + "target": 32, "overhead": [ { "field": "num_vertices", @@ -833,8 +851,8 @@ "doc_path": "rules/maximumclique_maximumindependentset/index.html" }, { - "source": 28, - "target": 29, + "source": 29, + "target": 30, "overhead": [ { "field": "num_vertices", @@ -848,8 +866,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 28, - "target": 33, + "source": 29, + "target": 34, "overhead": [ { "field": "num_vertices", @@ -863,8 +881,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 29, - "target": 34, + "source": 30, + "target": 35, "overhead": [ { "field": "num_vertices", @@ -878,8 +896,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 30, - "target": 28, + "source": 31, + "target": 29, "overhead": [ { "field": "num_vertices", @@ -893,8 +911,8 @@ "doc_path": "rules/maximumindependentset_gridgraph/index.html" }, { - "source": 30, - "target": 31, + "source": 31, + "target": 32, "overhead": [ { "field": "num_vertices", @@ -908,8 +926,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 30, - "target": 32, + "source": 31, + "target": 33, "overhead": [ { "field": "num_vertices", @@ -923,8 +941,8 @@ "doc_path": "rules/maximumindependentset_triangular/index.html" }, { - "source": 30, - "target": 36, + "source": 31, + "target": 37, "overhead": [ { "field": "num_sets", @@ -938,8 +956,8 @@ "doc_path": "rules/maximumindependentset_maximumsetpacking/index.html" }, { - "source": 31, - "target": 27, + "source": 32, + "target": 28, "overhead": [ { "field": "num_vertices", @@ -953,8 +971,8 @@ "doc_path": "rules/maximumindependentset_maximumclique/index.html" }, { - "source": 31, - "target": 38, + "source": 32, + "target": 39, "overhead": [ { "field": "num_sets", @@ -968,8 +986,8 @@ "doc_path": "rules/maximumindependentset_maximumsetpacking/index.html" }, { - "source": 31, - "target": 45, + "source": 32, + "target": 46, "overhead": [ { "field": "num_vertices", @@ -983,8 +1001,8 @@ "doc_path": "rules/minimumvertexcover_maximumindependentset/index.html" }, { - "source": 32, - "target": 34, + "source": 33, + "target": 35, "overhead": [ { "field": "num_vertices", @@ -998,8 +1016,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 33, - "target": 30, + "source": 34, + "target": 31, "overhead": [ { "field": "num_vertices", @@ -1013,8 +1031,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 33, - "target": 34, + "source": 34, + "target": 35, "overhead": [ { "field": "num_vertices", @@ -1028,8 +1046,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 34, - "target": 31, + "source": 35, + "target": 32, "overhead": [ { "field": "num_vertices", @@ -1043,8 +1061,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 35, - "target": 12, + "source": 36, + "target": 13, "overhead": [ { "field": "num_vars", @@ -1058,8 +1076,8 @@ "doc_path": "rules/maximummatching_ilp/index.html" }, { - "source": 35, - "target": 38, + "source": 36, + "target": 39, "overhead": [ { "field": "num_sets", @@ -1073,8 +1091,8 @@ "doc_path": "rules/maximummatching_maximumsetpacking/index.html" }, { - "source": 36, - "target": 30, + "source": 37, + "target": 31, "overhead": [ { "field": "num_vertices", @@ -1088,8 +1106,8 @@ "doc_path": "rules/maximumindependentset_maximumsetpacking/index.html" }, { - "source": 36, - "target": 38, + "source": 37, + "target": 39, "overhead": [ { "field": "num_sets", @@ -1103,8 +1121,8 @@ "doc_path": "rules/maximumsetpacking_casts/index.html" }, { - "source": 37, - "target": 49, + "source": 38, + "target": 50, "overhead": [ { "field": "num_vars", @@ -1114,8 +1132,8 @@ "doc_path": "rules/maximumsetpacking_qubo/index.html" }, { - "source": 38, - "target": 12, + "source": 39, + "target": 13, "overhead": [ { "field": "num_vars", @@ -1129,8 +1147,8 @@ "doc_path": "rules/maximumsetpacking_ilp/index.html" }, { - "source": 38, - "target": 31, + "source": 39, + "target": 32, "overhead": [ { "field": "num_vertices", @@ -1144,8 +1162,8 @@ "doc_path": "rules/maximumindependentset_maximumsetpacking/index.html" }, { - "source": 38, - "target": 37, + "source": 39, + "target": 38, "overhead": [ { "field": "num_sets", @@ -1159,8 +1177,8 @@ "doc_path": "rules/maximumsetpacking_casts/index.html" }, { - "source": 39, - "target": 12, + "source": 40, + "target": 13, "overhead": [ { "field": "num_vars", @@ -1174,8 +1192,8 @@ "doc_path": "rules/minimumdominatingset_ilp/index.html" }, { - "source": 42, - "target": 12, + "source": 43, + "target": 13, "overhead": [ { "field": "num_vars", @@ -1189,8 +1207,8 @@ "doc_path": "rules/minimumsetcovering_ilp/index.html" }, { - "source": 45, - "target": 31, + "source": 46, + "target": 32, "overhead": [ { "field": "num_vertices", @@ -1204,8 +1222,8 @@ "doc_path": "rules/minimumvertexcover_maximumindependentset/index.html" }, { - "source": 45, - "target": 42, + "source": 46, + "target": 43, "overhead": [ { "field": "num_sets", @@ -1219,8 +1237,8 @@ "doc_path": "rules/minimumvertexcover_minimumsetcovering/index.html" }, { - "source": 49, - "target": 12, + "source": 50, + "target": 13, "overhead": [ { "field": "num_vars", @@ -1234,8 +1252,8 @@ "doc_path": "rules/qubo_ilp/index.html" }, { - "source": 49, - "target": 53, + "source": 50, + "target": 54, "overhead": [ { "field": "num_spins", @@ -1245,7 +1263,7 @@ "doc_path": "rules/spinglass_qubo/index.html" }, { - "source": 51, + "source": 52, "target": 4, "overhead": [ { @@ -1260,8 +1278,8 @@ "doc_path": "rules/sat_circuitsat/index.html" }, { - "source": 51, - "target": 16, + "source": 52, + "target": 17, "overhead": [ { "field": "num_vertices", @@ -1275,8 +1293,8 @@ "doc_path": "rules/sat_coloring/index.html" }, { - "source": 51, - "target": 21, + "source": 52, + "target": 22, "overhead": [ { "field": "num_clauses", @@ -1290,8 +1308,8 @@ "doc_path": "rules/sat_ksat/index.html" }, { - "source": 51, - "target": 30, + "source": 52, + "target": 31, "overhead": [ { "field": "num_vertices", @@ -1305,8 +1323,8 @@ "doc_path": "rules/sat_maximumindependentset/index.html" }, { - "source": 51, - "target": 39, + "source": 52, + "target": 40, "overhead": [ { "field": "num_vertices", @@ -1320,8 +1338,8 @@ "doc_path": "rules/sat_minimumdominatingset/index.html" }, { - "source": 53, - "target": 49, + "source": 54, + "target": 50, "overhead": [ { "field": "num_vars", @@ -1331,8 +1349,8 @@ "doc_path": "rules/spinglass_qubo/index.html" }, { - "source": 54, - "target": 25, + "source": 55, + "target": 26, "overhead": [ { "field": "num_vertices", @@ -1346,8 +1364,8 @@ "doc_path": "rules/spinglass_maxcut/index.html" }, { - "source": 54, - "target": 53, + "source": 55, + "target": 54, "overhead": [ { "field": "num_spins", @@ -1361,8 +1379,8 @@ "doc_path": "rules/spinglass_casts/index.html" }, { - "source": 59, - "target": 12, + "source": 60, + "target": 13, "overhead": [ { "field": "num_vars", @@ -1376,8 +1394,8 @@ "doc_path": "rules/travelingsalesman_ilp/index.html" }, { - "source": 59, - "target": 49, + "source": 60, + "target": 50, "overhead": [ { "field": "num_vars", @@ -1387,4 +1405,4 @@ "doc_path": "rules/travelingsalesman_qubo/index.html" } ] -} \ No newline at end of file +} diff --git a/problemreductions-cli/src/cli.rs b/problemreductions-cli/src/cli.rs index e2dbd5b5..055ab27c 100644 --- a/problemreductions-cli/src/cli.rs +++ b/problemreductions-cli/src/cli.rs @@ -233,6 +233,7 @@ Flags by problem type: X3C (ExactCoverBy3Sets) --universe, --sets (3 elements each) BicliqueCover --left, --right, --biedges, --k BMF --matrix (0/1), --rank + ConsecutiveBlockMinimization --matrix (JSON 2D bool), --bound-k SteinerTree --graph, --edge-weights, --terminals CVP --basis, --target-vec [--bounds] OptimalLinearArrangement --graph, --bound @@ -407,6 +408,10 @@ pub struct CreateArgs { /// Alphabet size for SCS (optional; inferred from max symbol + 1 if omitted) #[arg(long)] pub alphabet_size: Option, + + /// Upper bound K on consecutive blocks for ConsecutiveBlockMinimization + #[arg(long)] + pub bound_k: Option, } #[derive(clap::Args)] diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index db0e6c58..30360d67 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -5,7 +5,9 @@ use crate::problem_name::{resolve_problem_ref, unknown_problem_error}; use crate::util; use anyhow::{bail, Context, Result}; use problemreductions::export::{ModelExample, ProblemRef, ProblemSide, RuleExample}; -use problemreductions::models::algebraic::{ClosestVectorProblem, BMF}; +use problemreductions::models::algebraic::{ + ClosestVectorProblem, ConsecutiveBlockMinimization, BMF, +}; use problemreductions::models::graph::{GraphPartitioning, HamiltonianPath}; use problemreductions::models::misc::{ BinPacking, FlowShopScheduling, LongestCommonSubsequence, MinimumTardinessSequencing, @@ -64,6 +66,7 @@ fn all_data_flags_empty(args: &CreateArgs) -> bool { && args.deadline.is_none() && args.num_processors.is_none() && args.alphabet_size.is_none() + && args.bound_k.is_none() } fn emit_problem_output(output: &ProblemJsonOutput, out: &OutputConfig) -> Result<()> { @@ -249,6 +252,9 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str { "SubgraphIsomorphism" => "--graph 0-1,1-2,2-0 --pattern 0-1", "SubsetSum" => "--sizes 3,7,1,8,2,4 --target 11", "ShortestCommonSupersequence" => "--strings \"0,1,2;1,2,0\" --bound 4", + "ConsecutiveBlockMinimization" => { + "--matrix '[[true,false,true],[false,true,true]]' --bound-k 2" + } _ => "", } } @@ -752,6 +758,28 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { (ser(BMF::new(matrix, rank))?, resolved_variant.clone()) } + // ConsecutiveBlockMinimization + "ConsecutiveBlockMinimization" => { + let matrix_str = args.matrix.as_deref().ok_or_else(|| { + anyhow::anyhow!( + "ConsecutiveBlockMinimization requires --matrix and --bound-k\n\n\ + Usage: pred create ConsecutiveBlockMinimization --matrix '[[true,false,true],[false,true,true]]' --bound-k 2" + ) + })?; + let bound_k = args.bound_k.ok_or_else(|| { + anyhow::anyhow!( + "ConsecutiveBlockMinimization requires --bound-k\n\n\ + Usage: pred create ConsecutiveBlockMinimization --matrix '[[true,false,true],[false,true,true]]' --bound-k 2" + ) + })?; + let matrix: Vec> = serde_json::from_str(matrix_str) + .context("Failed to parse --matrix as JSON 2D bool array")?; + ( + ser(ConsecutiveBlockMinimization::new(matrix, bound_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 83015b13..b509fad3 100644 --- a/src/example_db/fixtures/examples.json +++ b/src/example_db/fixtures/examples.json @@ -4,6 +4,7 @@ {"problem":"BicliqueCover","variant":{},"instance":{"graph":{"edges":[[0,0],[0,1],[1,1],[1,2]],"left_size":2,"right_size":3},"k":2},"samples":[{"config":[1,0,0,1,1,0,1,1,0,1],"metric":{"Valid":6}}],"optimal":[{"config":[0,1,0,1,0,1,0,1,0,1],"metric":{"Valid":5}},{"config":[1,0,1,0,1,0,1,0,1,0],"metric":{"Valid":5}}]}, {"problem":"CircuitSAT","variant":{},"instance":{"circuit":{"assignments":[{"expr":{"op":{"And":[{"op":{"Var":"x1"}},{"op":{"Var":"x2"}}]}},"outputs":["a"]},{"expr":{"op":{"Or":[{"op":{"Var":"x1"}},{"op":{"Var":"x2"}}]}},"outputs":["b"]},{"expr":{"op":{"Xor":[{"op":{"Var":"a"}},{"op":{"Var":"b"}}]}},"outputs":["c"]}]},"variables":["a","b","c","x1","x2"]},"samples":[{"config":[0,1,1,0,1],"metric":true},{"config":[0,1,1,1,0],"metric":true}],"optimal":[{"config":[0,0,0,0,0],"metric":true},{"config":[0,1,1,0,1],"metric":true},{"config":[0,1,1,1,0],"metric":true},{"config":[1,1,0,1,1],"metric":true}]}, {"problem":"ClosestVectorProblem","variant":{"weight":"i32"},"instance":{"basis":[[2,0],[1,2]],"bounds":[{"lower":-2,"upper":4},{"lower":-2,"upper":4}],"target":[2.8,1.5]},"samples":[{"config":[3,3],"metric":{"Valid":0.5385164807134505}}],"optimal":[{"config":[3,3],"metric":{"Valid":0.5385164807134505}}]}, + {"problem":"ConsecutiveBlockMinimization","variant":{},"instance":{"bound_k":2,"matrix":[[true,false,true],[false,true,true]],"num_cols":3,"num_rows":2},"samples":[{"config":[0,2,1],"metric":true}],"optimal":[{"config":[0,2,1],"metric":true},{"config":[1,2,0],"metric":true}]}, {"problem":"ExactCoverBy3Sets","variant":{},"instance":{"subsets":[[0,1,2],[0,2,4],[3,4,5],[3,5,7],[6,7,8],[1,4,6],[2,5,8]],"universe_size":9},"samples":[{"config":[1,0,1,0,1,0,0],"metric":true}],"optimal":[{"config":[1,0,1,0,1,0,0],"metric":true}]}, {"problem":"Factoring","variant":{},"instance":{"m":2,"n":3,"target":15},"samples":[{"config":[1,1,1,0,1],"metric":{"Valid":0}}],"optimal":[{"config":[1,1,1,0,1],"metric":{"Valid":0}}]}, {"problem":"HamiltonianPath","variant":{"graph":"SimpleGraph"},"instance":{"graph":{"inner":{"edge_property":"undirected","edges":[[0,1,null],[0,2,null],[1,3,null],[2,3,null],[3,4,null],[3,5,null],[4,2,null],[5,1,null]],"node_holes":[],"nodes":[null,null,null,null,null,null]}}},"samples":[{"config":[0,2,4,3,1,5],"metric":true}],"optimal":[{"config":[0,1,5,3,2,4],"metric":true},{"config":[0,1,5,3,4,2],"metric":true},{"config":[0,2,4,3,1,5],"metric":true},{"config":[0,2,4,3,5,1],"metric":true},{"config":[1,0,2,4,3,5],"metric":true},{"config":[1,5,3,4,2,0],"metric":true},{"config":[2,0,1,5,3,4],"metric":true},{"config":[2,4,3,5,1,0],"metric":true},{"config":[3,4,2,0,1,5],"metric":true},{"config":[3,5,1,0,2,4],"metric":true},{"config":[4,2,0,1,3,5],"metric":true},{"config":[4,2,0,1,5,3],"metric":true},{"config":[4,2,3,5,1,0],"metric":true},{"config":[4,3,2,0,1,5],"metric":true},{"config":[4,3,5,1,0,2],"metric":true},{"config":[5,1,0,2,3,4],"metric":true},{"config":[5,1,0,2,4,3],"metric":true},{"config":[5,1,3,4,2,0],"metric":true},{"config":[5,3,1,0,2,4],"metric":true},{"config":[5,3,4,2,0,1],"metric":true}]}, diff --git a/src/models/algebraic/consecutive_block_minimization.rs b/src/models/algebraic/consecutive_block_minimization.rs new file mode 100644 index 00000000..f53be480 --- /dev/null +++ b/src/models/algebraic/consecutive_block_minimization.rs @@ -0,0 +1,207 @@ +//! Consecutive Block Minimization (CBM) problem implementation. +//! +//! Given an m x n binary matrix A and a positive integer K, +//! determine whether there exists a permutation of the columns of A +//! such that the resulting matrix has at most K maximal blocks of +//! consecutive 1-entries (summed over all rows). +//! +//! A "block" is a maximal contiguous run of 1-entries in a row. +//! This is problem SR17 in Garey & Johnson. + +use crate::registry::{FieldInfo, ProblemSchemaEntry}; +use crate::traits::{Problem, SatisfactionProblem}; +use serde::{Deserialize, Serialize}; + +inventory::submit! { + ProblemSchemaEntry { + name: "ConsecutiveBlockMinimization", + display_name: "Consecutive Block Minimization", + aliases: &["CBM"], + dimensions: &[], + module_path: module_path!(), + description: "Permute columns of a binary matrix to have at most K consecutive blocks of 1s", + fields: &[ + FieldInfo { name: "matrix", type_name: "Vec>", description: "Binary matrix A (m x n)" }, + FieldInfo { name: "bound_k", type_name: "usize", description: "Upper bound K on total consecutive blocks" }, + ], + } +} + +/// Consecutive Block Minimization (CBM) problem. +/// +/// Given an m x n binary matrix A and a positive integer K, +/// determine whether there exists a permutation of the columns of A +/// such that the resulting matrix has at most K maximal blocks of +/// consecutive 1-entries (summed over all rows). +/// +/// # Example +/// +/// ``` +/// use problemreductions::models::algebraic::ConsecutiveBlockMinimization; +/// use problemreductions::{Problem, Solver, BruteForce}; +/// +/// // 2x3 binary matrix +/// let problem = ConsecutiveBlockMinimization::new( +/// vec![ +/// vec![true, false, true], +/// vec![false, true, true], +/// ], +/// 2, +/// ); +/// +/// let solver = BruteForce::new(); +/// let solutions = solver.find_all_satisfying(&problem); +/// +/// // Verify solutions satisfy the block bound +/// for sol in solutions { +/// assert!(problem.evaluate(&sol)); +/// } +/// ``` +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ConsecutiveBlockMinimization { + /// The binary matrix A (m x n). + matrix: Vec>, + /// Number of rows (m). + num_rows: usize, + /// Number of columns (n). + num_cols: usize, + /// Upper bound K on total consecutive blocks. + bound_k: usize, +} + +impl ConsecutiveBlockMinimization { + /// Create a new ConsecutiveBlockMinimization problem. + /// + /// # Arguments + /// * `matrix` - The m x n binary matrix + /// * `bound_k` - Upper bound on total consecutive blocks + /// + /// # Panics + /// Panics if rows have inconsistent lengths. + pub fn new(matrix: Vec>, bound_k: usize) -> Self { + let num_rows = matrix.len(); + let num_cols = if num_rows > 0 { matrix[0].len() } else { 0 }; + + for row in &matrix { + assert_eq!(row.len(), num_cols, "All rows must have the same length"); + } + + Self { + matrix, + num_rows, + num_cols, + bound_k, + } + } + + /// Get the binary matrix. + pub fn matrix(&self) -> &[Vec] { + &self.matrix + } + + /// Get the number of rows. + pub fn num_rows(&self) -> usize { + self.num_rows + } + + /// Get the number of columns. + pub fn num_cols(&self) -> usize { + self.num_cols + } + + /// Get the upper bound K. + pub fn bound_k(&self) -> usize { + self.bound_k + } + + /// Count the total number of maximal consecutive blocks of 1s + /// when columns are permuted according to `config`. + /// + /// `config[position] = column_index` defines the column permutation. + /// Returns `Some(total_blocks)` if the config is a valid permutation, + /// or `None` if it is not (wrong length, duplicate columns, or out-of-range). + pub fn count_consecutive_blocks(&self, config: &[usize]) -> Option { + if config.len() != self.num_cols { + return None; + } + + // Validate permutation: all values distinct and in 0..num_cols. + let mut seen = vec![false; self.num_cols]; + for &col in config { + if col >= self.num_cols || seen[col] { + return None; + } + seen[col] = true; + } + + let mut total_blocks = 0; + for row in &self.matrix { + let mut in_block = false; + for &pos in config { + if row[pos] { + if !in_block { + total_blocks += 1; + in_block = true; + } + } else { + in_block = false; + } + } + } + + Some(total_blocks) + } +} + +impl Problem for ConsecutiveBlockMinimization { + const NAME: &'static str = "ConsecutiveBlockMinimization"; + type Metric = bool; + + fn dims(&self) -> Vec { + vec![self.num_cols; self.num_cols] + } + + fn evaluate(&self, config: &[usize]) -> bool { + if self.num_cols == 0 { + // Empty matrix: zero blocks, always satisfies any bound. + return true; + } + + match self.count_consecutive_blocks(config) { + Some(total) => total <= self.bound_k, + None => false, + } + } + + fn variant() -> Vec<(&'static str, &'static str)> { + crate::variant_params![] + } + + fn num_variables(&self) -> usize { + self.num_cols + } +} + +impl SatisfactionProblem for ConsecutiveBlockMinimization {} + +crate::declare_variants! { + default sat ConsecutiveBlockMinimization => "factorial(num_cols) * num_rows * num_cols", +} + +#[cfg(feature = "example-db")] +pub(crate) fn canonical_model_example_specs() -> Vec { + vec![crate::example_db::specs::ModelExampleSpec { + id: "consecutive_block_minimization", + build: || { + let problem = ConsecutiveBlockMinimization::new( + vec![vec![true, false, true], vec![false, true, true]], + 2, + ); + crate::example_db::specs::satisfaction_example(problem, vec![vec![0, 2, 1]]) + }, + }] +} + +#[cfg(test)] +#[path = "../../unit_tests/models/algebraic/consecutive_block_minimization.rs"] +mod tests; diff --git a/src/models/algebraic/mod.rs b/src/models/algebraic/mod.rs index a4945487..56727d2b 100644 --- a/src/models/algebraic/mod.rs +++ b/src/models/algebraic/mod.rs @@ -5,14 +5,17 @@ //! - [`ILP`]: Integer Linear Programming //! - [`ClosestVectorProblem`]: Closest Vector Problem (minimize lattice distance) //! - [`BMF`]: Boolean Matrix Factorization +//! - [`ConsecutiveBlockMinimization`]: Consecutive Block Minimization pub(crate) mod bmf; pub(crate) mod closest_vector_problem; +pub(crate) mod consecutive_block_minimization; pub(crate) mod ilp; pub(crate) mod qubo; pub use bmf::BMF; pub use closest_vector_problem::{ClosestVectorProblem, VarBounds}; +pub use consecutive_block_minimization::ConsecutiveBlockMinimization; pub use ilp::{Comparison, LinearConstraint, ObjectiveSense, VariableDomain, ILP}; pub use qubo::QUBO; @@ -23,5 +26,6 @@ pub(crate) fn canonical_model_example_specs() -> Vec 1 block + // [0, 1, 1] -> 1 block + // Total = 2 blocks, bound_k = 2 => satisfies + let problem = ConsecutiveBlockMinimization::new( + vec![vec![true, false, true], vec![false, true, true]], + 2, + ); + assert!(problem.evaluate(&[0, 2, 1])); + + // Identity permutation [0, 1, 2]: + // [1, 0, 1] -> 2 blocks + // [0, 1, 1] -> 1 block + // Total = 3 blocks, bound_k = 2 => does not satisfy + assert!(!problem.evaluate(&[0, 1, 2])); +} + +#[test] +fn test_consecutive_block_minimization_count_blocks() { + let problem = ConsecutiveBlockMinimization::new( + vec![vec![true, false, true], vec![false, true, true]], + 2, + ); + assert_eq!(problem.count_consecutive_blocks(&[0, 2, 1]), Some(2)); + assert_eq!(problem.count_consecutive_blocks(&[0, 1, 2]), Some(3)); + // Invalid: duplicate column + assert_eq!(problem.count_consecutive_blocks(&[0, 0, 1]), None); + // Invalid: wrong length + assert_eq!(problem.count_consecutive_blocks(&[0, 1]), None); + // Invalid: out of range + assert_eq!(problem.count_consecutive_blocks(&[0, 1, 5]), None); +} + +#[test] +fn test_consecutive_block_minimization_brute_force() { + let problem = ConsecutiveBlockMinimization::new( + vec![vec![true, false, true], vec![false, true, true]], + 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_consecutive_block_minimization_empty_matrix() { + let problem = ConsecutiveBlockMinimization::new(vec![], 0); + assert_eq!(problem.num_rows(), 0); + assert_eq!(problem.num_cols(), 0); + assert!(problem.evaluate(&[])); +} + +#[test] +fn test_consecutive_block_minimization_serialization() { + let problem = ConsecutiveBlockMinimization::new(vec![vec![true, false], vec![false, true]], 2); + let json = serde_json::to_string(&problem).unwrap(); + let deserialized: ConsecutiveBlockMinimization = serde_json::from_str(&json).unwrap(); + assert_eq!(deserialized.num_rows(), problem.num_rows()); + assert_eq!(deserialized.num_cols(), problem.num_cols()); + assert_eq!(deserialized.bound_k(), problem.bound_k()); + assert_eq!(deserialized.matrix(), problem.matrix()); +} + +#[test] +fn test_consecutive_block_minimization_invalid_permutation() { + let problem = ConsecutiveBlockMinimization::new(vec![vec![true, false], vec![false, true]], 2); + // Not a valid permutation => evaluate returns false + assert!(!problem.evaluate(&[0, 0])); + // Wrong length + assert!(!problem.evaluate(&[0])); +} diff --git a/src/unit_tests/trait_consistency.rs b/src/unit_tests/trait_consistency.rs index 122362ef..2d8d8c41 100644 --- a/src/unit_tests/trait_consistency.rs +++ b/src/unit_tests/trait_consistency.rs @@ -84,6 +84,10 @@ fn test_all_problems_implement_trait_correctly() { ); check_problem_trait(&PaintShop::new(vec!["a", "a"]), "PaintShop"); check_problem_trait(&BMF::new(vec![vec![true]], 1), "BMF"); + check_problem_trait( + &ConsecutiveBlockMinimization::new(vec![vec![true, false], vec![false, true]], 2), + "ConsecutiveBlockMinimization", + ); check_problem_trait( &BicliqueCover::new(BipartiteGraph::new(2, 2, vec![(0, 0)]), 1), "BicliqueCover", From f351eb0d9f08065c3828f74941fa7763e97bd299 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Mon, 16 Mar 2026 21:08:14 +0800 Subject: [PATCH 3/6] chore: remove plan file after implementation --- ...26-03-16-consecutive-block-minimization.md | 167 ------------------ 1 file changed, 167 deletions(-) delete mode 100644 docs/plans/2026-03-16-consecutive-block-minimization.md diff --git a/docs/plans/2026-03-16-consecutive-block-minimization.md b/docs/plans/2026-03-16-consecutive-block-minimization.md deleted file mode 100644 index 5108c839..00000000 --- a/docs/plans/2026-03-16-consecutive-block-minimization.md +++ /dev/null @@ -1,167 +0,0 @@ -# Plan: Add ConsecutiveBlockMinimization Model (#420) - -## Summary - -Add the Consecutive Block Minimization satisfaction problem (Garey & Johnson SR17) to the `algebraic/` category. Given an m×n binary matrix A and positive integer K, decide whether there exists a column permutation yielding at most K maximal blocks of consecutive 1's across all rows. - -## Issue Details - -- **Problem**: ConsecutiveBlockMinimization -- **Type**: SatisfactionProblem (Metric = bool) -- **Category**: algebraic/ (matrix input) -- **No type parameters** (no graph type, no weight type) -- **NP-complete**: Kou (1977), reduction from Hamiltonian Path -- **Complexity**: O(n^n × m × n) brute-force - -## Batch 1: Implementation (Steps 1-5.5) - -### Step 1: Category - -Place in `src/models/algebraic/` alongside BMF, QUBO, ILP, CVP. - -### Step 1.5: Size Getters - -Getter methods needed (from complexity expression and overhead expressions): -- `num_rows()` → m (matrix.len()) -- `num_cols()` → n (matrix[0].len()) -- `bound_k()` → K -- `num_variables()` → num_cols (override default) - -### Step 2: Model File - -Create `src/models/algebraic/consecutive_block_minimization.rs`: - -**Schema entry** (inventory::submit!): -- name: "ConsecutiveBlockMinimization" -- display_name: "ConsecutiveBlockMinimization" -- aliases: &["CBM"] -- dimensions: &[] (no type params) -- fields: matrix (Vec>), bound_k (usize) - -**Struct**: -```rust -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ConsecutiveBlockMinimization { - matrix: Vec>, - num_rows: usize, - num_cols: usize, - bound_k: usize, -} -``` - -**Constructor** `new(matrix, bound_k)`: -- Validate all rows have same length -- Store derived num_rows, num_cols - -**Accessors**: `matrix()`, `num_rows()`, `num_cols()`, `bound_k()` - -**Helper** `count_blocks(config)`: -- Interpret config as column permutation (config[i] = position of column i) -- Validate it's a valid permutation (all distinct, in 0..num_cols) -- For each row, count maximal runs of consecutive 1's after reordering -- Return Option: None if invalid permutation, Some(total) otherwise - -**Problem impl**: -- `NAME = "ConsecutiveBlockMinimization"` -- `Metric = bool` -- `dims() = vec![num_cols; num_cols]` -- `evaluate(config)`: call count_blocks, return false if invalid permutation, otherwise total <= bound_k -- `variant() = crate::variant_params![]` -- Override `num_variables()` → num_cols - -**SatisfactionProblem**: empty marker impl - -### Step 2.5: declare_variants! - -```rust -crate::declare_variants! { - default sat ConsecutiveBlockMinimization => "num_cols^num_cols * num_rows * num_cols", -} -``` - -### Step 3: Register in Modules - -1. `src/models/algebraic/mod.rs`: - - Add `pub(crate) mod consecutive_block_minimization;` - - Add `pub use consecutive_block_minimization::ConsecutiveBlockMinimization;` - - Update module doc comment - - Add to `canonical_model_example_specs()` aggregator - -2. `src/models/mod.rs`: - - Re-export `ConsecutiveBlockMinimization` from `algebraic` - -### Step 4: CLI Registration - -1. **Aliases**: Already handled via `declare_variants!` + ProblemSchemaEntry aliases field ("CBM") - -2. **`problemreductions-cli/src/commands/create.rs`**: - - Add match arm for "ConsecutiveBlockMinimization" - - Parse `--matrix` (JSON 2D bool array) and `--bound-k` (integer) - - Add to `after_help` flag table - - Add to `all_data_flags_empty` - - Add to `example_for` with a small example instance - -3. **`problemreductions-cli/src/cli.rs`**: - - Add `--matrix` flag: `Option` for JSON-encoded matrix - - Add `--bound-k` flag: `Option` - -### Step 4.6: Example-DB - -Add `canonical_model_example_specs()` in the model file: -- Use the YES instance from the issue (Instance 2: path graph adjacency matrix, K=6) -- Or use a simpler small instance that has a satisfying permutation -- Use `satisfaction_example(problem, vec![valid_config])` - -### Step 5: Unit Tests - -Create `src/unit_tests/models/algebraic/consecutive_block_minimization.rs`: - -- `test_consecutive_block_minimization_creation`: constructor, getters -- `test_consecutive_block_minimization_evaluation`: test with known YES and NO configs -- `test_consecutive_block_minimization_invalid_permutation`: non-permutation configs return false -- `test_consecutive_block_minimization_serialization`: serde round-trip -- `test_consecutive_block_minimization_solver`: BruteForce::find_satisfying on small instance -- `test_consecutive_block_minimization_paper_example`: test with the issue's example instances -- `test_consecutive_block_minimization_empty_matrix`: edge case - -Add `#[path]` link in model file: -```rust -#[cfg(test)] -#[path = "../../unit_tests/models/algebraic/consecutive_block_minimization.rs"] -mod tests; -``` - -### Step 5.5: Trait Consistency - -Add entry in `src/unit_tests/trait_consistency.rs`: -```rust -check_problem_trait( - &ConsecutiveBlockMinimization::new(matrix, bound_k), - "ConsecutiveBlockMinimization", -); -``` - -## Batch 2: Paper Entry (Step 6) - -### Step 6: Paper Documentation - -In `docs/paper/reductions.typ`: - -1. Add to `display-name` dict: - ```typst - "ConsecutiveBlockMinimization": [Consecutive Block Minimization], - ``` - -2. Add `problem-def` block after related algebraic problems: - - Formal definition: m×n binary matrix, positive integer K, column permutation, blocks - - Background: GJ SR17, information retrieval, scheduling, glass cutting - - Reference to Kou (1977), Booth (1975), Haddadi & Layouni (2008) - - Connection to consecutive ones property (PQ-trees) - -## Verification - -After each batch, run: -```bash -make check # fmt + clippy + test -make paper # build paper -``` From 27a69baa2dce9cd450dc10690e0f4ce9d1873c4a Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Mon, 16 Mar 2026 23:49:42 +0800 Subject: [PATCH 4/6] fix: validate empty CBM permutations --- src/models/algebraic/consecutive_block_minimization.rs | 5 ----- .../models/algebraic/consecutive_block_minimization.rs | 1 + 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/models/algebraic/consecutive_block_minimization.rs b/src/models/algebraic/consecutive_block_minimization.rs index f53be480..2b737659 100644 --- a/src/models/algebraic/consecutive_block_minimization.rs +++ b/src/models/algebraic/consecutive_block_minimization.rs @@ -162,11 +162,6 @@ impl Problem for ConsecutiveBlockMinimization { } fn evaluate(&self, config: &[usize]) -> bool { - if self.num_cols == 0 { - // Empty matrix: zero blocks, always satisfies any bound. - return true; - } - match self.count_consecutive_blocks(config) { Some(total) => total <= self.bound_k, None => false, diff --git a/src/unit_tests/models/algebraic/consecutive_block_minimization.rs b/src/unit_tests/models/algebraic/consecutive_block_minimization.rs index 4e48fe56..79192985 100644 --- a/src/unit_tests/models/algebraic/consecutive_block_minimization.rs +++ b/src/unit_tests/models/algebraic/consecutive_block_minimization.rs @@ -73,6 +73,7 @@ fn test_consecutive_block_minimization_empty_matrix() { assert_eq!(problem.num_rows(), 0); assert_eq!(problem.num_cols(), 0); assert!(problem.evaluate(&[])); + assert!(!problem.evaluate(&[0])); } #[test] From 79204ff4d336b6e9535391a59cb61f96700ae189 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Tue, 17 Mar 2026 00:08:16 +0800 Subject: [PATCH 5/6] fix: harden CBM CLI and deserialization --- problemreductions-cli/src/cli.rs | 3 +- problemreductions-cli/src/commands/create.rs | 16 +++-- problemreductions-cli/tests/cli_tests.rs | 66 +++++++++++++++++++ .../consecutive_block_minimization.rs | 57 +++++++++++++--- .../consecutive_block_minimization.rs | 14 +++- 5 files changed, 138 insertions(+), 18 deletions(-) diff --git a/problemreductions-cli/src/cli.rs b/problemreductions-cli/src/cli.rs index 11ce16ef..cd274450 100644 --- a/problemreductions-cli/src/cli.rs +++ b/problemreductions-cli/src/cli.rs @@ -318,7 +318,8 @@ 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. QUBO uses semicolon-separated numeric rows ("1,0.5;0.5,2"); + /// ConsecutiveBlockMinimization uses a JSON 2D bool array ('[[true,false],[false,true]]') #[arg(long)] pub matrix: Option, /// Number of colors for KColoring diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index 4123b674..dc36a2dd 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -218,6 +218,7 @@ fn type_format_hint(type_name: &str, graph_type: Option<&str>) -> &'static str { "Vec" => "comma-separated integers: 1,1,2", "Vec" => "comma-separated: 1,2,3", "Vec" => "semicolon-separated clauses: \"1,2;-1,3\"", + "Vec>" => "JSON 2D bool array: '[[true,false],[false,true]]'", "Vec>" => "semicolon-separated rows: \"1,0.5;0.5,2\"", "usize" => "integer", "u64" => "integer", @@ -989,22 +990,23 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { // ConsecutiveBlockMinimization "ConsecutiveBlockMinimization" => { + let usage = "Usage: pred create ConsecutiveBlockMinimization --matrix '[[true,false,true],[false,true,true]]' --bound-k 2"; let matrix_str = args.matrix.as_deref().ok_or_else(|| { anyhow::anyhow!( - "ConsecutiveBlockMinimization requires --matrix and --bound-k\n\n\ - Usage: pred create ConsecutiveBlockMinimization --matrix '[[true,false,true],[false,true,true]]' --bound-k 2" + "ConsecutiveBlockMinimization requires --matrix as a JSON 2D bool array and --bound-k\n\n{usage}" ) })?; let bound_k = args.bound_k.ok_or_else(|| { + anyhow::anyhow!("ConsecutiveBlockMinimization requires --bound-k\n\n{usage}") + })?; + let matrix: Vec> = serde_json::from_str(matrix_str).map_err(|err| { anyhow::anyhow!( - "ConsecutiveBlockMinimization requires --bound-k\n\n\ - Usage: pred create ConsecutiveBlockMinimization --matrix '[[true,false,true],[false,true,true]]' --bound-k 2" + "ConsecutiveBlockMinimization requires --matrix as a JSON 2D bool array (e.g., '[[true,false,true],[false,true,true]]')\n\n{usage}\n\nFailed to parse --matrix: {err}" ) })?; - let matrix: Vec> = serde_json::from_str(matrix_str) - .context("Failed to parse --matrix as JSON 2D bool array")?; ( - ser(ConsecutiveBlockMinimization::new(matrix, bound_k))?, + ser(ConsecutiveBlockMinimization::try_new(matrix, bound_k) + .map_err(|err| anyhow::anyhow!("{err}\n\n{usage}"))?)?, resolved_variant.clone(), ) } diff --git a/problemreductions-cli/tests/cli_tests.rs b/problemreductions-cli/tests/cli_tests.rs index 8d481fcc..6947c6d8 100644 --- a/problemreductions-cli/tests/cli_tests.rs +++ b/problemreductions-cli/tests/cli_tests.rs @@ -310,6 +310,31 @@ fn test_evaluate_sat() { std::fs::remove_file(&tmp).ok(); } +#[test] +fn test_evaluate_consecutive_block_minimization_rejects_inconsistent_dimensions() { + let problem_json = r#"{ + "type": "ConsecutiveBlockMinimization", + "data": { + "matrix": [[true]], + "num_rows": 1, + "num_cols": 2, + "bound_k": 1 + } + }"#; + let tmp = std::env::temp_dir().join("pred_test_eval_cbm_invalid_dims.json"); + std::fs::write(&tmp, problem_json).unwrap(); + + let output = pred() + .args(["evaluate", tmp.to_str().unwrap(), "--config", "0,1"]) + .output() + .unwrap(); + assert!(!output.status.success()); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("num_cols must match matrix column count")); + assert!(!stderr.contains("panicked at"), "stderr: {stderr}"); + std::fs::remove_file(&tmp).ok(); +} + #[test] fn test_create_undirected_two_commodity_integral_flow() { let output = pred() @@ -510,6 +535,47 @@ fn test_create_undirected_two_commodity_integral_flow_rejects_out_of_range_termi assert!(!stderr.contains("panicked at"), "stderr: {stderr}"); } +#[test] +fn test_create_consecutive_block_minimization_rejects_ragged_matrix() { + let output = pred() + .args([ + "create", + "ConsecutiveBlockMinimization", + "--matrix", + "[[true],[true,false]]", + "--bound-k", + "2", + ]) + .output() + .unwrap(); + assert!(!output.status.success()); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("all matrix rows must have the same length")); + assert!(stderr.contains("Usage: pred create ConsecutiveBlockMinimization")); + assert!(!stderr.contains("panicked at"), "stderr: {stderr}"); +} + +#[test] +fn test_create_consecutive_block_minimization_help_mentions_json_matrix_format() { + let output = pred() + .args(["create", "ConsecutiveBlockMinimization"]) + .output() + .unwrap(); + assert!(!output.status.success()); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("JSON 2D bool array")); + assert!(stderr.contains("[[true,false,true],[false,true,true]]")); +} + +#[test] +fn test_create_help_mentions_consecutive_block_minimization_matrix_format() { + let output = pred().args(["create", "--help"]).output().unwrap(); + assert!(output.status.success()); + let stdout = String::from_utf8(output.stdout).unwrap(); + assert!(stdout.contains("ConsecutiveBlockMinimization")); + assert!(stdout.contains("JSON 2D bool array")); +} + #[test] fn test_reduce() { let problem_json = r#"{ diff --git a/src/models/algebraic/consecutive_block_minimization.rs b/src/models/algebraic/consecutive_block_minimization.rs index 2b737659..ba83a91a 100644 --- a/src/models/algebraic/consecutive_block_minimization.rs +++ b/src/models/algebraic/consecutive_block_minimization.rs @@ -58,6 +58,7 @@ inventory::submit! { /// } /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "ConsecutiveBlockMinimizationDef")] pub struct ConsecutiveBlockMinimization { /// The binary matrix A (m x n). matrix: Vec>, @@ -79,19 +80,19 @@ impl ConsecutiveBlockMinimization { /// # Panics /// Panics if rows have inconsistent lengths. pub fn new(matrix: Vec>, bound_k: usize) -> Self { - let num_rows = matrix.len(); - let num_cols = if num_rows > 0 { matrix[0].len() } else { 0 }; - - for row in &matrix { - assert_eq!(row.len(), num_cols, "All rows must have the same length"); - } + Self::try_new(matrix, bound_k).unwrap_or_else(|err| panic!("{err}")) + } - Self { + /// Create a new ConsecutiveBlockMinimization problem, returning an error + /// instead of panicking when the matrix is ragged. + pub fn try_new(matrix: Vec>, bound_k: usize) -> Result { + let (num_rows, num_cols) = validate_matrix_dimensions(&matrix)?; + Ok(Self { matrix, num_rows, num_cols, bound_k, - } + }) } /// Get the binary matrix. @@ -183,6 +184,46 @@ crate::declare_variants! { default sat ConsecutiveBlockMinimization => "factorial(num_cols) * num_rows * num_cols", } +#[derive(Debug, Clone, Deserialize)] +struct ConsecutiveBlockMinimizationDef { + matrix: Vec>, + num_rows: usize, + num_cols: usize, + bound_k: usize, +} + +impl TryFrom for ConsecutiveBlockMinimization { + type Error = String; + + fn try_from(value: ConsecutiveBlockMinimizationDef) -> Result { + let problem = Self::try_new(value.matrix, value.bound_k)?; + if value.num_rows != problem.num_rows { + return Err(format!( + "num_rows must match matrix row count ({})", + problem.num_rows + )); + } + if value.num_cols != problem.num_cols { + return Err(format!( + "num_cols must match matrix column count ({})", + problem.num_cols + )); + } + Ok(problem) + } +} + +fn validate_matrix_dimensions(matrix: &[Vec]) -> Result<(usize, usize), String> { + let num_rows = matrix.len(); + let num_cols = matrix.first().map_or(0, Vec::len); + + if matrix.iter().any(|row| row.len() != num_cols) { + return Err("all matrix rows must have the same length".to_string()); + } + + Ok((num_rows, num_cols)) +} + #[cfg(feature = "example-db")] pub(crate) fn canonical_model_example_specs() -> Vec { vec![crate::example_db::specs::ModelExampleSpec { diff --git a/src/unit_tests/models/algebraic/consecutive_block_minimization.rs b/src/unit_tests/models/algebraic/consecutive_block_minimization.rs index 79192985..a4c05488 100644 --- a/src/unit_tests/models/algebraic/consecutive_block_minimization.rs +++ b/src/unit_tests/models/algebraic/consecutive_block_minimization.rs @@ -60,8 +60,11 @@ fn test_consecutive_block_minimization_brute_force() { 2, ); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); - assert!(!solutions.is_empty()); + let mut solutions = solver.find_all_satisfying(&problem); + solutions.sort(); + let mut expected = vec![vec![0, 2, 1], vec![1, 2, 0]]; + expected.sort(); + assert_eq!(solutions, expected); for sol in &solutions { assert!(problem.evaluate(sol)); } @@ -87,6 +90,13 @@ fn test_consecutive_block_minimization_serialization() { assert_eq!(deserialized.matrix(), problem.matrix()); } +#[test] +fn test_consecutive_block_minimization_deserialization_rejects_inconsistent_dimensions() { + let json = r#"{"matrix":[[true]],"num_rows":1,"num_cols":2,"bound_k":1}"#; + let err = serde_json::from_str::(json).unwrap_err(); + assert!(err.to_string().contains("num_cols")); +} + #[test] fn test_consecutive_block_minimization_invalid_permutation() { let problem = ConsecutiveBlockMinimization::new(vec![vec![true, false], vec![false, true]], 2); From 80dd74ba70d069577344686b74be9a2fec358bec Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Tue, 17 Mar 2026 00:29:40 +0800 Subject: [PATCH 6/6] docs: surface CBM CLI usage --- README.md | 11 +++++++++++ docs/src/cli.md | 11 +++++++++++ 2 files changed, 22 insertions(+) diff --git a/README.md b/README.md index 39482bbd..91d1077a 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,17 @@ make cli # builds target/release/pred See the [Getting Started](https://codingthrust.github.io/problem-reductions/getting-started.html) guide for usage examples, the reduction workflow, and [CLI usage](https://codingthrust.github.io/problem-reductions/cli.html). +Try a model directly from the CLI: + +```bash +# Show the Consecutive Block Minimization model (alias: CBM) +pred show CBM + +# Create and solve a small CBM instance (currently with brute-force) +pred create CBM --matrix '[[true,false,true],[false,true,true]]' --bound-k 2 \ + | pred solve - --solver brute-force +``` + ## MCP Server (AI Integration) The `pred` CLI includes a built-in [MCP](https://modelcontextprotocol.io/) server for AI assistant integration (Claude Code, Cursor, Windsurf, OpenCode, etc.). diff --git a/docs/src/cli.md b/docs/src/cli.md index 99e70904..fbe12f3b 100644 --- a/docs/src/cli.md +++ b/docs/src/cli.md @@ -58,6 +58,12 @@ pred create SteinerTree --graph 0-1,0-3,1-2,1-3,2-3,2-4,3-4 --edge-weights 2,5,2 # Create a Length-Bounded Disjoint Paths instance pred create LengthBoundedDisjointPaths --graph 0-1,1-6,0-2,2-3,3-6,0-4,4-5,5-6 --source 0 --sink 6 --num-paths-required 2 --bound 3 -o lbdp.json +# Create a Consecutive Block Minimization instance (alias: CBM) +pred create CBM --matrix '[[true,false,true],[false,true,true]]' --bound-k 2 -o cbm.json + +# CBM currently needs the brute-force solver +pred solve cbm.json --solver brute-force + # Or start from a canonical model example pred create --example MIS/SimpleGraph/i32 -o example.json @@ -288,6 +294,7 @@ pred create MIS --graph 0-1,1-2,2-3 -o problem.json pred create MIS --graph 0-1,1-2,2-3 --weights 2,1,3,1 -o problem.json pred create SAT --num-vars 3 --clauses "1,2;-1,3" -o sat.json pred create QUBO --matrix "1,0.5;0.5,2" -o qubo.json +pred create CBM --matrix '[[true,false,true],[false,true,true]]' --bound-k 2 -o cbm.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 @@ -303,6 +310,10 @@ pred create MinimumTardinessSequencing --n 5 --deadlines 5,5,5,3,3 --precedence- For `LengthBoundedDisjointPaths`, the CLI flag `--bound` maps to the JSON field `max_length`. +For `ConsecutiveBlockMinimization`, the `--matrix` flag expects a JSON 2D bool array such as +`'[[true,false,true],[false,true,true]]'`. The example above shows the accepted shape, and solving +CBM instances currently requires `--solver brute-force`. + Canonical examples are useful when you want a known-good instance from the paper/example database. For model examples, `pred create --example ` emits the canonical instance for that graph node.