From c3d0e199fbf627d38e6e2d0f9c1b0ecf82c088ba Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Mon, 16 Mar 2026 22:24:15 +0800 Subject: [PATCH 1/7] Add plan for #422: [Model] TwoDimensionalConsecutiveSets --- ...-03-16-two-dimensional-consecutive-sets.md | 157 ++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 docs/plans/2026-03-16-two-dimensional-consecutive-sets.md diff --git a/docs/plans/2026-03-16-two-dimensional-consecutive-sets.md b/docs/plans/2026-03-16-two-dimensional-consecutive-sets.md new file mode 100644 index 000000000..baef69a0b --- /dev/null +++ b/docs/plans/2026-03-16-two-dimensional-consecutive-sets.md @@ -0,0 +1,157 @@ +# Plan: [Model] TwoDimensionalConsecutiveSets (#422) + +## Problem Summary + +**Name:** `TwoDimensionalConsecutiveSets` +**Canonical name:** 2-DIMENSIONAL CONSECUTIVE SETS (Garey & Johnson A4 SR19) +**Type:** Satisfaction (Metric = bool) +**Category:** `set` +**Type parameters:** None +**Associated rule:** #437 (Graph 3-Colorability → 2-Dimensional Consecutive Sets) + +**Definition:** Given a finite alphabet Σ = {0, ..., alphabet_size-1} and a collection C = {Σ₁, ..., Σₙ} of subsets of Σ, determine whether Σ can be partitioned into disjoint sets X₁, X₂, ..., Xₖ such that: +1. Each Xᵢ has at most one element in common with each Σⱼ (intersection constraint) +2. For each Σⱼ ∈ C, there exists an index l(j) such that Σⱼ ⊆ X_{l(j)} ∪ X_{l(j)+1} ∪ ... ∪ X_{l(j)+|Σⱼ|-1} (consecutiveness constraint) + +**Fields:** `alphabet_size: usize`, `subsets: Vec>` +**Getters:** `alphabet_size()`, `num_subsets()`, `subsets()` +**Complexity:** `alphabet_size^alphabet_size` (brute force) +**Reference:** Lipski Jr. (1977), "Two NP-complete problems related to information retrieval," FCT 1977, LNCS 56 + +## Batch 1: Implementation (Steps 1–5.5) + +### Task 1.1: Create model file `src/models/set/two_dimensional_consecutive_sets.rs` + +Follow `ExactCoverBy3Sets` pattern: + +1. `inventory::submit!` with `ProblemSchemaEntry`: + - name: "TwoDimensionalConsecutiveSets" + - display_name: "2-Dimensional Consecutive Sets" + - aliases: &[] + - dimensions: &[] + - fields: alphabet_size (usize), subsets (Vec>) + +2. Struct: + ```rust + #[derive(Debug, Clone, Serialize, Deserialize)] + pub struct TwoDimensionalConsecutiveSets { + alphabet_size: usize, + subsets: Vec>, + } + ``` + +3. Constructor `new(alphabet_size, subsets)`: + - Validate: alphabet_size > 0 + - Validate: all elements in subsets are < alphabet_size + - Validate: no duplicate elements within a subset + - Sort each subset + +4. Getters: `alphabet_size()`, `num_subsets()`, `subsets()` + +5. `Problem` impl: + - `NAME = "TwoDimensionalConsecutiveSets"` + - `Metric = bool` + - `dims()` = `vec![alphabet_size; alphabet_size]` — each of the `alphabet_size` symbols is assigned to a group index in {0, ..., alphabet_size-1} + - `evaluate(config)`: + - Check config length == alphabet_size + - Check all values < alphabet_size + - Build partition: for each group index g, collect symbols assigned to g + - For each subset Σⱼ: + - Check intersection constraint: each group has at most 1 element from Σⱼ + - Check consecutiveness: the group indices of Σⱼ's elements form a contiguous range of length |Σⱼ| + - Return true iff all subsets pass both checks + - `variant()` = `crate::variant_params![]` + +6. `SatisfactionProblem` impl (marker) + +7. `declare_variants!`: + ```rust + crate::declare_variants! { + default sat TwoDimensionalConsecutiveSets => "alphabet_size^alphabet_size", + } + ``` + +8. Canonical example (feature-gated `example-db`): + - Use the YES instance from the issue: alphabet_size=6, subsets=[[0,1,2],[3,4,5],[1,3],[2,4],[0,5]] + - Sample config: the known valid partition [0, 1, 1, 2, 3, 1] (or whatever maps to X₁={0}, X₂={1,5}, X₃={2,3}, X₄={4}) + +9. `#[cfg(test)] #[path]` link to test file + +### Task 1.2: Register module in `src/models/set/mod.rs` + +- Add `pub(crate) mod two_dimensional_consecutive_sets;` +- Add `pub use two_dimensional_consecutive_sets::TwoDimensionalConsecutiveSets;` +- Add `specs.extend(two_dimensional_consecutive_sets::canonical_model_example_specs());` in `canonical_model_example_specs()` + +### Task 1.3: Re-export in `src/models/mod.rs` + +Add `TwoDimensionalConsecutiveSets` to the `pub use set::` line. + +### Task 1.4: CLI discovery — `problemreductions-cli/src/problem_name.rs` + +Alias resolution is now registry-backed. No manual alias needed (no well-known abbreviation). The `ProblemSchemaEntry` handles it. + +### Task 1.5: CLI create support — `problemreductions-cli/src/commands/create.rs` + +Add a match arm for `"TwoDimensionalConsecutiveSets"`: +- Parse `--universe` (alphabet_size) and `--sets` (subsets) +- Construct `TwoDimensionalConsecutiveSets::new(universe, sets)` +- Add example hint in `example_hint()` +- Pattern: similar to `SetBasis` arm but without `--k` + +### Task 1.6: CLI help table — `problemreductions-cli/src/cli.rs` + +Add entry: +``` +TwoDimensionalConsecutiveSets --universe, --sets +``` +Add example: +``` +pred create TwoDimensionalConsecutiveSets --universe 6 --sets "0,1,2;3,4,5;1,3;2,4;0,5" +``` + +### Task 1.7: Create test file `src/unit_tests/models/set/two_dimensional_consecutive_sets.rs` + +Tests: +- `test_two_dimensional_consecutive_sets_creation` — constructor, dims, num_variables +- `test_two_dimensional_consecutive_sets_evaluation` — YES and NO configs +- `test_two_dimensional_consecutive_sets_no_instance` — unsatisfiable instance (from issue Example 2) +- `test_two_dimensional_consecutive_sets_solver` — BruteForce finds satisfying solutions +- `test_two_dimensional_consecutive_sets_serialization` — JSON round-trip +- `test_two_dimensional_consecutive_sets_paper_example` — verify paper instance (written after Step 6) +- Panic tests: out-of-range elements, duplicate elements, zero alphabet_size + +### Task 1.8: Add trait_consistency entry + +In `src/unit_tests/trait_consistency.rs`: +- Add `check_problem_trait(&TwoDimensionalConsecutiveSets::new(...), "TwoDimensionalConsecutiveSets")` with a small instance + +### Task 1.9: Verify + +```bash +make test clippy +``` + +## Batch 2: Paper Entry (Step 6) + +### Task 2.1: Add display name in `docs/paper/reductions.typ` + +```typst +"TwoDimensionalConsecutiveSets": [2-Dimensional Consecutive Sets], +``` + +### Task 2.2: Write problem-def entry + +```typst +#problem-def("TwoDimensionalConsecutiveSets")[ + Given a finite alphabet $Sigma = {0, 1, ..., n-1}$ and a collection $cal(C) = {Sigma_1, ..., Sigma_m}$ of subsets of $Sigma$, determine whether $Sigma$ can be partitioned into disjoint sets $X_1, X_2, ..., X_k$ such that each $X_i$ has at most one element in common with each $Sigma_j$, and for each $Sigma_j in cal(C)$, there is an index $l(j)$ such that $Sigma_j subset.eq X_(l(j)) union X_(l(j)+1) union dots union X_(l(j)+|Sigma_j|-1)$. +][ + Background, example with CeTZ diagram, evaluation. +] +``` + +### Task 2.3: Build and verify + +```bash +make paper +``` From df2755473565eacaf0d0649e40ec83cac4b4b8b0 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Mon, 16 Mar 2026 22:49:07 +0800 Subject: [PATCH 2/7] Implement #422: [Model] TwoDimensionalConsecutiveSets --- docs/paper/reductions.typ | 48 +++++ docs/paper/references.bib | 12 ++ docs/src/reductions/problem_schemas.json | 16 ++ docs/src/reductions/reduction_graph.json | 7 + problemreductions-cli/src/cli.rs | 4 +- problemreductions-cli/src/commands/create.rs | 34 ++++ src/example_db/fixtures/examples.json | 1 + src/models/mod.rs | 5 +- src/models/set/mod.rs | 3 + .../set/two_dimensional_consecutive_sets.rs | 190 ++++++++++++++++++ .../set/two_dimensional_consecutive_sets.rs | 152 ++++++++++++++ src/unit_tests/trait_consistency.rs | 4 + 12 files changed, 474 insertions(+), 2 deletions(-) create mode 100644 src/models/set/two_dimensional_consecutive_sets.rs create mode 100644 src/unit_tests/models/set/two_dimensional_consecutive_sets.rs diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index 707822b03..daed775fa 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], + "TwoDimensionalConsecutiveSets": [2-Dimensional Consecutive Sets], ) // Definition label: "def:" — each definition block must have a matching label @@ -1259,6 +1260,53 @@ NP-completeness was established by Garey, Johnson, and Stockmeyer @gareyJohnsonS ] } +#{ + let x = load-model-example("TwoDimensionalConsecutiveSets") + let n = x.instance.alphabet_size + let subs = x.instance.subsets + let m = subs.len() + let sol = x.optimal.at(0) + let config = sol.config + let sat-count = x.optimal.len() + // Build groups from config: groups.at(g) = list of symbols in group g + let groups = range(n).map(g => range(n).filter(s => config.at(s) == g)) + // Only non-empty groups + let nonempty = groups.enumerate().filter(((_, g)) => g.len() > 0) + let k = nonempty.len() + let fmt-set(s) = "${" + s.map(e => str(e)).join(", ") + "}$" + [ + #problem-def("TwoDimensionalConsecutiveSets")[ + Given finite alphabet $Sigma = {0, 1, dots, n - 1}$ and collection $cal(C) = {Sigma_1, dots, Sigma_m}$ of subsets of $Sigma$, determine whether $Sigma$ can be partitioned into disjoint sets $X_1, X_2, dots, X_k$ such that each $X_i$ has at most one element in common with each $Sigma_j$, and for each $Sigma_j in cal(C)$ there is an index $l(j)$ with $Sigma_j subset.eq X_(l(j)) union X_(l(j)+1) union dots.c union X_(l(j)+|Sigma_j|-1)$. + ][ + This problem generalizes the Consecutive Sets problem (SR18) by requiring not just that each subset's elements appear consecutively in an ordering, but that they be spread across consecutive groups of a partition where each group contributes at most one element per subset. Shown NP-complete by Lipski @lipski1977fct via transformation from Graph 3-Colorability. The problem arises in information storage and retrieval where records must be organized in contiguous blocks. It remains NP-complete if all subsets have at most 5 elements, but is solvable in polynomial time if all subsets have at most 2 elements. The brute-force algorithm assigns each of $n$ symbols to one of up to $n$ groups, giving $O^*(n^n)$ time#footnote[No algorithm improving on brute-force enumeration is known for this problem.]. + + *Example.* Let $Sigma = {0, 1, dots, #(n - 1)}$ and $cal(C) = {#range(m).map(i => $Sigma_#(i + 1)$).join(", ")}$ with #subs.enumerate().map(((i, s)) => $Sigma_#(i + 1) = #fmt-set(s)$).join(", "). A valid partition uses $k = #k$ groups: #nonempty.map(((g, elems)) => $X_#(g + 1) = #fmt-set(elems)$).join(", "). Each group intersects every subset in at most one element, and each subset's elements span exactly $|Sigma_j|$ consecutive groups. For instance, $Sigma_1 = {0, 1, 2}$ maps to groups $X_1, X_2, X_3$ (consecutive), and $Sigma_5 = {0, 5}$ maps to groups $X_1, X_2$ (consecutive). In total, there are #sat-count valid partitions. + + #figure( + canvas(length: 1cm, { + import draw: * + // Draw groups as labeled columns + let gw = 1.4 + let gh = 0.45 + for (col, (g, elems)) in nonempty.enumerate() { + let x0 = col * (gw + 0.3) + // Group header + content((x0 + gw / 2, 0.5), $X_#(g + 1)$, anchor: "south") + // Draw box for the group + rect((x0, -elems.len() * gh), (x0 + gw, 0), + stroke: 0.5pt + black, fill: rgb("#e8f0fe")) + // Elements inside + for (row, elem) in elems.enumerate() { + content((x0 + gw / 2, -row * gh - gh / 2), text(size: 9pt, str(elem))) + } + } + }), + caption: [2-Dimensional Consecutive Sets: partition of $Sigma = {0, dots, 5}$ into #k groups satisfying intersection and consecutiveness constraints for all #m subsets.], + ) + ] + ] +} + == Optimization Problems #{ diff --git a/docs/paper/references.bib b/docs/paper/references.bib index 05740229e..fa2e2f0bc 100644 --- a/docs/paper/references.bib +++ b/docs/paper/references.bib @@ -692,3 +692,15 @@ @article{papadimitriou1982 year = {1982}, doi = {10.1145/322307.322309} } + +@inproceedings{lipski1977fct, + author = {Witold Lipski Jr.}, + title = {Two {NP}-Complete Problems Related to Information Retrieval}, + booktitle = {Fundamentals of Computation Theory (FCT 1977)}, + series = {Lecture Notes in Computer Science}, + volume = {56}, + pages = {452--458}, + publisher = {Springer}, + year = {1977}, + doi = {10.1007/3-540-08442-8_115} +} diff --git a/docs/src/reductions/problem_schemas.json b/docs/src/reductions/problem_schemas.json index 7d60308b8..88586d72a 100644 --- a/docs/src/reductions/problem_schemas.json +++ b/docs/src/reductions/problem_schemas.json @@ -757,6 +757,22 @@ } ] }, + { + "name": "TwoDimensionalConsecutiveSets", + "description": "Determine if alphabet can be partitioned into ordered groups with intersection and consecutiveness constraints", + "fields": [ + { + "name": "alphabet_size", + "type_name": "usize", + "description": "Size of the alphabet (elements are 0..alphabet_size-1)" + }, + { + "name": "subsets", + "type_name": "Vec>", + "description": "Collection of subsets of the alphabet" + } + ] + }, { "name": "UndirectedTwoCommodityIntegralFlow", "description": "Determine whether two integral commodities can satisfy sink demands in an undirected capacitated graph", diff --git a/docs/src/reductions/reduction_graph.json b/docs/src/reductions/reduction_graph.json index eb2008453..a89affbfd 100644 --- a/docs/src/reductions/reduction_graph.json +++ b/docs/src/reductions/reduction_graph.json @@ -551,6 +551,13 @@ "doc_path": "models/graph/struct.TravelingSalesman.html", "complexity": "2^num_vertices" }, + { + "name": "TwoDimensionalConsecutiveSets", + "variant": {}, + "category": "set", + "doc_path": "models/set/struct.TwoDimensionalConsecutiveSets.html", + "complexity": "alphabet_size^alphabet_size" + }, { "name": "UndirectedTwoCommodityIntegralFlow", "variant": {}, diff --git a/problemreductions-cli/src/cli.rs b/problemreductions-cli/src/cli.rs index a289e9153..fd3179fcf 100644 --- a/problemreductions-cli/src/cli.rs +++ b/problemreductions-cli/src/cli.rs @@ -234,6 +234,7 @@ Flags by problem type: MinimumSetCovering --universe, --sets [--weights] X3C (ExactCoverBy3Sets) --universe, --sets (3 elements each) SetBasis --universe, --sets, --k + TwoDimensionalConsecutiveSets --universe, --sets BicliqueCover --left, --right, --biedges, --k BMF --matrix (0/1), --rank SteinerTree --graph, --edge-weights, --terminals @@ -269,7 +270,8 @@ Examples: pred create FVS --arcs \"0>1,1>2,2>0\" --weights 1,1,1 pred create UndirectedTwoCommodityIntegralFlow --graph 0-2,1-2,2-3 --capacities 1,1,2 --source-1 0 --sink-1 3 --source-2 1 --sink-2 3 --requirement-1 1 --requirement-2 1 pred create X3C --universe 9 --sets \"0,1,2;0,2,4;3,4,5;3,5,7;6,7,8;1,4,6;2,5,8\" - pred create SetBasis --universe 4 --sets \"0,1;1,2;0,2;0,1,2\" --k 3")] + pred create SetBasis --universe 4 --sets \"0,1;1,2;0,2;0,1,2\" --k 3 + pred create TwoDimensionalConsecutiveSets --universe 6 --sets \"0,1,2;3,4,5;1,3;2,4;0,5\"")] pub struct CreateArgs { /// Problem type (e.g., MIS, QUBO, SAT). Omit when using --example. #[arg(value_parser = crate::problem_name::ProblemNameParser)] diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index a6261ac00..0b07083fb 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -284,6 +284,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", "SetBasis" => "--universe 4 --sets \"0,1;1,2;0,2;0,1,2\" --k 3", + "TwoDimensionalConsecutiveSets" => { + "--universe 6 --sets \"0,1,2;3,4,5;1,3;2,4;0,5\"" + } "ShortestCommonSupersequence" => "--strings \"0,1,2;1,2,0\" --bound 4", _ => "", } @@ -944,6 +947,37 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { ) } + // TwoDimensionalConsecutiveSets + "TwoDimensionalConsecutiveSets" => { + let universe = args.universe.ok_or_else(|| { + anyhow::anyhow!( + "TwoDimensionalConsecutiveSets requires --universe and --sets\n\n\ + Usage: pred create TwoDimensionalConsecutiveSets --universe 6 --sets \"0,1,2;3,4,5;1,3;2,4;0,5\"" + ) + })?; + let sets = parse_sets(args)?; + for (i, set) in sets.iter().enumerate() { + for &element in set { + if element >= universe { + bail!( + "Set {} contains element {} which is outside alphabet of size {}", + i, + element, + universe + ); + } + } + } + ( + ser( + problemreductions::models::set::TwoDimensionalConsecutiveSets::new( + universe, sets, + ), + )?, + resolved_variant.clone(), + ) + } + // BicliqueCover "BicliqueCover" => { let left = args.left.ok_or_else(|| { diff --git a/src/example_db/fixtures/examples.json b/src/example_db/fixtures/examples.json index 891fe8e70..f2272c4f1 100644 --- a/src/example_db/fixtures/examples.json +++ b/src/example_db/fixtures/examples.json @@ -34,6 +34,7 @@ {"problem":"SpinGlass","variant":{"graph":"SimpleGraph","weight":"i32"},"instance":{"couplings":[1,1,1,1,1,1,1],"fields":[0,0,0,0,0],"graph":{"inner":{"edge_property":"undirected","edges":[[0,1,null],[1,2,null],[3,4,null],[0,3,null],[1,3,null],[1,4,null],[2,4,null]],"node_holes":[],"nodes":[null,null,null,null,null]}}},"samples":[{"config":[1,0,1,1,0],"metric":{"Valid":-3}}],"optimal":[{"config":[0,0,1,1,0],"metric":{"Valid":-3}},{"config":[0,1,0,0,1],"metric":{"Valid":-3}},{"config":[0,1,0,1,0],"metric":{"Valid":-3}},{"config":[0,1,1,1,0],"metric":{"Valid":-3}},{"config":[1,0,0,0,1],"metric":{"Valid":-3}},{"config":[1,0,1,0,1],"metric":{"Valid":-3}},{"config":[1,0,1,1,0],"metric":{"Valid":-3}},{"config":[1,1,0,0,1],"metric":{"Valid":-3}}]}, {"problem":"SteinerTree","variant":{"graph":"SimpleGraph","weight":"i32"},"instance":{"edge_weights":[2,5,2,1,5,6,1],"graph":{"inner":{"edge_property":"undirected","edges":[[0,1,null],[0,3,null],[1,2,null],[1,3,null],[2,3,null],[2,4,null],[3,4,null]],"node_holes":[],"nodes":[null,null,null,null,null]}},"terminals":[0,2,4]},"samples":[{"config":[1,0,1,1,0,0,1],"metric":{"Valid":6}}],"optimal":[{"config":[1,0,1,1,0,0,1],"metric":{"Valid":6}}]}, {"problem":"TravelingSalesman","variant":{"graph":"SimpleGraph","weight":"i32"},"instance":{"edge_weights":[1,3,2,2,3,1],"graph":{"inner":{"edge_property":"undirected","edges":[[0,1,null],[0,2,null],[0,3,null],[1,2,null],[1,3,null],[2,3,null]],"node_holes":[],"nodes":[null,null,null,null]}}},"samples":[{"config":[1,0,1,1,0,1],"metric":{"Valid":6}}],"optimal":[{"config":[1,0,1,1,0,1],"metric":{"Valid":6}}]}, + {"problem":"TwoDimensionalConsecutiveSets","variant":{},"instance":{"alphabet_size":6,"subsets":[[0,1,2],[3,4,5],[1,3],[2,4],[0,5]]},"samples":[{"config":[0,1,2,2,3,1],"metric":true},{"config":[0,0,0,0,0,0],"metric":false}],"optimal":[{"config":[0,1,2,2,3,1],"metric":true},{"config":[0,2,1,3,2,1],"metric":true},{"config":[1,0,2,1,3,2],"metric":true},{"config":[1,2,0,3,1,2],"metric":true},{"config":[1,2,3,1,2,0],"metric":true},{"config":[1,2,3,3,4,2],"metric":true},{"config":[1,3,2,2,1,0],"metric":true},{"config":[1,3,2,4,3,2],"metric":true},{"config":[2,0,1,1,2,3],"metric":true},{"config":[2,1,0,2,1,3],"metric":true},{"config":[2,1,3,0,2,1],"metric":true},{"config":[2,1,3,2,4,3],"metric":true},{"config":[2,3,1,2,0,1],"metric":true},{"config":[2,3,1,4,2,3],"metric":true},{"config":[2,3,4,2,3,1],"metric":true},{"config":[2,3,4,4,5,3],"metric":true},{"config":[2,4,3,3,2,1],"metric":true},{"config":[2,4,3,5,4,3],"metric":true},{"config":[3,1,2,0,1,2],"metric":true},{"config":[3,1,2,2,3,4],"metric":true},{"config":[3,2,1,1,0,2],"metric":true},{"config":[3,2,1,3,2,4],"metric":true},{"config":[3,2,4,1,3,2],"metric":true},{"config":[3,2,4,3,5,4],"metric":true},{"config":[3,4,2,3,1,2],"metric":true},{"config":[3,4,2,5,3,4],"metric":true},{"config":[3,4,5,3,4,2],"metric":true},{"config":[3,5,4,4,3,2],"metric":true},{"config":[4,2,3,1,2,3],"metric":true},{"config":[4,2,3,3,4,5],"metric":true},{"config":[4,3,2,2,1,3],"metric":true},{"config":[4,3,2,4,3,5],"metric":true},{"config":[4,3,5,2,4,3],"metric":true},{"config":[4,5,3,4,2,3],"metric":true},{"config":[5,3,4,2,3,4],"metric":true},{"config":[5,4,3,3,2,4],"metric":true}]}, {"problem":"UndirectedTwoCommodityIntegralFlow","variant":{},"instance":{"capacities":[1,1,2],"graph":{"inner":{"edge_property":"undirected","edges":[[0,2,null],[1,2,null],[2,3,null]],"node_holes":[],"nodes":[null,null,null,null]}},"requirement_1":1,"requirement_2":1,"sink_1":3,"sink_2":3,"source_1":0,"source_2":1},"samples":[{"config":[1,0,0,0,0,0,1,0,1,0,1,0],"metric":true}],"optimal":[{"config":[0,0,1,0,1,0,0,0,1,0,1,0],"metric":true},{"config":[1,0,0,0,0,0,1,0,1,0,1,0],"metric":true}]} ], "rules": [ diff --git a/src/models/mod.rs b/src/models/mod.rs index 3031a2d9a..1256b9cb4 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -23,4 +23,7 @@ pub use misc::{ BinPacking, Factoring, FlowShopScheduling, Knapsack, LongestCommonSubsequence, MinimumTardinessSequencing, PaintShop, ShortestCommonSupersequence, SubsetSum, }; -pub use set::{ExactCoverBy3Sets, MaximumSetPacking, MinimumSetCovering, SetBasis}; +pub use set::{ + ExactCoverBy3Sets, MaximumSetPacking, MinimumSetCovering, SetBasis, + TwoDimensionalConsecutiveSets, +}; diff --git a/src/models/set/mod.rs b/src/models/set/mod.rs index fb8ee7cd8..3c08c816e 100644 --- a/src/models/set/mod.rs +++ b/src/models/set/mod.rs @@ -9,11 +9,13 @@ pub(crate) mod exact_cover_by_3_sets; pub(crate) mod maximum_set_packing; pub(crate) mod minimum_set_covering; pub(crate) mod set_basis; +pub(crate) mod two_dimensional_consecutive_sets; pub use exact_cover_by_3_sets::ExactCoverBy3Sets; pub use maximum_set_packing::MaximumSetPacking; pub use minimum_set_covering::MinimumSetCovering; pub use set_basis::SetBasis; +pub use two_dimensional_consecutive_sets::TwoDimensionalConsecutiveSets; #[cfg(feature = "example-db")] pub(crate) fn canonical_model_example_specs() -> Vec { @@ -22,5 +24,6 @@ pub(crate) fn canonical_model_example_specs() -> Vec>", description: "Collection of subsets of the alphabet" }, + ], + } +} + +/// 2-Dimensional Consecutive Sets problem. +/// +/// Given a finite alphabet Σ = {0, 1, ..., n-1} and a collection C = {Σ₁, ..., Σₘ} +/// of subsets of Σ, determine whether Σ can be partitioned into disjoint sets +/// X₁, X₂, ..., Xₖ such that: +/// 1. Each Xᵢ has at most one element in common with each Σⱼ (intersection constraint) +/// 2. For each Σⱼ, its elements are spread across |Σⱼ| consecutive groups (consecutiveness) +/// +/// This is NP-complete (Lipski, 1977) via transformation from Graph 3-Colorability. +/// +/// # Example +/// +/// ``` +/// use problemreductions::models::set::TwoDimensionalConsecutiveSets; +/// use problemreductions::{Problem, Solver, BruteForce}; +/// +/// // Alphabet: {0,1,2,3,4,5} +/// // Subsets: {0,1,2}, {3,4,5}, {1,3}, {2,4}, {0,5} +/// let problem = TwoDimensionalConsecutiveSets::new( +/// 6, +/// vec![vec![0, 1, 2], vec![3, 4, 5], vec![1, 3], vec![2, 4], vec![0, 5]], +/// ); +/// +/// // Partition: X0={0}, X1={1,5}, X2={2,3}, X3={4} +/// // config[i] = group index of symbol i +/// assert!(problem.evaluate(&[0, 1, 2, 2, 3, 1])); +/// ``` +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TwoDimensionalConsecutiveSets { + /// Size of the alphabet (elements are 0..alphabet_size-1). + alphabet_size: usize, + /// Collection of subsets, each a sorted list of alphabet elements. + subsets: Vec>, +} + +impl TwoDimensionalConsecutiveSets { + /// Create a new 2-Dimensional Consecutive Sets instance. + /// + /// # Panics + /// + /// Panics if `alphabet_size` is 0, if any subset contains elements + /// outside the alphabet, or if any subset has duplicate elements. + pub fn new(alphabet_size: usize, subsets: Vec>) -> Self { + assert!(alphabet_size > 0, "Alphabet size must be positive"); + let mut subsets = subsets; + for (i, subset) in subsets.iter_mut().enumerate() { + let mut seen = HashSet::new(); + for &elem in subset.iter() { + assert!( + elem < alphabet_size, + "Subset {} contains element {} which is outside alphabet of size {}", + i, + elem, + alphabet_size + ); + assert!( + seen.insert(elem), + "Subset {} contains duplicate element {}", + i, + elem + ); + } + subset.sort(); + } + Self { + alphabet_size, + subsets, + } + } + + /// Get the alphabet size. + pub fn alphabet_size(&self) -> usize { + self.alphabet_size + } + + /// Get the number of subsets. + pub fn num_subsets(&self) -> usize { + self.subsets.len() + } + + /// Get the subsets. + pub fn subsets(&self) -> &[Vec] { + &self.subsets + } +} + +impl Problem for TwoDimensionalConsecutiveSets { + const NAME: &'static str = "TwoDimensionalConsecutiveSets"; + type Metric = bool; + + fn dims(&self) -> Vec { + vec![self.alphabet_size; self.alphabet_size] + } + + fn evaluate(&self, config: &[usize]) -> bool { + if config.len() != self.alphabet_size { + return false; + } + if config.iter().any(|&v| v >= self.alphabet_size) { + return false; + } + + for subset in &self.subsets { + if subset.is_empty() { + continue; + } + // Collect group indices for this subset's elements + let groups: Vec = subset.iter().map(|&s| config[s]).collect(); + + // Intersection constraint: all group indices must be distinct + let unique: HashSet = groups.iter().copied().collect(); + if unique.len() != subset.len() { + return false; + } + + // Consecutiveness: group indices must form a contiguous range + let min_g = *unique.iter().min().unwrap(); + let max_g = *unique.iter().max().unwrap(); + if max_g - min_g + 1 != subset.len() { + return false; + } + } + + true + } + + fn variant() -> Vec<(&'static str, &'static str)> { + crate::variant_params![] + } +} + +impl SatisfactionProblem for TwoDimensionalConsecutiveSets {} + +crate::declare_variants! { + default sat TwoDimensionalConsecutiveSets => "alphabet_size^alphabet_size", +} + +#[cfg(feature = "example-db")] +pub(crate) fn canonical_model_example_specs() -> Vec { + vec![crate::example_db::specs::ModelExampleSpec { + id: "two_dimensional_consecutive_sets", + build: || { + let problem = TwoDimensionalConsecutiveSets::new( + 6, + vec![ + vec![0, 1, 2], + vec![3, 4, 5], + vec![1, 3], + vec![2, 4], + vec![0, 5], + ], + ); + crate::example_db::specs::satisfaction_example( + problem, + vec![vec![0, 1, 2, 2, 3, 1], vec![0, 0, 0, 0, 0, 0]], + ) + }, + }] +} + +#[cfg(test)] +#[path = "../../unit_tests/models/set/two_dimensional_consecutive_sets.rs"] +mod tests; diff --git a/src/unit_tests/models/set/two_dimensional_consecutive_sets.rs b/src/unit_tests/models/set/two_dimensional_consecutive_sets.rs new file mode 100644 index 000000000..1cf3dd659 --- /dev/null +++ b/src/unit_tests/models/set/two_dimensional_consecutive_sets.rs @@ -0,0 +1,152 @@ +use super::*; +use crate::solvers::BruteForce; +use crate::traits::Problem; + +#[test] +fn test_two_dimensional_consecutive_sets_creation() { + let problem = TwoDimensionalConsecutiveSets::new( + 6, + vec![ + vec![0, 1, 2], + vec![3, 4, 5], + vec![1, 3], + vec![2, 4], + vec![0, 5], + ], + ); + assert_eq!(problem.alphabet_size(), 6); + assert_eq!(problem.num_subsets(), 5); + assert_eq!(problem.num_variables(), 6); + assert_eq!(problem.dims(), vec![6, 6, 6, 6, 6, 6]); +} + +#[test] +fn test_two_dimensional_consecutive_sets_evaluation() { + // YES instance from issue: + // Alphabet: {0,1,2,3,4,5} + // Subsets: {0,1,2}, {3,4,5}, {1,3}, {2,4}, {0,5} + let problem = TwoDimensionalConsecutiveSets::new( + 6, + vec![ + vec![0, 1, 2], + vec![3, 4, 5], + vec![1, 3], + vec![2, 4], + vec![0, 5], + ], + ); + + // Valid partition: X0={0}, X1={1,5}, X2={2,3}, X3={4} + // config[i] = group of symbol i + assert!(problem.evaluate(&[0, 1, 2, 2, 3, 1])); + + // Invalid: all symbols in same group (intersection constraint violated) + assert!(!problem.evaluate(&[0, 0, 0, 0, 0, 0])); + + // Invalid: wrong config length + assert!(!problem.evaluate(&[0, 1, 2])); + + // Invalid: group index out of range + assert!(!problem.evaluate(&[0, 1, 2, 2, 3, 7])); + + // Invalid: {0,1,2} not consecutive (0 in group 0, 1 in group 1, 2 in group 5) + assert!(!problem.evaluate(&[0, 1, 5, 2, 3, 1])); +} + +#[test] +fn test_two_dimensional_consecutive_sets_no_instance() { + // NO instance from issue: + // Alphabet: {0,1,2,3,4,5} + // Subsets: {0,1,2}, {0,3,4}, {0,5,1}, {2,3,5} + let problem = TwoDimensionalConsecutiveSets::new( + 6, + vec![vec![0, 1, 2], vec![0, 3, 4], vec![0, 1, 5], vec![2, 3, 5]], + ); + + let solver = BruteForce::new(); + let solutions = solver.find_all_satisfying(&problem); + assert!(solutions.is_empty()); +} + +#[test] +fn test_two_dimensional_consecutive_sets_solver() { + // Small YES instance: alphabet_size=4, subsets={0,1},{2,3},{1,2} + let problem = TwoDimensionalConsecutiveSets::new(4, vec![vec![0, 1], vec![2, 3], vec![1, 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_two_dimensional_consecutive_sets_serialization() { + let problem = TwoDimensionalConsecutiveSets::new(4, vec![vec![0, 1], vec![2, 3]]); + let json = serde_json::to_string(&problem).unwrap(); + let deserialized: TwoDimensionalConsecutiveSets = serde_json::from_str(&json).unwrap(); + assert_eq!(deserialized.alphabet_size(), problem.alphabet_size()); + assert_eq!(deserialized.num_subsets(), problem.num_subsets()); + assert_eq!(deserialized.subsets(), problem.subsets()); +} + +#[test] +fn test_two_dimensional_consecutive_sets_empty_subsets() { + // All empty subsets — trivially satisfiable + let problem = TwoDimensionalConsecutiveSets::new(3, vec![vec![], vec![]]); + assert!(problem.evaluate(&[0, 1, 2])); + assert!(problem.evaluate(&[0, 0, 0])); +} + +#[test] +fn test_two_dimensional_consecutive_sets_single_element_subsets() { + // Single-element subsets: always satisfiable (no consecutiveness constraint to check) + let problem = TwoDimensionalConsecutiveSets::new(3, vec![vec![0], vec![1], vec![2]]); + assert!(problem.evaluate(&[0, 1, 2])); + assert!(problem.evaluate(&[0, 0, 0])); // single elements always consecutive +} + +#[test] +fn test_two_dimensional_consecutive_sets_paper_example() { + // Same instance used in the paper entry + let problem = TwoDimensionalConsecutiveSets::new( + 6, + vec![ + vec![0, 1, 2], + vec![3, 4, 5], + vec![1, 3], + vec![2, 4], + vec![0, 5], + ], + ); + + // Verify the known valid solution + let valid_config = vec![0, 1, 2, 2, 3, 1]; + assert!(problem.evaluate(&valid_config)); + + // Use brute force to find all solutions + let solver = BruteForce::new(); + let solutions = solver.find_all_satisfying(&problem); + assert!(!solutions.is_empty()); + // The known solution should be among them + assert!(solutions.contains(&valid_config)); +} + +#[test] +#[should_panic(expected = "Alphabet size must be positive")] +fn test_two_dimensional_consecutive_sets_zero_alphabet() { + TwoDimensionalConsecutiveSets::new(0, vec![]); +} + +#[test] +#[should_panic(expected = "outside alphabet")] +fn test_two_dimensional_consecutive_sets_element_out_of_range() { + TwoDimensionalConsecutiveSets::new(3, vec![vec![0, 5]]); +} + +#[test] +#[should_panic(expected = "duplicate element")] +fn test_two_dimensional_consecutive_sets_duplicate_elements() { + TwoDimensionalConsecutiveSets::new(3, vec![vec![0, 0]]); +} diff --git a/src/unit_tests/trait_consistency.rs b/src/unit_tests/trait_consistency.rs index e5117e24d..5103ce949 100644 --- a/src/unit_tests/trait_consistency.rs +++ b/src/unit_tests/trait_consistency.rs @@ -86,6 +86,10 @@ fn test_all_problems_implement_trait_correctly() { &SetBasis::new(3, vec![vec![0, 1], vec![1, 2]], 2), "SetBasis", ); + check_problem_trait( + &TwoDimensionalConsecutiveSets::new(4, vec![vec![0, 1], vec![2, 3]]), + "TwoDimensionalConsecutiveSets", + ); check_problem_trait(&PaintShop::new(vec!["a", "a"]), "PaintShop"); check_problem_trait(&BMF::new(vec![vec![true]], 1), "BMF"); check_problem_trait( From 789ccbc064f83da7d93e468be0368e6c566d5da8 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Mon, 16 Mar 2026 22:49:16 +0800 Subject: [PATCH 3/7] chore: remove plan file after implementation --- ...-03-16-two-dimensional-consecutive-sets.md | 157 ------------------ 1 file changed, 157 deletions(-) delete mode 100644 docs/plans/2026-03-16-two-dimensional-consecutive-sets.md diff --git a/docs/plans/2026-03-16-two-dimensional-consecutive-sets.md b/docs/plans/2026-03-16-two-dimensional-consecutive-sets.md deleted file mode 100644 index baef69a0b..000000000 --- a/docs/plans/2026-03-16-two-dimensional-consecutive-sets.md +++ /dev/null @@ -1,157 +0,0 @@ -# Plan: [Model] TwoDimensionalConsecutiveSets (#422) - -## Problem Summary - -**Name:** `TwoDimensionalConsecutiveSets` -**Canonical name:** 2-DIMENSIONAL CONSECUTIVE SETS (Garey & Johnson A4 SR19) -**Type:** Satisfaction (Metric = bool) -**Category:** `set` -**Type parameters:** None -**Associated rule:** #437 (Graph 3-Colorability → 2-Dimensional Consecutive Sets) - -**Definition:** Given a finite alphabet Σ = {0, ..., alphabet_size-1} and a collection C = {Σ₁, ..., Σₙ} of subsets of Σ, determine whether Σ can be partitioned into disjoint sets X₁, X₂, ..., Xₖ such that: -1. Each Xᵢ has at most one element in common with each Σⱼ (intersection constraint) -2. For each Σⱼ ∈ C, there exists an index l(j) such that Σⱼ ⊆ X_{l(j)} ∪ X_{l(j)+1} ∪ ... ∪ X_{l(j)+|Σⱼ|-1} (consecutiveness constraint) - -**Fields:** `alphabet_size: usize`, `subsets: Vec>` -**Getters:** `alphabet_size()`, `num_subsets()`, `subsets()` -**Complexity:** `alphabet_size^alphabet_size` (brute force) -**Reference:** Lipski Jr. (1977), "Two NP-complete problems related to information retrieval," FCT 1977, LNCS 56 - -## Batch 1: Implementation (Steps 1–5.5) - -### Task 1.1: Create model file `src/models/set/two_dimensional_consecutive_sets.rs` - -Follow `ExactCoverBy3Sets` pattern: - -1. `inventory::submit!` with `ProblemSchemaEntry`: - - name: "TwoDimensionalConsecutiveSets" - - display_name: "2-Dimensional Consecutive Sets" - - aliases: &[] - - dimensions: &[] - - fields: alphabet_size (usize), subsets (Vec>) - -2. Struct: - ```rust - #[derive(Debug, Clone, Serialize, Deserialize)] - pub struct TwoDimensionalConsecutiveSets { - alphabet_size: usize, - subsets: Vec>, - } - ``` - -3. Constructor `new(alphabet_size, subsets)`: - - Validate: alphabet_size > 0 - - Validate: all elements in subsets are < alphabet_size - - Validate: no duplicate elements within a subset - - Sort each subset - -4. Getters: `alphabet_size()`, `num_subsets()`, `subsets()` - -5. `Problem` impl: - - `NAME = "TwoDimensionalConsecutiveSets"` - - `Metric = bool` - - `dims()` = `vec![alphabet_size; alphabet_size]` — each of the `alphabet_size` symbols is assigned to a group index in {0, ..., alphabet_size-1} - - `evaluate(config)`: - - Check config length == alphabet_size - - Check all values < alphabet_size - - Build partition: for each group index g, collect symbols assigned to g - - For each subset Σⱼ: - - Check intersection constraint: each group has at most 1 element from Σⱼ - - Check consecutiveness: the group indices of Σⱼ's elements form a contiguous range of length |Σⱼ| - - Return true iff all subsets pass both checks - - `variant()` = `crate::variant_params![]` - -6. `SatisfactionProblem` impl (marker) - -7. `declare_variants!`: - ```rust - crate::declare_variants! { - default sat TwoDimensionalConsecutiveSets => "alphabet_size^alphabet_size", - } - ``` - -8. Canonical example (feature-gated `example-db`): - - Use the YES instance from the issue: alphabet_size=6, subsets=[[0,1,2],[3,4,5],[1,3],[2,4],[0,5]] - - Sample config: the known valid partition [0, 1, 1, 2, 3, 1] (or whatever maps to X₁={0}, X₂={1,5}, X₃={2,3}, X₄={4}) - -9. `#[cfg(test)] #[path]` link to test file - -### Task 1.2: Register module in `src/models/set/mod.rs` - -- Add `pub(crate) mod two_dimensional_consecutive_sets;` -- Add `pub use two_dimensional_consecutive_sets::TwoDimensionalConsecutiveSets;` -- Add `specs.extend(two_dimensional_consecutive_sets::canonical_model_example_specs());` in `canonical_model_example_specs()` - -### Task 1.3: Re-export in `src/models/mod.rs` - -Add `TwoDimensionalConsecutiveSets` to the `pub use set::` line. - -### Task 1.4: CLI discovery — `problemreductions-cli/src/problem_name.rs` - -Alias resolution is now registry-backed. No manual alias needed (no well-known abbreviation). The `ProblemSchemaEntry` handles it. - -### Task 1.5: CLI create support — `problemreductions-cli/src/commands/create.rs` - -Add a match arm for `"TwoDimensionalConsecutiveSets"`: -- Parse `--universe` (alphabet_size) and `--sets` (subsets) -- Construct `TwoDimensionalConsecutiveSets::new(universe, sets)` -- Add example hint in `example_hint()` -- Pattern: similar to `SetBasis` arm but without `--k` - -### Task 1.6: CLI help table — `problemreductions-cli/src/cli.rs` - -Add entry: -``` -TwoDimensionalConsecutiveSets --universe, --sets -``` -Add example: -``` -pred create TwoDimensionalConsecutiveSets --universe 6 --sets "0,1,2;3,4,5;1,3;2,4;0,5" -``` - -### Task 1.7: Create test file `src/unit_tests/models/set/two_dimensional_consecutive_sets.rs` - -Tests: -- `test_two_dimensional_consecutive_sets_creation` — constructor, dims, num_variables -- `test_two_dimensional_consecutive_sets_evaluation` — YES and NO configs -- `test_two_dimensional_consecutive_sets_no_instance` — unsatisfiable instance (from issue Example 2) -- `test_two_dimensional_consecutive_sets_solver` — BruteForce finds satisfying solutions -- `test_two_dimensional_consecutive_sets_serialization` — JSON round-trip -- `test_two_dimensional_consecutive_sets_paper_example` — verify paper instance (written after Step 6) -- Panic tests: out-of-range elements, duplicate elements, zero alphabet_size - -### Task 1.8: Add trait_consistency entry - -In `src/unit_tests/trait_consistency.rs`: -- Add `check_problem_trait(&TwoDimensionalConsecutiveSets::new(...), "TwoDimensionalConsecutiveSets")` with a small instance - -### Task 1.9: Verify - -```bash -make test clippy -``` - -## Batch 2: Paper Entry (Step 6) - -### Task 2.1: Add display name in `docs/paper/reductions.typ` - -```typst -"TwoDimensionalConsecutiveSets": [2-Dimensional Consecutive Sets], -``` - -### Task 2.2: Write problem-def entry - -```typst -#problem-def("TwoDimensionalConsecutiveSets")[ - Given a finite alphabet $Sigma = {0, 1, ..., n-1}$ and a collection $cal(C) = {Sigma_1, ..., Sigma_m}$ of subsets of $Sigma$, determine whether $Sigma$ can be partitioned into disjoint sets $X_1, X_2, ..., X_k$ such that each $X_i$ has at most one element in common with each $Sigma_j$, and for each $Sigma_j in cal(C)$, there is an index $l(j)$ such that $Sigma_j subset.eq X_(l(j)) union X_(l(j)+1) union dots union X_(l(j)+|Sigma_j|-1)$. -][ - Background, example with CeTZ diagram, evaluation. -] -``` - -### Task 2.3: Build and verify - -```bash -make paper -``` From aa3dbc94f4b4e4374e9e5d3587f6b406eb94272e Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Tue, 17 Mar 2026 01:49:55 +0800 Subject: [PATCH 4/7] fix: address PR #673 review comments --- problemreductions-cli/src/cli.rs | 6 +- problemreductions-cli/src/commands/create.rs | 18 +++-- problemreductions-cli/tests/cli_tests.rs | 54 ++++++++++++++ .../set/two_dimensional_consecutive_sets.rs | 71 +++++++++++++------ .../set/two_dimensional_consecutive_sets.rs | 7 ++ 5 files changed, 126 insertions(+), 30 deletions(-) diff --git a/problemreductions-cli/src/cli.rs b/problemreductions-cli/src/cli.rs index 1fec9072f..ce94ff914 100644 --- a/problemreductions-cli/src/cli.rs +++ b/problemreductions-cli/src/cli.rs @@ -235,7 +235,7 @@ Flags by problem type: MinimumSetCovering --universe, --sets [--weights] X3C (ExactCoverBy3Sets) --universe, --sets (3 elements each) SetBasis --universe, --sets, --k - TwoDimensionalConsecutiveSets --universe, --sets + TwoDimensionalConsecutiveSets --alphabet-size, --sets BicliqueCover --left, --right, --biedges, --k BMF --matrix (0/1), --rank SteinerTree --graph, --edge-weights, --terminals @@ -276,7 +276,7 @@ Examples: pred create UndirectedTwoCommodityIntegralFlow --graph 0-2,1-2,2-3 --capacities 1,1,2 --source-1 0 --sink-1 3 --source-2 1 --sink-2 3 --requirement-1 1 --requirement-2 1 pred create X3C --universe 9 --sets \"0,1,2;0,2,4;3,4,5;3,5,7;6,7,8;1,4,6;2,5,8\" pred create SetBasis --universe 4 --sets \"0,1;1,2;0,2;0,1,2\" --k 3 - pred create TwoDimensionalConsecutiveSets --universe 6 --sets \"0,1,2;3,4,5;1,3;2,4;0,5\"")] + pred create TwoDimensionalConsecutiveSets --alphabet-size 6 --sets \"0,1,2;3,4,5;1,3;2,4;0,5\"")] pub struct CreateArgs { /// Problem type (e.g., MIS, QUBO, SAT). Omit when using --example. #[arg(value_parser = crate::problem_name::ProblemNameParser)] @@ -455,7 +455,7 @@ pub struct CreateArgs { /// Number of processors/machines for FlowShopScheduling #[arg(long)] pub num_processors: Option, - /// Alphabet size for SCS (optional; inferred from max symbol + 1 if omitted) + /// Alphabet size for SCS (optional) or TwoDimensionalConsecutiveSets #[arg(long)] pub alphabet_size: Option, } diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index 5d7286ef8..a97b7992e 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -296,7 +296,7 @@ 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", "TwoDimensionalConsecutiveSets" => { - "--universe 6 --sets \"0,1,2;3,4,5;1,3;2,4;0,5\"" + "--alphabet-size 6 --sets \"0,1,2;3,4,5;1,3;2,4;0,5\"" } "ShortestCommonSupersequence" => "--strings \"0,1,2;1,2,0\" --bound 4", _ => "", @@ -1078,21 +1078,24 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { // TwoDimensionalConsecutiveSets "TwoDimensionalConsecutiveSets" => { - let universe = args.universe.ok_or_else(|| { + let alphabet_size = args.alphabet_size.or(args.universe).ok_or_else(|| { anyhow::anyhow!( - "TwoDimensionalConsecutiveSets requires --universe and --sets\n\n\ - Usage: pred create TwoDimensionalConsecutiveSets --universe 6 --sets \"0,1,2;3,4,5;1,3;2,4;0,5\"" + "TwoDimensionalConsecutiveSets requires --alphabet-size (or --universe) and --sets\n\n\ + Usage: pred create TwoDimensionalConsecutiveSets --alphabet-size 6 --sets \"0,1,2;3,4,5;1,3;2,4;0,5\"" ) })?; + if alphabet_size == 0 { + bail!("Alphabet size must be positive"); + } let sets = parse_sets(args)?; for (i, set) in sets.iter().enumerate() { for &element in set { - if element >= universe { + if element >= alphabet_size { bail!( "Set {} contains element {} which is outside alphabet of size {}", i, element, - universe + alphabet_size ); } } @@ -1100,7 +1103,8 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { ( ser( problemreductions::models::set::TwoDimensionalConsecutiveSets::new( - universe, sets, + alphabet_size, + sets, ), )?, resolved_variant.clone(), diff --git a/problemreductions-cli/tests/cli_tests.rs b/problemreductions-cli/tests/cli_tests.rs index 9c35fd001..9c80949e4 100644 --- a/problemreductions-cli/tests/cli_tests.rs +++ b/problemreductions-cli/tests/cli_tests.rs @@ -1045,6 +1045,60 @@ fn test_create_set_basis_rejects_out_of_range_elements() { assert!(!stderr.contains("panicked at"), "stderr: {stderr}"); } +#[test] +fn test_create_two_dimensional_consecutive_sets_accepts_alphabet_size_flag() { + let output_file = + std::env::temp_dir().join("pred_test_create_two_dimensional_consecutive_sets.json"); + let output = pred() + .args([ + "-o", + output_file.to_str().unwrap(), + "create", + "TwoDimensionalConsecutiveSets", + "--alphabet-size", + "6", + "--sets", + "0,1,2;3,4,5;1,3;2,4;0,5", + ]) + .output() + .unwrap(); + assert!( + output.status.success(), + "stderr: {}", + String::from_utf8_lossy(&output.stderr) + ); + + let content = std::fs::read_to_string(&output_file).unwrap(); + let json: serde_json::Value = serde_json::from_str(&content).unwrap(); + assert_eq!(json["type"], "TwoDimensionalConsecutiveSets"); + assert_eq!(json["data"]["alphabet_size"], 6); + assert_eq!(json["data"]["subsets"][0], serde_json::json!([0, 1, 2])); + + std::fs::remove_file(&output_file).ok(); +} + +#[test] +fn test_create_two_dimensional_consecutive_sets_rejects_zero_alphabet_size_without_panic() { + let output = pred() + .args([ + "create", + "TwoDimensionalConsecutiveSets", + "--alphabet-size", + "0", + "--sets", + "0", + ]) + .output() + .unwrap(); + assert!(!output.status.success()); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + stderr.contains("Alphabet size must be positive"), + "stderr: {stderr}" + ); + assert!(!stderr.contains("panicked at"), "stderr: {stderr}"); +} + #[test] fn test_create_then_evaluate() { // Create a problem diff --git a/src/models/set/two_dimensional_consecutive_sets.rs b/src/models/set/two_dimensional_consecutive_sets.rs index 8f6bd000c..2529eb810 100644 --- a/src/models/set/two_dimensional_consecutive_sets.rs +++ b/src/models/set/two_dimensional_consecutive_sets.rs @@ -7,6 +7,7 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry}; use crate::traits::{Problem, SatisfactionProblem}; +use serde::de::Error as _; use serde::{Deserialize, Serialize}; use std::collections::HashSet; @@ -52,7 +53,7 @@ inventory::submit! { /// // config[i] = group index of symbol i /// assert!(problem.evaluate(&[0, 1, 2, 2, 3, 1])); /// ``` -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize)] pub struct TwoDimensionalConsecutiveSets { /// Size of the alphabet (elements are 0..alphabet_size-1). alphabet_size: usize, @@ -60,6 +61,53 @@ pub struct TwoDimensionalConsecutiveSets { subsets: Vec>, } +#[derive(Debug, Deserialize)] +struct TwoDimensionalConsecutiveSetsUnchecked { + alphabet_size: usize, + subsets: Vec>, +} + +fn validation_error(alphabet_size: usize, subsets: &mut [Vec]) -> Option { + if alphabet_size == 0 { + return Some("Alphabet size must be positive".to_string()); + } + + for (i, subset) in subsets.iter_mut().enumerate() { + let mut seen = HashSet::new(); + for &elem in subset.iter() { + if elem >= alphabet_size { + return Some(format!( + "Subset {} contains element {} which is outside alphabet of size {}", + i, elem, alphabet_size + )); + } + if !seen.insert(elem) { + return Some(format!("Subset {} contains duplicate element {}", i, elem)); + } + } + subset.sort(); + } + + None +} + +impl<'de> Deserialize<'de> for TwoDimensionalConsecutiveSets { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let mut unchecked = TwoDimensionalConsecutiveSetsUnchecked::deserialize(deserializer)?; + if let Some(message) = validation_error(unchecked.alphabet_size, &mut unchecked.subsets) { + return Err(D::Error::custom(message)); + } + + Ok(Self { + alphabet_size: unchecked.alphabet_size, + subsets: unchecked.subsets, + }) + } +} + impl TwoDimensionalConsecutiveSets { /// Create a new 2-Dimensional Consecutive Sets instance. /// @@ -68,26 +116,9 @@ impl TwoDimensionalConsecutiveSets { /// Panics if `alphabet_size` is 0, if any subset contains elements /// outside the alphabet, or if any subset has duplicate elements. pub fn new(alphabet_size: usize, subsets: Vec>) -> Self { - assert!(alphabet_size > 0, "Alphabet size must be positive"); let mut subsets = subsets; - for (i, subset) in subsets.iter_mut().enumerate() { - let mut seen = HashSet::new(); - for &elem in subset.iter() { - assert!( - elem < alphabet_size, - "Subset {} contains element {} which is outside alphabet of size {}", - i, - elem, - alphabet_size - ); - assert!( - seen.insert(elem), - "Subset {} contains duplicate element {}", - i, - elem - ); - } - subset.sort(); + if let Some(message) = validation_error(alphabet_size, &mut subsets) { + panic!("{message}"); } Self { alphabet_size, diff --git a/src/unit_tests/models/set/two_dimensional_consecutive_sets.rs b/src/unit_tests/models/set/two_dimensional_consecutive_sets.rs index 1cf3dd659..079b9cdf8 100644 --- a/src/unit_tests/models/set/two_dimensional_consecutive_sets.rs +++ b/src/unit_tests/models/set/two_dimensional_consecutive_sets.rs @@ -91,6 +91,13 @@ fn test_two_dimensional_consecutive_sets_serialization() { assert_eq!(deserialized.subsets(), problem.subsets()); } +#[test] +fn test_two_dimensional_consecutive_sets_deserialization_rejects_out_of_range_elements() { + let json = r#"{"alphabet_size":3,"subsets":[[0,5]]}"#; + let err = serde_json::from_str::(json).unwrap_err(); + assert!(err.to_string().contains("outside alphabet"), "error: {err}"); +} + #[test] fn test_two_dimensional_consecutive_sets_empty_subsets() { // All empty subsets — trivially satisfiable From 3b5782013db841908912aea3722fc860ef6d7a63 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Tue, 17 Mar 2026 02:18:23 +0800 Subject: [PATCH 5/7] fix: resolve remaining review findings for TwoDimensionalConsecutiveSets --- docs/paper/reductions.typ | 3 +- docs/src/cli.md | 1 + problemreductions-cli/src/cli.rs | 1 + problemreductions-cli/src/commands/create.rs | 20 ++------ problemreductions-cli/tests/cli_tests.rs | 19 ++++++++ src/example_db/fixtures/examples.json | 2 +- .../set/two_dimensional_consecutive_sets.rs | 47 ++++++++++++------- .../set/two_dimensional_consecutive_sets.rs | 8 ++++ 8 files changed, 63 insertions(+), 38 deletions(-) diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index 7a2449161..6c3df4288 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -1321,7 +1321,6 @@ NP-completeness was established by Garey, Johnson, and Stockmeyer @gareyJohnsonS let m = subs.len() let sol = x.optimal.at(0) let config = sol.config - let sat-count = x.optimal.len() // Build groups from config: groups.at(g) = list of symbols in group g let groups = range(n).map(g => range(n).filter(s => config.at(s) == g)) // Only non-empty groups @@ -1334,7 +1333,7 @@ NP-completeness was established by Garey, Johnson, and Stockmeyer @gareyJohnsonS ][ This problem generalizes the Consecutive Sets problem (SR18) by requiring not just that each subset's elements appear consecutively in an ordering, but that they be spread across consecutive groups of a partition where each group contributes at most one element per subset. Shown NP-complete by Lipski @lipski1977fct via transformation from Graph 3-Colorability. The problem arises in information storage and retrieval where records must be organized in contiguous blocks. It remains NP-complete if all subsets have at most 5 elements, but is solvable in polynomial time if all subsets have at most 2 elements. The brute-force algorithm assigns each of $n$ symbols to one of up to $n$ groups, giving $O^*(n^n)$ time#footnote[No algorithm improving on brute-force enumeration is known for this problem.]. - *Example.* Let $Sigma = {0, 1, dots, #(n - 1)}$ and $cal(C) = {#range(m).map(i => $Sigma_#(i + 1)$).join(", ")}$ with #subs.enumerate().map(((i, s)) => $Sigma_#(i + 1) = #fmt-set(s)$).join(", "). A valid partition uses $k = #k$ groups: #nonempty.map(((g, elems)) => $X_#(g + 1) = #fmt-set(elems)$).join(", "). Each group intersects every subset in at most one element, and each subset's elements span exactly $|Sigma_j|$ consecutive groups. For instance, $Sigma_1 = {0, 1, 2}$ maps to groups $X_1, X_2, X_3$ (consecutive), and $Sigma_5 = {0, 5}$ maps to groups $X_1, X_2$ (consecutive). In total, there are #sat-count valid partitions. + *Example.* Let $Sigma = {0, 1, dots, #(n - 1)}$ and $cal(C) = {#range(m).map(i => $Sigma_#(i + 1)$).join(", ")}$ with #subs.enumerate().map(((i, s)) => $Sigma_#(i + 1) = #fmt-set(s)$).join(", "). A valid partition uses $k = #k$ groups: #nonempty.map(((g, elems)) => $X_#(g + 1) = #fmt-set(elems)$).join(", "). Each group intersects every subset in at most one element, and each subset's elements span exactly $|Sigma_j|$ consecutive groups. For instance, $Sigma_1 = {0, 1, 2}$ maps to groups $X_1, X_2, X_3$ (consecutive), and $Sigma_5 = {0, 5}$ maps to groups $X_1, X_2$ (consecutive). The example database records many satisfying assignments for this instance, including encodings that differ only by unused or shifted group labels. #figure( canvas(length: 1cm, { diff --git a/docs/src/cli.md b/docs/src/cli.md index 5c595e905..9f03551c9 100644 --- a/docs/src/cli.md +++ b/docs/src/cli.md @@ -435,6 +435,7 @@ Stdin is supported with `-`: ```bash pred create MIS --graph 0-1,1-2,2-3 | pred solve - pred create MIS --graph 0-1,1-2,2-3 | pred solve - --solver brute-force +pred create TwoDimensionalConsecutiveSets --alphabet-size 6 --sets "0,1,2;3,4,5;1,3;2,4;0,5" | pred solve - --solver brute-force ``` When the problem is not ILP, the solver automatically reduces it to ILP, solves, and maps the solution back. The auto-reduction is shown in the output: diff --git a/problemreductions-cli/src/cli.rs b/problemreductions-cli/src/cli.rs index ce94ff914..4244a62ab 100644 --- a/problemreductions-cli/src/cli.rs +++ b/problemreductions-cli/src/cli.rs @@ -468,6 +468,7 @@ Examples: pred solve reduced.json # solve a reduction bundle pred solve reduced.json -o solution.json # save result to file pred create MIS --graph 0-1,1-2 | pred solve - # read from stdin + pred create TwoDimensionalConsecutiveSets --alphabet-size 6 --sets \"0,1,2;3,4,5;1,3;2,4;0,5\" | pred solve - --solver brute-force pred solve problem.json --timeout 10 # abort after 10 seconds Typical workflow: diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index a97b7992e..9e7894894 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -1084,28 +1084,14 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { Usage: pred create TwoDimensionalConsecutiveSets --alphabet-size 6 --sets \"0,1,2;3,4,5;1,3;2,4;0,5\"" ) })?; - if alphabet_size == 0 { - bail!("Alphabet size must be positive"); - } let sets = parse_sets(args)?; - for (i, set) in sets.iter().enumerate() { - for &element in set { - if element >= alphabet_size { - bail!( - "Set {} contains element {} which is outside alphabet of size {}", - i, - element, - alphabet_size - ); - } - } - } ( ser( - problemreductions::models::set::TwoDimensionalConsecutiveSets::new( + problemreductions::models::set::TwoDimensionalConsecutiveSets::try_new( alphabet_size, sets, - ), + ) + .map_err(anyhow::Error::msg)?, )?, resolved_variant.clone(), ) diff --git a/problemreductions-cli/tests/cli_tests.rs b/problemreductions-cli/tests/cli_tests.rs index 9c80949e4..117c090f4 100644 --- a/problemreductions-cli/tests/cli_tests.rs +++ b/problemreductions-cli/tests/cli_tests.rs @@ -1099,6 +1099,25 @@ fn test_create_two_dimensional_consecutive_sets_rejects_zero_alphabet_size_witho assert!(!stderr.contains("panicked at"), "stderr: {stderr}"); } +#[test] +fn test_create_two_dimensional_consecutive_sets_rejects_duplicate_elements_without_panic() { + let output = pred() + .args([ + "create", + "TwoDimensionalConsecutiveSets", + "--alphabet-size", + "3", + "--sets", + "0,0", + ]) + .output() + .unwrap(); + assert!(!output.status.success()); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("duplicate element"), "stderr: {stderr}"); + assert!(!stderr.contains("panicked at"), "stderr: {stderr}"); +} + #[test] fn test_create_then_evaluate() { // Create a problem diff --git a/src/example_db/fixtures/examples.json b/src/example_db/fixtures/examples.json index 010ba5927..6b14b2492 100644 --- a/src/example_db/fixtures/examples.json +++ b/src/example_db/fixtures/examples.json @@ -38,7 +38,7 @@ {"problem":"SpinGlass","variant":{"graph":"SimpleGraph","weight":"i32"},"instance":{"couplings":[1,1,1,1,1,1,1],"fields":[0,0,0,0,0],"graph":{"inner":{"edge_property":"undirected","edges":[[0,1,null],[1,2,null],[3,4,null],[0,3,null],[1,3,null],[1,4,null],[2,4,null]],"node_holes":[],"nodes":[null,null,null,null,null]}}},"samples":[{"config":[1,0,1,1,0],"metric":{"Valid":-3}}],"optimal":[{"config":[0,0,1,1,0],"metric":{"Valid":-3}},{"config":[0,1,0,0,1],"metric":{"Valid":-3}},{"config":[0,1,0,1,0],"metric":{"Valid":-3}},{"config":[0,1,1,1,0],"metric":{"Valid":-3}},{"config":[1,0,0,0,1],"metric":{"Valid":-3}},{"config":[1,0,1,0,1],"metric":{"Valid":-3}},{"config":[1,0,1,1,0],"metric":{"Valid":-3}},{"config":[1,1,0,0,1],"metric":{"Valid":-3}}]}, {"problem":"SteinerTree","variant":{"graph":"SimpleGraph","weight":"i32"},"instance":{"edge_weights":[2,5,2,1,5,6,1],"graph":{"inner":{"edge_property":"undirected","edges":[[0,1,null],[0,3,null],[1,2,null],[1,3,null],[2,3,null],[2,4,null],[3,4,null]],"node_holes":[],"nodes":[null,null,null,null,null]}},"terminals":[0,2,4]},"samples":[{"config":[1,0,1,1,0,0,1],"metric":{"Valid":6}}],"optimal":[{"config":[1,0,1,1,0,0,1],"metric":{"Valid":6}}]}, {"problem":"TravelingSalesman","variant":{"graph":"SimpleGraph","weight":"i32"},"instance":{"edge_weights":[1,3,2,2,3,1],"graph":{"inner":{"edge_property":"undirected","edges":[[0,1,null],[0,2,null],[0,3,null],[1,2,null],[1,3,null],[2,3,null]],"node_holes":[],"nodes":[null,null,null,null]}}},"samples":[{"config":[1,0,1,1,0,1],"metric":{"Valid":6}}],"optimal":[{"config":[1,0,1,1,0,1],"metric":{"Valid":6}}]}, - {"problem":"TwoDimensionalConsecutiveSets","variant":{},"instance":{"alphabet_size":6,"subsets":[[0,1,2],[3,4,5],[1,3],[2,4],[0,5]]},"samples":[{"config":[0,1,2,2,3,1],"metric":true},{"config":[0,0,0,0,0,0],"metric":false}],"optimal":[{"config":[0,1,2,2,3,1],"metric":true},{"config":[0,2,1,3,2,1],"metric":true},{"config":[1,0,2,1,3,2],"metric":true},{"config":[1,2,0,3,1,2],"metric":true},{"config":[1,2,3,1,2,0],"metric":true},{"config":[1,2,3,3,4,2],"metric":true},{"config":[1,3,2,2,1,0],"metric":true},{"config":[1,3,2,4,3,2],"metric":true},{"config":[2,0,1,1,2,3],"metric":true},{"config":[2,1,0,2,1,3],"metric":true},{"config":[2,1,3,0,2,1],"metric":true},{"config":[2,1,3,2,4,3],"metric":true},{"config":[2,3,1,2,0,1],"metric":true},{"config":[2,3,1,4,2,3],"metric":true},{"config":[2,3,4,2,3,1],"metric":true},{"config":[2,3,4,4,5,3],"metric":true},{"config":[2,4,3,3,2,1],"metric":true},{"config":[2,4,3,5,4,3],"metric":true},{"config":[3,1,2,0,1,2],"metric":true},{"config":[3,1,2,2,3,4],"metric":true},{"config":[3,2,1,1,0,2],"metric":true},{"config":[3,2,1,3,2,4],"metric":true},{"config":[3,2,4,1,3,2],"metric":true},{"config":[3,2,4,3,5,4],"metric":true},{"config":[3,4,2,3,1,2],"metric":true},{"config":[3,4,2,5,3,4],"metric":true},{"config":[3,4,5,3,4,2],"metric":true},{"config":[3,5,4,4,3,2],"metric":true},{"config":[4,2,3,1,2,3],"metric":true},{"config":[4,2,3,3,4,5],"metric":true},{"config":[4,3,2,2,1,3],"metric":true},{"config":[4,3,2,4,3,5],"metric":true},{"config":[4,3,5,2,4,3],"metric":true},{"config":[4,5,3,4,2,3],"metric":true},{"config":[5,3,4,2,3,4],"metric":true},{"config":[5,4,3,3,2,4],"metric":true}]}, + {"problem":"TwoDimensionalConsecutiveSets","variant":{},"instance":{"alphabet_size":6,"subsets":[[0,1,2],[3,4,5],[1,3],[2,4],[0,5]]},"samples":[{"config":[0,1,2,2,3,1],"metric":true},{"config":[0,0,0,0,0,0],"metric":false}],"optimal":[{"config":[0,1,2,2,3,1],"metric":true},{"config":[0,1,2,2,4,1],"metric":true},{"config":[0,1,2,2,5,1],"metric":true},{"config":[0,1,3,3,4,1],"metric":true},{"config":[0,1,3,3,5,1],"metric":true},{"config":[0,1,4,4,5,1],"metric":true},{"config":[0,2,1,3,2,1],"metric":true},{"config":[0,2,1,4,2,1],"metric":true},{"config":[0,2,1,5,2,1],"metric":true},{"config":[0,2,3,3,4,2],"metric":true},{"config":[0,2,3,3,5,2],"metric":true},{"config":[0,2,4,4,5,2],"metric":true},{"config":[0,3,1,4,3,1],"metric":true},{"config":[0,3,1,5,3,1],"metric":true},{"config":[0,3,2,4,3,2],"metric":true},{"config":[0,3,2,5,3,2],"metric":true},{"config":[0,3,4,4,5,3],"metric":true},{"config":[0,4,1,5,4,1],"metric":true},{"config":[0,4,2,5,4,2],"metric":true},{"config":[0,4,3,5,4,3],"metric":true},{"config":[1,0,2,1,3,2],"metric":true},{"config":[1,0,2,1,4,2],"metric":true},{"config":[1,0,2,1,5,2],"metric":true},{"config":[1,0,3,1,4,3],"metric":true},{"config":[1,0,3,1,5,3],"metric":true},{"config":[1,0,4,1,5,4],"metric":true},{"config":[1,2,0,3,1,2],"metric":true},{"config":[1,2,0,4,1,2],"metric":true},{"config":[1,2,0,5,1,2],"metric":true},{"config":[1,2,3,1,2,0],"metric":true},{"config":[1,2,3,3,4,2],"metric":true},{"config":[1,2,3,3,5,2],"metric":true},{"config":[1,2,4,1,2,0],"metric":true},{"config":[1,2,4,4,5,2],"metric":true},{"config":[1,2,5,1,2,0],"metric":true},{"config":[1,3,0,4,1,3],"metric":true},{"config":[1,3,0,5,1,3],"metric":true},{"config":[1,3,2,2,1,0],"metric":true},{"config":[1,3,2,4,3,2],"metric":true},{"config":[1,3,2,5,3,2],"metric":true},{"config":[1,3,4,1,3,0],"metric":true},{"config":[1,3,4,4,5,3],"metric":true},{"config":[1,3,5,1,3,0],"metric":true},{"config":[1,4,0,5,1,4],"metric":true},{"config":[1,4,2,2,1,0],"metric":true},{"config":[1,4,2,5,4,2],"metric":true},{"config":[1,4,3,3,1,0],"metric":true},{"config":[1,4,3,5,4,3],"metric":true},{"config":[1,4,5,1,4,0],"metric":true},{"config":[1,5,2,2,1,0],"metric":true},{"config":[1,5,3,3,1,0],"metric":true},{"config":[1,5,4,4,1,0],"metric":true},{"config":[2,0,1,1,2,3],"metric":true},{"config":[2,0,1,1,2,4],"metric":true},{"config":[2,0,1,1,2,5],"metric":true},{"config":[2,0,3,2,4,3],"metric":true},{"config":[2,0,3,2,5,3],"metric":true},{"config":[2,0,4,2,5,4],"metric":true},{"config":[2,1,0,2,1,3],"metric":true},{"config":[2,1,0,2,1,4],"metric":true},{"config":[2,1,0,2,1,5],"metric":true},{"config":[2,1,3,0,2,1],"metric":true},{"config":[2,1,3,2,4,3],"metric":true},{"config":[2,1,3,2,5,3],"metric":true},{"config":[2,1,4,0,2,1],"metric":true},{"config":[2,1,4,2,5,4],"metric":true},{"config":[2,1,5,0,2,1],"metric":true},{"config":[2,3,0,4,2,3],"metric":true},{"config":[2,3,0,5,2,3],"metric":true},{"config":[2,3,1,2,0,1],"metric":true},{"config":[2,3,1,4,2,3],"metric":true},{"config":[2,3,1,5,2,3],"metric":true},{"config":[2,3,4,2,3,0],"metric":true},{"config":[2,3,4,2,3,1],"metric":true},{"config":[2,3,4,4,5,3],"metric":true},{"config":[2,3,5,2,3,0],"metric":true},{"config":[2,3,5,2,3,1],"metric":true},{"config":[2,4,0,5,2,4],"metric":true},{"config":[2,4,1,2,0,1],"metric":true},{"config":[2,4,1,5,2,4],"metric":true},{"config":[2,4,3,3,2,0],"metric":true},{"config":[2,4,3,3,2,1],"metric":true},{"config":[2,4,3,5,4,3],"metric":true},{"config":[2,4,5,2,4,0],"metric":true},{"config":[2,4,5,2,4,1],"metric":true},{"config":[2,5,1,2,0,1],"metric":true},{"config":[2,5,3,3,2,0],"metric":true},{"config":[2,5,3,3,2,1],"metric":true},{"config":[2,5,4,4,2,0],"metric":true},{"config":[2,5,4,4,2,1],"metric":true},{"config":[3,0,1,1,3,4],"metric":true},{"config":[3,0,1,1,3,5],"metric":true},{"config":[3,0,2,2,3,4],"metric":true},{"config":[3,0,2,2,3,5],"metric":true},{"config":[3,0,4,3,5,4],"metric":true},{"config":[3,1,0,3,1,4],"metric":true},{"config":[3,1,0,3,1,5],"metric":true},{"config":[3,1,2,0,1,2],"metric":true},{"config":[3,1,2,2,3,4],"metric":true},{"config":[3,1,2,2,3,5],"metric":true},{"config":[3,1,4,0,3,1],"metric":true},{"config":[3,1,4,3,5,4],"metric":true},{"config":[3,1,5,0,3,1],"metric":true},{"config":[3,2,0,3,2,4],"metric":true},{"config":[3,2,0,3,2,5],"metric":true},{"config":[3,2,1,1,0,2],"metric":true},{"config":[3,2,1,3,2,4],"metric":true},{"config":[3,2,1,3,2,5],"metric":true},{"config":[3,2,4,0,3,2],"metric":true},{"config":[3,2,4,1,3,2],"metric":true},{"config":[3,2,4,3,5,4],"metric":true},{"config":[3,2,5,0,3,2],"metric":true},{"config":[3,2,5,1,3,2],"metric":true},{"config":[3,4,0,5,3,4],"metric":true},{"config":[3,4,1,3,0,1],"metric":true},{"config":[3,4,1,5,3,4],"metric":true},{"config":[3,4,2,3,0,2],"metric":true},{"config":[3,4,2,3,1,2],"metric":true},{"config":[3,4,2,5,3,4],"metric":true},{"config":[3,4,5,3,4,0],"metric":true},{"config":[3,4,5,3,4,1],"metric":true},{"config":[3,4,5,3,4,2],"metric":true},{"config":[3,5,1,3,0,1],"metric":true},{"config":[3,5,2,3,0,2],"metric":true},{"config":[3,5,2,3,1,2],"metric":true},{"config":[3,5,4,4,3,0],"metric":true},{"config":[3,5,4,4,3,1],"metric":true},{"config":[3,5,4,4,3,2],"metric":true},{"config":[4,0,1,1,4,5],"metric":true},{"config":[4,0,2,2,4,5],"metric":true},{"config":[4,0,3,3,4,5],"metric":true},{"config":[4,1,0,4,1,5],"metric":true},{"config":[4,1,2,0,1,2],"metric":true},{"config":[4,1,2,2,4,5],"metric":true},{"config":[4,1,3,0,1,3],"metric":true},{"config":[4,1,3,3,4,5],"metric":true},{"config":[4,1,5,0,4,1],"metric":true},{"config":[4,2,0,4,2,5],"metric":true},{"config":[4,2,1,1,0,2],"metric":true},{"config":[4,2,1,4,2,5],"metric":true},{"config":[4,2,3,0,2,3],"metric":true},{"config":[4,2,3,1,2,3],"metric":true},{"config":[4,2,3,3,4,5],"metric":true},{"config":[4,2,5,0,4,2],"metric":true},{"config":[4,2,5,1,4,2],"metric":true},{"config":[4,3,0,4,3,5],"metric":true},{"config":[4,3,1,1,0,3],"metric":true},{"config":[4,3,1,4,3,5],"metric":true},{"config":[4,3,2,2,0,3],"metric":true},{"config":[4,3,2,2,1,3],"metric":true},{"config":[4,3,2,4,3,5],"metric":true},{"config":[4,3,5,0,4,3],"metric":true},{"config":[4,3,5,1,4,3],"metric":true},{"config":[4,3,5,2,4,3],"metric":true},{"config":[4,5,1,4,0,1],"metric":true},{"config":[4,5,2,4,0,2],"metric":true},{"config":[4,5,2,4,1,2],"metric":true},{"config":[4,5,3,4,0,3],"metric":true},{"config":[4,5,3,4,1,3],"metric":true},{"config":[4,5,3,4,2,3],"metric":true},{"config":[5,1,2,0,1,2],"metric":true},{"config":[5,1,3,0,1,3],"metric":true},{"config":[5,1,4,0,1,4],"metric":true},{"config":[5,2,1,1,0,2],"metric":true},{"config":[5,2,3,0,2,3],"metric":true},{"config":[5,2,3,1,2,3],"metric":true},{"config":[5,2,4,0,2,4],"metric":true},{"config":[5,2,4,1,2,4],"metric":true},{"config":[5,3,1,1,0,3],"metric":true},{"config":[5,3,2,2,0,3],"metric":true},{"config":[5,3,2,2,1,3],"metric":true},{"config":[5,3,4,0,3,4],"metric":true},{"config":[5,3,4,1,3,4],"metric":true},{"config":[5,3,4,2,3,4],"metric":true},{"config":[5,4,1,1,0,4],"metric":true},{"config":[5,4,2,2,0,4],"metric":true},{"config":[5,4,2,2,1,4],"metric":true},{"config":[5,4,3,3,0,4],"metric":true},{"config":[5,4,3,3,1,4],"metric":true},{"config":[5,4,3,3,2,4],"metric":true}]}, {"problem":"UndirectedTwoCommodityIntegralFlow","variant":{},"instance":{"capacities":[1,1,2],"graph":{"inner":{"edge_property":"undirected","edges":[[0,2,null],[1,2,null],[2,3,null]],"node_holes":[],"nodes":[null,null,null,null]}},"requirement_1":1,"requirement_2":1,"sink_1":3,"sink_2":3,"source_1":0,"source_2":1},"samples":[{"config":[1,0,0,0,0,0,1,0,1,0,1,0],"metric":true}],"optimal":[{"config":[0,0,1,0,1,0,0,0,1,0,1,0],"metric":true},{"config":[1,0,0,0,0,0,1,0,1,0,1,0],"metric":true}]} ], "rules": [ diff --git a/src/models/set/two_dimensional_consecutive_sets.rs b/src/models/set/two_dimensional_consecutive_sets.rs index 2529eb810..5f39ec4ea 100644 --- a/src/models/set/two_dimensional_consecutive_sets.rs +++ b/src/models/set/two_dimensional_consecutive_sets.rs @@ -96,19 +96,24 @@ impl<'de> Deserialize<'de> for TwoDimensionalConsecutiveSets { where D: serde::Deserializer<'de>, { - let mut unchecked = TwoDimensionalConsecutiveSetsUnchecked::deserialize(deserializer)?; - if let Some(message) = validation_error(unchecked.alphabet_size, &mut unchecked.subsets) { - return Err(D::Error::custom(message)); - } + let unchecked = TwoDimensionalConsecutiveSetsUnchecked::deserialize(deserializer)?; + Self::try_new(unchecked.alphabet_size, unchecked.subsets).map_err(D::Error::custom) + } +} +impl TwoDimensionalConsecutiveSets { + /// Create a new 2-Dimensional Consecutive Sets instance, returning validation errors. + pub fn try_new(alphabet_size: usize, subsets: Vec>) -> Result { + let mut subsets = subsets; + if let Some(message) = validation_error(alphabet_size, &mut subsets) { + return Err(message); + } Ok(Self { - alphabet_size: unchecked.alphabet_size, - subsets: unchecked.subsets, + alphabet_size, + subsets, }) } -} -impl TwoDimensionalConsecutiveSets { /// Create a new 2-Dimensional Consecutive Sets instance. /// /// # Panics @@ -116,14 +121,7 @@ impl TwoDimensionalConsecutiveSets { /// Panics if `alphabet_size` is 0, if any subset contains elements /// outside the alphabet, or if any subset has duplicate elements. pub fn new(alphabet_size: usize, subsets: Vec>) -> Self { - let mut subsets = subsets; - if let Some(message) = validation_error(alphabet_size, &mut subsets) { - panic!("{message}"); - } - Self { - alphabet_size, - subsets, - } + Self::try_new(alphabet_size, subsets).unwrap_or_else(|message| panic!("{message}")) } /// Get the alphabet size. @@ -158,12 +156,25 @@ impl Problem for TwoDimensionalConsecutiveSets { return false; } + // Empty labels do not create gaps in the partition order, so compress used labels first. + let mut used = vec![false; self.alphabet_size]; + for &group in config { + used[group] = true; + } + let mut dense_labels = vec![0; self.alphabet_size]; + let mut next_label = 0; + for (label, is_used) in used.into_iter().enumerate() { + if is_used { + dense_labels[label] = next_label; + next_label += 1; + } + } + for subset in &self.subsets { if subset.is_empty() { continue; } - // Collect group indices for this subset's elements - let groups: Vec = subset.iter().map(|&s| config[s]).collect(); + let groups: Vec = subset.iter().map(|&s| dense_labels[config[s]]).collect(); // Intersection constraint: all group indices must be distinct let unique: HashSet = groups.iter().copied().collect(); diff --git a/src/unit_tests/models/set/two_dimensional_consecutive_sets.rs b/src/unit_tests/models/set/two_dimensional_consecutive_sets.rs index 079b9cdf8..ffbb7208e 100644 --- a/src/unit_tests/models/set/two_dimensional_consecutive_sets.rs +++ b/src/unit_tests/models/set/two_dimensional_consecutive_sets.rs @@ -53,6 +53,14 @@ fn test_two_dimensional_consecutive_sets_evaluation() { assert!(!problem.evaluate(&[0, 1, 5, 2, 3, 1])); } +#[test] +fn test_two_dimensional_consecutive_sets_evaluation_ignores_empty_group_labels() { + let problem = TwoDimensionalConsecutiveSets::new(3, vec![vec![0, 1]]); + + // The empty label 1 should be ignored, so this encodes the ordered partition {0} | {1,2}. + assert!(problem.evaluate(&[0, 2, 2])); +} + #[test] fn test_two_dimensional_consecutive_sets_no_instance() { // NO instance from issue: From c7654baff784473eacb04617acac11d8268dbfdf Mon Sep 17 00:00:00 2001 From: Xiwei Pan Date: Fri, 20 Mar 2026 16:02:00 +0800 Subject: [PATCH 6/7] Fix example-db API: adapt to new ModelExampleSpec format The ModelExampleSpec struct changed on main to use instance/optimal_config/optimal_value fields instead of a build closure. Also update Typst paper to use new example-db format. Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/paper/reductions.typ | 4 +-- .../set/two_dimensional_consecutive_sets.rs | 28 ++++++++----------- 2 files changed, 14 insertions(+), 18 deletions(-) diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index 6e6decd1d..80e4e0f43 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -1912,7 +1912,7 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76], let n = x.instance.alphabet_size let subs = x.instance.subsets let m = subs.len() - let sol = x.optimal.at(0) + let sol = (config: x.optimal_config, metric: x.optimal_value) let config = sol.config // Build groups from config: groups.at(g) = list of symbols in group g let groups = range(n).map(g => range(n).filter(s => config.at(s) == g)) @@ -1926,7 +1926,7 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76], ][ This problem generalizes the Consecutive Sets problem (SR18) by requiring not just that each subset's elements appear consecutively in an ordering, but that they be spread across consecutive groups of a partition where each group contributes at most one element per subset. Shown NP-complete by Lipski @lipski1977fct via transformation from Graph 3-Colorability. The problem arises in information storage and retrieval where records must be organized in contiguous blocks. It remains NP-complete if all subsets have at most 5 elements, but is solvable in polynomial time if all subsets have at most 2 elements. The brute-force algorithm assigns each of $n$ symbols to one of up to $n$ groups, giving $O^*(n^n)$ time#footnote[No algorithm improving on brute-force enumeration is known for this problem.]. - *Example.* Let $Sigma = {0, 1, dots, #(n - 1)}$ and $cal(C) = {#range(m).map(i => $Sigma_#(i + 1)$).join(", ")}$ with #subs.enumerate().map(((i, s)) => $Sigma_#(i + 1) = #fmt-set(s)$).join(", "). A valid partition uses $k = #k$ groups: #nonempty.map(((g, elems)) => $X_#(g + 1) = #fmt-set(elems)$).join(", "). Each group intersects every subset in at most one element, and each subset's elements span exactly $|Sigma_j|$ consecutive groups. For instance, $Sigma_1 = {0, 1, 2}$ maps to groups $X_1, X_2, X_3$ (consecutive), and $Sigma_5 = {0, 5}$ maps to groups $X_1, X_2$ (consecutive). The example database records many satisfying assignments for this instance, including encodings that differ only by unused or shifted group labels. + *Example.* Let $Sigma = {0, 1, dots, #(n - 1)}$ and $cal(C) = {#range(m).map(i => $Sigma_#(i + 1)$).join(", ")}$ with #subs.enumerate().map(((i, s)) => $Sigma_#(i + 1) = #fmt-set(s)$).join(", "). A valid partition uses $k = #k$ groups: #nonempty.map(((g, elems)) => $X_#(g + 1) = #fmt-set(elems)$).join(", "). Each group intersects every subset in at most one element, and each subset's elements span exactly $|Sigma_j|$ consecutive groups. For instance, $Sigma_1 = {0, 1, 2}$ maps to groups $X_1, X_2, X_3$ (consecutive), and $Sigma_5 = {0, 5}$ maps to groups $X_1, X_2$ (consecutive). Multiple valid partitions exist for this instance, differing only by unused or shifted group labels. #figure( canvas(length: 1cm, { diff --git a/src/models/set/two_dimensional_consecutive_sets.rs b/src/models/set/two_dimensional_consecutive_sets.rs index 5f39ec4ea..d12d8bd4f 100644 --- a/src/models/set/two_dimensional_consecutive_sets.rs +++ b/src/models/set/two_dimensional_consecutive_sets.rs @@ -208,22 +208,18 @@ crate::declare_variants! { pub(crate) fn canonical_model_example_specs() -> Vec { vec![crate::example_db::specs::ModelExampleSpec { id: "two_dimensional_consecutive_sets", - build: || { - let problem = TwoDimensionalConsecutiveSets::new( - 6, - vec![ - vec![0, 1, 2], - vec![3, 4, 5], - vec![1, 3], - vec![2, 4], - vec![0, 5], - ], - ); - crate::example_db::specs::satisfaction_example( - problem, - vec![vec![0, 1, 2, 2, 3, 1], vec![0, 0, 0, 0, 0, 0]], - ) - }, + instance: Box::new(TwoDimensionalConsecutiveSets::new( + 6, + vec![ + vec![0, 1, 2], + vec![3, 4, 5], + vec![1, 3], + vec![2, 4], + vec![0, 5], + ], + )), + optimal_config: vec![0, 1, 2, 2, 3, 1], + optimal_value: serde_json::json!(true), }] } From 66788e3b6a6d9ed98e535ef3c9338d8447bb7116 Mon Sep 17 00:00:00 2001 From: Xiwei Pan Date: Fri, 20 Mar 2026 16:20:37 +0800 Subject: [PATCH 7/7] Separate validation from subset sorting in try_new Split validation_error (which validated AND sorted) into a pure validate() function and an explicit sorting step in try_new(). Co-Authored-By: Claude Opus 4.6 (1M context) --- .../set/two_dimensional_consecutive_sets.rs | 21 ++++++++----------- 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/src/models/set/two_dimensional_consecutive_sets.rs b/src/models/set/two_dimensional_consecutive_sets.rs index d12d8bd4f..ca41c02d0 100644 --- a/src/models/set/two_dimensional_consecutive_sets.rs +++ b/src/models/set/two_dimensional_consecutive_sets.rs @@ -67,28 +67,27 @@ struct TwoDimensionalConsecutiveSetsUnchecked { subsets: Vec>, } -fn validation_error(alphabet_size: usize, subsets: &mut [Vec]) -> Option { +fn validate(alphabet_size: usize, subsets: &[Vec]) -> Result<(), String> { if alphabet_size == 0 { - return Some("Alphabet size must be positive".to_string()); + return Err("Alphabet size must be positive".to_string()); } - for (i, subset) in subsets.iter_mut().enumerate() { + for (i, subset) in subsets.iter().enumerate() { let mut seen = HashSet::new(); - for &elem in subset.iter() { + for &elem in subset { if elem >= alphabet_size { - return Some(format!( + return Err(format!( "Subset {} contains element {} which is outside alphabet of size {}", i, elem, alphabet_size )); } if !seen.insert(elem) { - return Some(format!("Subset {} contains duplicate element {}", i, elem)); + return Err(format!("Subset {} contains duplicate element {}", i, elem)); } } - subset.sort(); } - None + Ok(()) } impl<'de> Deserialize<'de> for TwoDimensionalConsecutiveSets { @@ -104,10 +103,8 @@ impl<'de> Deserialize<'de> for TwoDimensionalConsecutiveSets { impl TwoDimensionalConsecutiveSets { /// Create a new 2-Dimensional Consecutive Sets instance, returning validation errors. pub fn try_new(alphabet_size: usize, subsets: Vec>) -> Result { - let mut subsets = subsets; - if let Some(message) = validation_error(alphabet_size, &mut subsets) { - return Err(message); - } + validate(alphabet_size, &subsets)?; + let subsets = subsets.into_iter().map(|mut s| { s.sort(); s }).collect(); Ok(Self { alphabet_size, subsets,