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/paper/reductions.typ b/docs/paper/reductions.typ index 76cddb44..7340027c 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -108,6 +108,7 @@ "PartitionIntoTriangles": [Partition Into Triangles], "FlowShopScheduling": [Flow Shop Scheduling], "MinimumTardinessSequencing": [Minimum Tardiness Sequencing], + "ConsecutiveBlockMinimization": [Consecutive Block Minimization], "DirectedTwoCommodityIntegralFlow": [Directed Two-Commodity Integral Flow], ) @@ -1649,6 +1650,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 6482b48a..9170935d 100644 --- a/docs/paper/references.bib +++ b/docs/paper/references.bib @@ -703,3 +703,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/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. diff --git a/docs/src/reductions/problem_schemas.json b/docs/src/reductions/problem_schemas.json index a69a1275..cbda69f8 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": "DirectedTwoCommodityIntegralFlow", "description": "Two-commodity integral flow feasibility on a directed graph", diff --git a/docs/src/reductions/reduction_graph.json b/docs/src/reductions/reduction_graph.json index 5429b350..1a41cc24 100644 --- a/docs/src/reductions/reduction_graph.json +++ b/docs/src/reductions/reduction_graph.json @@ -57,6 +57,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": "DirectedTwoCommodityIntegralFlow", "variant": {}, @@ -569,7 +576,7 @@ "edges": [ { "source": 3, - "target": 13, + "target": 14, "overhead": [ { "field": "num_vars", @@ -584,7 +591,7 @@ }, { "source": 4, - "target": 13, + "target": 14, "overhead": [ { "field": "num_vars", @@ -599,7 +606,7 @@ }, { "source": 4, - "target": 57, + "target": 58, "overhead": [ { "field": "num_spins", @@ -613,7 +620,7 @@ "doc_path": "rules/circuit_spinglass/index.html" }, { - "source": 9, + "source": 10, "target": 4, "overhead": [ { @@ -628,8 +635,8 @@ "doc_path": "rules/factoring_circuit/index.html" }, { - "source": 9, - "target": 14, + "source": 10, + "target": 15, "overhead": [ { "field": "num_vars", @@ -643,8 +650,8 @@ "doc_path": "rules/factoring_ilp/index.html" }, { - "source": 13, - "target": 14, + "source": 14, + "target": 15, "overhead": [ { "field": "num_vars", @@ -658,8 +665,8 @@ "doc_path": "rules/ilp_bool_ilp_i32/index.html" }, { - "source": 13, - "target": 51, + "source": 14, + "target": 52, "overhead": [ { "field": "num_vars", @@ -669,8 +676,8 @@ "doc_path": "rules/ilp_qubo/index.html" }, { - "source": 17, - "target": 20, + "source": 18, + "target": 21, "overhead": [ { "field": "num_vertices", @@ -684,8 +691,8 @@ "doc_path": "rules/kcoloring_casts/index.html" }, { - "source": 20, - "target": 13, + "source": 21, + "target": 14, "overhead": [ { "field": "num_vars", @@ -699,8 +706,8 @@ "doc_path": "rules/coloring_ilp/index.html" }, { - "source": 20, - "target": 51, + "source": 21, + "target": 52, "overhead": [ { "field": "num_vars", @@ -710,8 +717,8 @@ "doc_path": "rules/coloring_qubo/index.html" }, { - "source": 21, - "target": 23, + "source": 22, + "target": 24, "overhead": [ { "field": "num_vars", @@ -725,8 +732,8 @@ "doc_path": "rules/ksatisfiability_casts/index.html" }, { - "source": 21, - "target": 51, + "source": 22, + "target": 52, "overhead": [ { "field": "num_vars", @@ -736,8 +743,8 @@ "doc_path": "rules/ksatisfiability_qubo/index.html" }, { - "source": 22, - "target": 23, + "source": 23, + "target": 24, "overhead": [ { "field": "num_vars", @@ -751,8 +758,8 @@ "doc_path": "rules/ksatisfiability_casts/index.html" }, { - "source": 22, - "target": 51, + "source": 23, + "target": 52, "overhead": [ { "field": "num_vars", @@ -762,8 +769,8 @@ "doc_path": "rules/ksatisfiability_qubo/index.html" }, { - "source": 22, - "target": 61, + "source": 23, + "target": 62, "overhead": [ { "field": "num_elements", @@ -773,8 +780,8 @@ "doc_path": "rules/ksatisfiability_subsetsum/index.html" }, { - "source": 23, - "target": 53, + "source": 24, + "target": 54, "overhead": [ { "field": "num_clauses", @@ -792,8 +799,8 @@ "doc_path": "rules/sat_ksat/index.html" }, { - "source": 24, - "target": 51, + "source": 25, + "target": 52, "overhead": [ { "field": "num_vars", @@ -803,8 +810,8 @@ "doc_path": "rules/knapsack_qubo/index.html" }, { - "source": 26, - "target": 13, + "source": 27, + "target": 14, "overhead": [ { "field": "num_vars", @@ -818,8 +825,8 @@ "doc_path": "rules/longestcommonsubsequence_ilp/index.html" }, { - "source": 27, - "target": 57, + "source": 28, + "target": 58, "overhead": [ { "field": "num_spins", @@ -833,8 +840,8 @@ "doc_path": "rules/spinglass_maxcut/index.html" }, { - "source": 29, - "target": 13, + "source": 30, + "target": 14, "overhead": [ { "field": "num_vars", @@ -848,8 +855,8 @@ "doc_path": "rules/maximumclique_ilp/index.html" }, { - "source": 29, - "target": 33, + "source": 30, + "target": 34, "overhead": [ { "field": "num_vertices", @@ -863,8 +870,8 @@ "doc_path": "rules/maximumclique_maximumindependentset/index.html" }, { - "source": 30, - "target": 31, + "source": 31, + "target": 32, "overhead": [ { "field": "num_vertices", @@ -878,8 +885,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 30, - "target": 35, + "source": 31, + "target": 36, "overhead": [ { "field": "num_vertices", @@ -893,8 +900,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 31, - "target": 36, + "source": 32, + "target": 37, "overhead": [ { "field": "num_vertices", @@ -908,8 +915,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 32, - "target": 30, + "source": 33, + "target": 31, "overhead": [ { "field": "num_vertices", @@ -923,8 +930,8 @@ "doc_path": "rules/maximumindependentset_gridgraph/index.html" }, { - "source": 32, - "target": 33, + "source": 33, + "target": 34, "overhead": [ { "field": "num_vertices", @@ -938,8 +945,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 32, - "target": 34, + "source": 33, + "target": 35, "overhead": [ { "field": "num_vertices", @@ -953,8 +960,8 @@ "doc_path": "rules/maximumindependentset_triangular/index.html" }, { - "source": 32, - "target": 38, + "source": 33, + "target": 39, "overhead": [ { "field": "num_sets", @@ -968,8 +975,8 @@ "doc_path": "rules/maximumindependentset_maximumsetpacking/index.html" }, { - "source": 33, - "target": 29, + "source": 34, + "target": 30, "overhead": [ { "field": "num_vertices", @@ -983,8 +990,8 @@ "doc_path": "rules/maximumindependentset_maximumclique/index.html" }, { - "source": 33, - "target": 40, + "source": 34, + "target": 41, "overhead": [ { "field": "num_sets", @@ -998,8 +1005,8 @@ "doc_path": "rules/maximumindependentset_maximumsetpacking/index.html" }, { - "source": 33, - "target": 47, + "source": 34, + "target": 48, "overhead": [ { "field": "num_vertices", @@ -1013,8 +1020,8 @@ "doc_path": "rules/minimumvertexcover_maximumindependentset/index.html" }, { - "source": 34, - "target": 36, + "source": 35, + "target": 37, "overhead": [ { "field": "num_vertices", @@ -1028,8 +1035,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 35, - "target": 32, + "source": 36, + "target": 33, "overhead": [ { "field": "num_vertices", @@ -1043,8 +1050,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 35, - "target": 36, + "source": 36, + "target": 37, "overhead": [ { "field": "num_vertices", @@ -1058,8 +1065,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 36, - "target": 33, + "source": 37, + "target": 34, "overhead": [ { "field": "num_vertices", @@ -1073,8 +1080,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 37, - "target": 13, + "source": 38, + "target": 14, "overhead": [ { "field": "num_vars", @@ -1088,8 +1095,8 @@ "doc_path": "rules/maximummatching_ilp/index.html" }, { - "source": 37, - "target": 40, + "source": 38, + "target": 41, "overhead": [ { "field": "num_sets", @@ -1103,8 +1110,8 @@ "doc_path": "rules/maximummatching_maximumsetpacking/index.html" }, { - "source": 38, - "target": 32, + "source": 39, + "target": 33, "overhead": [ { "field": "num_vertices", @@ -1118,8 +1125,8 @@ "doc_path": "rules/maximumindependentset_maximumsetpacking/index.html" }, { - "source": 38, - "target": 40, + "source": 39, + "target": 41, "overhead": [ { "field": "num_sets", @@ -1133,8 +1140,8 @@ "doc_path": "rules/maximumsetpacking_casts/index.html" }, { - "source": 39, - "target": 51, + "source": 40, + "target": 52, "overhead": [ { "field": "num_vars", @@ -1144,8 +1151,8 @@ "doc_path": "rules/maximumsetpacking_qubo/index.html" }, { - "source": 40, - "target": 13, + "source": 41, + "target": 14, "overhead": [ { "field": "num_vars", @@ -1159,8 +1166,8 @@ "doc_path": "rules/maximumsetpacking_ilp/index.html" }, { - "source": 40, - "target": 33, + "source": 41, + "target": 34, "overhead": [ { "field": "num_vertices", @@ -1174,8 +1181,8 @@ "doc_path": "rules/maximumindependentset_maximumsetpacking/index.html" }, { - "source": 40, - "target": 39, + "source": 41, + "target": 40, "overhead": [ { "field": "num_sets", @@ -1189,8 +1196,8 @@ "doc_path": "rules/maximumsetpacking_casts/index.html" }, { - "source": 41, - "target": 13, + "source": 42, + "target": 14, "overhead": [ { "field": "num_vars", @@ -1204,8 +1211,8 @@ "doc_path": "rules/minimumdominatingset_ilp/index.html" }, { - "source": 44, - "target": 13, + "source": 45, + "target": 14, "overhead": [ { "field": "num_vars", @@ -1219,8 +1226,8 @@ "doc_path": "rules/minimumsetcovering_ilp/index.html" }, { - "source": 47, - "target": 33, + "source": 48, + "target": 34, "overhead": [ { "field": "num_vertices", @@ -1234,8 +1241,8 @@ "doc_path": "rules/minimumvertexcover_maximumindependentset/index.html" }, { - "source": 47, - "target": 44, + "source": 48, + "target": 45, "overhead": [ { "field": "num_sets", @@ -1249,8 +1256,8 @@ "doc_path": "rules/minimumvertexcover_minimumsetcovering/index.html" }, { - "source": 51, - "target": 13, + "source": 52, + "target": 14, "overhead": [ { "field": "num_vars", @@ -1264,8 +1271,8 @@ "doc_path": "rules/qubo_ilp/index.html" }, { - "source": 51, - "target": 56, + "source": 52, + "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,8 +1297,8 @@ "doc_path": "rules/sat_circuitsat/index.html" }, { - "source": 53, - "target": 17, + "source": 54, + "target": 18, "overhead": [ { "field": "num_vertices", @@ -1305,8 +1312,8 @@ "doc_path": "rules/sat_coloring/index.html" }, { - "source": 53, - "target": 22, + "source": 54, + "target": 23, "overhead": [ { "field": "num_clauses", @@ -1320,8 +1327,8 @@ "doc_path": "rules/sat_ksat/index.html" }, { - "source": 53, - "target": 32, + "source": 54, + "target": 33, "overhead": [ { "field": "num_vertices", @@ -1335,8 +1342,8 @@ "doc_path": "rules/sat_maximumindependentset/index.html" }, { - "source": 53, - "target": 41, + "source": 54, + "target": 42, "overhead": [ { "field": "num_vertices", @@ -1350,8 +1357,8 @@ "doc_path": "rules/sat_minimumdominatingset/index.html" }, { - "source": 56, - "target": 51, + "source": 57, + "target": 52, "overhead": [ { "field": "num_vars", @@ -1361,8 +1368,8 @@ "doc_path": "rules/spinglass_qubo/index.html" }, { - "source": 57, - "target": 27, + "source": 58, + "target": 28, "overhead": [ { "field": "num_vertices", @@ -1376,8 +1383,8 @@ "doc_path": "rules/spinglass_maxcut/index.html" }, { - "source": 57, - "target": 56, + "source": 58, + "target": 57, "overhead": [ { "field": "num_spins", @@ -1391,8 +1398,8 @@ "doc_path": "rules/spinglass_casts/index.html" }, { - "source": 62, - "target": 13, + "source": 63, + "target": 14, "overhead": [ { "field": "num_vars", @@ -1406,8 +1413,8 @@ "doc_path": "rules/travelingsalesman_ilp/index.html" }, { - "source": 62, - "target": 51, + "source": 63, + "target": 52, "overhead": [ { "field": "num_vars", diff --git a/problemreductions-cli/src/cli.rs b/problemreductions-cli/src/cli.rs index 32d58428..cd274450 100644 --- a/problemreductions-cli/src/cli.rs +++ b/problemreductions-cli/src/cli.rs @@ -236,6 +236,7 @@ Flags by problem type: SetBasis --universe, --sets, --k 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 @@ -317,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 @@ -443,6 +445,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 dc1aaca6..dc36a2dd 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, LengthBoundedDisjointPaths, }; @@ -76,13 +78,7 @@ fn all_data_flags_empty(args: &CreateArgs) -> bool { && args.deadline.is_none() && args.num_processors.is_none() && args.alphabet_size.is_none() - && args.capacities.is_none() - && args.source_1.is_none() - && args.sink_1.is_none() - && args.source_2.is_none() - && args.sink_2.is_none() - && args.requirement_1.is_none() - && args.requirement_2.is_none() + && args.bound_k.is_none() } fn emit_problem_output(output: &ProblemJsonOutput, out: &OutputConfig) -> Result<()> { @@ -222,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", @@ -295,6 +292,9 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str { "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", + "ConsecutiveBlockMinimization" => { + "--matrix '[[true,false,true],[false,true,true]]' --bound-k 2" + } _ => "", } } @@ -988,6 +988,29 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { (ser(BMF::new(matrix, rank))?, resolved_variant.clone()) } + // ConsecutiveBlockMinimization + "ConsecutiveBlockMinimization" => { + let usage = "Usage: pred create ConsecutiveBlockMinimization --matrix '[[true,false,true],[false,true,true]]' --bound-k 2"; + let matrix_str = args.matrix.as_deref().ok_or_else(|| { + anyhow::anyhow!( + "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 --matrix as a JSON 2D bool array (e.g., '[[true,false,true],[false,true,true]]')\n\n{usage}\n\nFailed to parse --matrix: {err}" + ) + })?; + ( + ser(ConsecutiveBlockMinimization::try_new(matrix, bound_k) + .map_err(|err| anyhow::anyhow!("{err}\n\n{usage}"))?)?, + resolved_variant.clone(), + ) + } + // LongestCommonSubsequence "LongestCommonSubsequence" => { let strings_str = args.strings.as_deref().ok_or_else(|| { 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/example_db/fixtures/examples.json b/src/example_db/fixtures/examples.json index 39501490..833bedea 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":"DirectedTwoCommodityIntegralFlow","variant":{},"instance":{"capacities":[1,1,1,1,1,1,1,1],"graph":{"inner":{"edge_property":"directed","edges":[[0,2,null],[0,3,null],[1,2,null],[1,3,null],[2,4,null],[2,5,null],[3,4,null],[3,5,null]],"node_holes":[],"nodes":[null,null,null,null,null,null]}},"requirement_1":1,"requirement_2":1,"sink_1":4,"sink_2":5,"source_1":0,"source_2":1},"samples":[{"config":[1,0,0,0,1,0,0,0,0,0,0,1,0,0,0,1],"metric":true}],"optimal":[{"config":[0,0,0,1,0,0,1,0,0,0,1,0,0,1,0,0],"metric":true},{"config":[0,0,0,1,0,0,1,0,0,1,0,0,0,0,0,1],"metric":true},{"config":[0,0,0,1,0,0,1,0,0,1,1,0,0,1,0,1],"metric":true},{"config":[0,0,0,1,0,0,1,0,0,1,1,0,1,0,0,1],"metric":true},{"config":[0,0,0,1,0,0,1,0,1,0,0,0,0,1,0,0],"metric":true},{"config":[0,0,0,1,0,0,1,0,1,0,1,0,1,1,0,0],"metric":true},{"config":[0,0,0,1,0,0,1,0,1,1,0,0,0,1,0,1],"metric":true},{"config":[0,0,0,1,0,0,1,0,1,1,0,0,1,0,0,1],"metric":true},{"config":[0,0,0,1,0,0,1,0,1,1,1,0,1,1,0,1],"metric":true},{"config":[0,0,1,0,1,0,0,0,0,0,0,1,0,0,0,1],"metric":true},{"config":[0,0,1,0,1,0,0,0,0,1,0,0,0,0,0,1],"metric":true},{"config":[0,0,1,0,1,0,0,0,0,1,0,1,0,0,1,1],"metric":true},{"config":[0,0,1,0,1,0,0,0,1,0,0,0,0,1,0,0],"metric":true},{"config":[0,0,1,0,1,0,0,0,1,0,0,1,0,1,0,1],"metric":true},{"config":[0,0,1,0,1,0,0,0,1,0,0,1,0,1,1,0],"metric":true},{"config":[0,0,1,0,1,0,0,0,1,1,0,0,0,1,0,1],"metric":true},{"config":[0,0,1,0,1,0,0,0,1,1,0,0,0,1,1,0],"metric":true},{"config":[0,0,1,0,1,0,0,0,1,1,0,1,0,1,1,1],"metric":true},{"config":[0,0,1,1,0,1,1,0,0,1,0,0,0,0,0,1],"metric":true},{"config":[0,0,1,1,0,1,1,0,1,1,0,0,1,0,0,1],"metric":true},{"config":[0,0,1,1,1,0,0,1,1,0,0,0,0,1,0,0],"metric":true},{"config":[0,0,1,1,1,0,0,1,1,1,0,0,0,1,1,0],"metric":true},{"config":[0,0,1,1,1,0,1,0,0,1,0,0,0,0,0,1],"metric":true},{"config":[0,0,1,1,1,0,1,0,1,0,0,0,0,1,0,0],"metric":true},{"config":[0,0,1,1,1,0,1,0,1,1,0,0,0,1,0,1],"metric":true},{"config":[0,1,0,0,0,0,1,0,0,0,0,1,0,0,0,1],"metric":true},{"config":[0,1,0,0,0,0,1,0,0,0,1,0,0,1,0,0],"metric":true},{"config":[0,1,0,0,0,0,1,0,0,0,1,1,0,1,0,1],"metric":true},{"config":[0,1,0,0,0,0,1,0,0,0,1,1,1,0,0,1],"metric":true},{"config":[0,1,0,0,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,1,0,1,0,1],"metric":true},{"config":[0,1,0,0,0,0,1,0,1,0,0,1,1,0,0,1],"metric":true},{"config":[0,1,0,0,0,0,1,0,1,0,1,0,1,1,0,0],"metric":true},{"config":[0,1,0,0,0,0,1,0,1,0,1,1,1,1,0,1],"metric":true},{"config":[0,1,0,1,0,0,1,1,0,0,1,0,0,1,0,0],"metric":true},{"config":[0,1,0,1,0,0,1,1,1,0,0,0,0,1,0,0],"metric":true},{"config":[0,1,0,1,0,0,1,1,1,0,1,0,1,1,0,0],"metric":true},{"config":[0,1,1,0,0,1,1,0,0,0,0,1,0,0,0,1],"metric":true},{"config":[0,1,1,0,0,1,1,0,1,0,0,1,1,0,0,1],"metric":true},{"config":[0,1,1,0,1,0,0,1,1,0,0,0,0,1,0,0],"metric":true},{"config":[0,1,1,0,1,0,0,1,1,0,0,1,0,1,1,0],"metric":true},{"config":[0,1,1,0,1,0,1,0,0,0,0,1,0,0,0,1],"metric":true},{"config":[0,1,1,0,1,0,1,0,1,0,0,0,0,1,0,0],"metric":true},{"config":[0,1,1,0,1,0,1,0,1,0,0,1,0,1,0,1],"metric":true},{"config":[0,1,1,1,1,0,1,1,1,0,0,0,0,1,0,0],"metric":true},{"config":[1,0,0,0,1,0,0,0,0,0,0,1,0,0,0,1],"metric":true},{"config":[1,0,0,0,1,0,0,0,0,0,1,0,0,1,0,0],"metric":true},{"config":[1,0,0,0,1,0,0,0,0,0,1,1,0,1,0,1],"metric":true},{"config":[1,0,0,0,1,0,0,0,0,0,1,1,0,1,1,0],"metric":true},{"config":[1,0,0,0,1,0,0,0,0,1,0,0,0,0,0,1],"metric":true},{"config":[1,0,0,0,1,0,0,0,0,1,0,1,0,0,1,1],"metric":true},{"config":[1,0,0,0,1,0,0,0,0,1,1,0,0,1,0,1],"metric":true},{"config":[1,0,0,0,1,0,0,0,0,1,1,0,0,1,1,0],"metric":true},{"config":[1,0,0,0,1,0,0,0,0,1,1,1,0,1,1,1],"metric":true},{"config":[1,0,0,1,0,1,1,0,0,1,0,0,0,0,0,1],"metric":true},{"config":[1,0,0,1,0,1,1,0,0,1,1,0,1,0,0,1],"metric":true},{"config":[1,0,0,1,1,0,0,1,0,0,1,0,0,1,0,0],"metric":true},{"config":[1,0,0,1,1,0,0,1,0,1,1,0,0,1,1,0],"metric":true},{"config":[1,0,0,1,1,0,1,0,0,0,1,0,0,1,0,0],"metric":true},{"config":[1,0,0,1,1,0,1,0,0,1,0,0,0,0,0,1],"metric":true},{"config":[1,0,0,1,1,0,1,0,0,1,1,0,0,1,0,1],"metric":true},{"config":[1,0,1,0,1,1,0,0,0,0,0,1,0,0,0,1],"metric":true},{"config":[1,0,1,0,1,1,0,0,0,1,0,0,0,0,0,1],"metric":true},{"config":[1,0,1,0,1,1,0,0,0,1,0,1,0,0,1,1],"metric":true},{"config":[1,0,1,1,1,1,1,0,0,1,0,0,0,0,0,1],"metric":true},{"config":[1,1,0,0,0,1,1,0,0,0,0,1,0,0,0,1],"metric":true},{"config":[1,1,0,0,0,1,1,0,0,0,1,1,1,0,0,1],"metric":true},{"config":[1,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0],"metric":true},{"config":[1,1,0,0,1,0,0,1,0,0,1,1,0,1,1,0],"metric":true},{"config":[1,1,0,0,1,0,1,0,0,0,0,1,0,0,0,1],"metric":true},{"config":[1,1,0,0,1,0,1,0,0,0,1,0,0,1,0,0],"metric":true},{"config":[1,1,0,0,1,0,1,0,0,0,1,1,0,1,0,1],"metric":true},{"config":[1,1,0,1,1,0,1,1,0,0,1,0,0,1,0,0],"metric":true},{"config":[1,1,1,0,1,1,1,0,0,0,0,1,0,0,0,1],"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}}]}, diff --git a/src/models/algebraic/consecutive_block_minimization.rs b/src/models/algebraic/consecutive_block_minimization.rs new file mode 100644 index 00000000..ba83a91a --- /dev/null +++ b/src/models/algebraic/consecutive_block_minimization.rs @@ -0,0 +1,243 @@ +//! 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)] +#[serde(try_from = "ConsecutiveBlockMinimizationDef")] +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 { + Self::try_new(matrix, bound_k).unwrap_or_else(|err| panic!("{err}")) + } + + /// 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. + 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 { + 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", +} + +#[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 { + 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 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)); + } +} + +#[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(&[])); + assert!(!problem.evaluate(&[0])); +} + +#[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_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); + // 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 ef0f09f6..9aaaf3ab 100644 --- a/src/unit_tests/trait_consistency.rs +++ b/src/unit_tests/trait_consistency.rs @@ -88,6 +88,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",