From 1977ba9226421da12e53f731f908f9546c9925aa Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Mon, 16 Mar 2026 21:25:19 +0800 Subject: [PATCH 1/9] Add plan for #421: ConsecutiveSets model --- docs/plans/2026-03-16-consecutive-sets.md | 91 +++++++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 docs/plans/2026-03-16-consecutive-sets.md diff --git a/docs/plans/2026-03-16-consecutive-sets.md b/docs/plans/2026-03-16-consecutive-sets.md new file mode 100644 index 000000000..2e94afa88 --- /dev/null +++ b/docs/plans/2026-03-16-consecutive-sets.md @@ -0,0 +1,91 @@ +# Plan: Add ConsecutiveSets Model (#421) + +## Context + +**ConsecutiveSets** (Garey & Johnson A4 SR18): Given finite alphabet Σ (size `alphabet_size`), collection C = {Σ₁, ..., Σₙ} of subsets of Σ, and positive integer K (`bound_k`), decide if there exists a string w ∈ Σ* with |w| ≤ K such that for each i, the elements of Σᵢ occur in a consecutive block of |Σᵢ| symbols of w. + +- **Type:** Satisfaction problem (Metric = bool) +- **NP-completeness:** Kou 1977, reduction from Hamiltonian Path +- **Category:** `set/` (universe + subsets input structure) +- **Complexity:** `"alphabet_size^bound_k * num_subsets"` (brute-force) + +## Batch 1: Implementation (Steps 1–5.5) + +### Task 1: Create model file `src/models/set/consecutive_sets.rs` + +**Struct:** +```rust +pub struct ConsecutiveSets { + alphabet_size: usize, + subsets: Vec>, + bound_k: usize, +} +``` + +**Constructor** `new(alphabet_size, subsets, bound_k)`: +- Validate: all subset elements < alphabet_size +- Validate: bound_k > 0 +- Validate: no duplicate elements within individual subsets +- Sort each subset for canonical form + +**Getters:** `alphabet_size()`, `num_subsets()`, `bound_k()`, `subsets()` + +**Problem impl:** +- `NAME = "ConsecutiveSets"` +- `type Metric = bool` +- `dims()`: `vec![alphabet_size + 1; bound_k]` — values 0..alphabet_size-1 are symbols, alphabet_size = "unused" +- `evaluate(config)`: + 1. Validate config length == bound_k and all values <= alphabet_size + 2. Build string w: take symbols until trailing "unused" values; reject if any internal "unused" + 3. For each subset Σᵢ: scan all windows of length |Σᵢ| in w, check if window contains exactly the elements of Σᵢ (as a set). If no valid window found for any subset, return false. + 4. Return true if all subsets satisfied. +- `variant()`: `variant_params![]` + +**SatisfactionProblem** marker trait impl. + +**declare_variants!:** +```rust +default sat ConsecutiveSets => "alphabet_size^bound_k * num_subsets", +``` + +**ProblemSchemaEntry** with fields: `alphabet_size` (usize), `subsets` (Vec>), `bound_k` (usize). + +**Canonical example spec** using YES instance from issue: alphabet_size=6, subsets=[{0,4},{2,4},{2,5},{1,5},{1,3}], bound_k=6, solution=[0,4,2,5,1,3]. + +### Task 2: Register model + +- `src/models/set/mod.rs`: Add `pub(crate) mod consecutive_sets;`, `pub use`, and extend `canonical_model_example_specs()` +- `src/models/mod.rs`: Add `ConsecutiveSets` to `pub use set::` re-export + +### Task 3: Write unit tests `src/unit_tests/models/set/consecutive_sets.rs` + +Tests (reference: `exact_cover_by_3_sets.rs`): +- `test_consecutive_sets_creation` — dims, getters, num_variables +- `test_consecutive_sets_evaluation` — YES config [0,4,2,5,1,3] and NO configs +- `test_consecutive_sets_no_instance` — NO instance from issue (conflicting block constraints) +- `test_consecutive_sets_serialization` — serde round-trip +- `test_consecutive_sets_solver` — BruteForce finds satisfying configs, verify known solution present +- `test_consecutive_sets_empty` — edge case: empty subsets collection +- `test_consecutive_sets_invalid_constructor` — #[should_panic] for bad inputs + +Link via `#[path]` in model file. + +### Task 4: Verify build + +Run `make check` (fmt + clippy + test). Fix any issues. + +### Task 5: Run trait_consistency + +Ensure the new model passes all trait consistency checks automatically via `declare_variants!`. + +## Batch 2: Paper Entry (Step 6) + +### Task 6: Write paper entry in `docs/paper/reductions.typ` + +- Add `"ConsecutiveSets": [Consecutive Sets]` to `display-name` dict +- Add `#problem-def("ConsecutiveSets")[definition][body]` with: + - Formal definition from Garey & Johnson + - NP-completeness reference (Kou 1977) + - Connection to consecutive ones property + - Circular variant mention (Booth 1975) +- Run `make paper` to verify compilation From 6e7b16731d561fbe505503c4ff24a93b6552766f Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Mon, 16 Mar 2026 21:33:42 +0800 Subject: [PATCH 2/9] Implement ConsecutiveSets model (#421) --- src/models/mod.rs | 2 +- src/models/set/consecutive_sets.rs | 218 ++++++++++++++++++ src/models/set/mod.rs | 8 +- src/unit_tests/models/set/consecutive_sets.rs | 96 ++++++++ 4 files changed, 321 insertions(+), 3 deletions(-) create mode 100644 src/models/set/consecutive_sets.rs create mode 100644 src/unit_tests/models/set/consecutive_sets.rs diff --git a/src/models/mod.rs b/src/models/mod.rs index 94e95d0ca..a30aca928 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -22,4 +22,4 @@ pub use misc::{ BinPacking, Factoring, FlowShopScheduling, Knapsack, LongestCommonSubsequence, MinimumTardinessSequencing, PaintShop, ShortestCommonSupersequence, SubsetSum, }; -pub use set::{ExactCoverBy3Sets, MaximumSetPacking, MinimumSetCovering}; +pub use set::{ConsecutiveSets, ExactCoverBy3Sets, MaximumSetPacking, MinimumSetCovering}; diff --git a/src/models/set/consecutive_sets.rs b/src/models/set/consecutive_sets.rs new file mode 100644 index 000000000..be041ca8d --- /dev/null +++ b/src/models/set/consecutive_sets.rs @@ -0,0 +1,218 @@ +//! Consecutive Sets problem implementation. +//! +//! Given an alphabet of size n, a collection of subsets of the alphabet, and a +//! bound K, determine if there exists a string of length at most K over the +//! alphabet such that the elements of each subset appear consecutively (as a +//! contiguous block in some order) within the string. + +use crate::registry::{FieldInfo, ProblemSchemaEntry}; +use crate::traits::{Problem, SatisfactionProblem}; +use serde::{Deserialize, Serialize}; +use std::collections::HashSet; + +inventory::submit! { + ProblemSchemaEntry { + name: "ConsecutiveSets", + display_name: "Consecutive Sets", + aliases: &[], + dimensions: &[], + module_path: module_path!(), + description: "Determine if a string exists where each subset's elements appear consecutively", + fields: &[ + FieldInfo { name: "alphabet_size", type_name: "usize", description: "Size of the alphabet (elements are 0..alphabet_size-1)" }, + FieldInfo { name: "subsets", type_name: "Vec>", description: "Collection of subsets of the alphabet" }, + FieldInfo { name: "bound_k", type_name: "usize", description: "Maximum string length K" }, + ], + } +} + +/// Consecutive Sets problem. +/// +/// Given an alphabet {0, 1, ..., n-1}, a collection of subsets, and a bound K, +/// determine if there exists a string w of length at most K over the alphabet +/// such that the elements of each subset appear as a contiguous block (in any +/// order) within w. +/// +/// This problem is NP-complete and arises in physical mapping of DNA and in +/// consecutive arrangements of hypergraph vertices. +/// +/// # Example +/// +/// ``` +/// use problemreductions::models::set::ConsecutiveSets; +/// use problemreductions::{Problem, Solver, BruteForce}; +/// +/// // Alphabet: {0, 1, 2, 3, 4, 5}, subsets that must appear consecutively +/// let problem = ConsecutiveSets::new( +/// 6, +/// vec![vec![0, 4], vec![2, 4], vec![2, 5], vec![1, 5], vec![1, 3]], +/// 6, +/// ); +/// +/// let solver = BruteForce::new(); +/// let solution = solver.find_satisfying(&problem); +/// +/// // w = [0, 4, 2, 5, 1, 3] is a valid solution +/// assert!(solution.is_some()); +/// assert!(problem.evaluate(&solution.unwrap())); +/// ``` +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ConsecutiveSets { + /// Size of the alphabet (elements are 0..alphabet_size-1). + alphabet_size: usize, + /// Collection of subsets of the alphabet, each sorted in canonical form. + subsets: Vec>, + /// Maximum string length K. + bound_k: usize, +} + +impl ConsecutiveSets { + /// Create a new Consecutive Sets problem. + /// + /// # Panics + /// + /// Panics if `bound_k` is zero, if any subset contains duplicate elements, + /// or if any element is outside the alphabet. + pub fn new(alphabet_size: usize, subsets: Vec>, bound_k: usize) -> Self { + assert!(bound_k > 0, "bound_k must be positive, got 0"); + let mut subsets = subsets; + for (i, subset) in subsets.iter_mut().enumerate() { + let mut seen = HashSet::with_capacity(subset.len()); + 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 elements", + i + ); + } + subset.sort(); + } + Self { + alphabet_size, + subsets, + bound_k, + } + } + + /// Get the alphabet size. + pub fn alphabet_size(&self) -> usize { + self.alphabet_size + } + + /// Get the number of subsets in the collection. + pub fn num_subsets(&self) -> usize { + self.subsets.len() + } + + /// Get the bound K. + pub fn bound_k(&self) -> usize { + self.bound_k + } + + /// Get the subsets. + pub fn subsets(&self) -> &[Vec] { + &self.subsets + } +} + +impl Problem for ConsecutiveSets { + const NAME: &'static str = "ConsecutiveSets"; + type Metric = bool; + + fn dims(&self) -> Vec { + // Each position can be any symbol (0..alphabet_size-1) or "unused" (alphabet_size) + vec![self.alphabet_size + 1; self.bound_k] + } + + fn evaluate(&self, config: &[usize]) -> bool { + // 1. Validate config + if config.len() != self.bound_k || config.iter().any(|&v| v > self.alphabet_size) { + return false; + } + + // 2. Build string: find the actual string length (strip trailing "unused") + let unused = self.alphabet_size; + let str_len = config + .iter() + .rposition(|&v| v != unused) + .map_or(0, |p| p + 1); + + // 3. Check no internal "unused" symbols + let w = &config[..str_len]; + if w.contains(&unused) { + return false; + } + + // 4. Check each subset has a consecutive block + for subset in &self.subsets { + let subset_len = subset.len(); + if subset_len == 0 { + continue; // empty subset trivially satisfied + } + if subset_len > str_len { + return false; // can't fit + } + + let mut found = false; + for start in 0..=(str_len - subset_len) { + let window = &w[start..start + subset_len]; + // Check if window is a permutation of subset + let mut window_sorted: Vec = window.to_vec(); + window_sorted.sort(); + if window_sorted == *subset { + // subset is already sorted + found = true; + break; + } + } + if !found { + return false; + } + } + + true + } + + fn variant() -> Vec<(&'static str, &'static str)> { + crate::variant_params![] + } +} + +impl SatisfactionProblem for ConsecutiveSets {} + +crate::declare_variants! { + default sat ConsecutiveSets => "alphabet_size^bound_k * num_subsets", +} + +#[cfg(feature = "example-db")] +pub(crate) fn canonical_model_example_specs() -> Vec { + vec![crate::example_db::specs::ModelExampleSpec { + id: "consecutive_sets", + build: || { + // YES instance from issue: w = [0, 4, 2, 5, 1, 3] + let problem = ConsecutiveSets::new( + 6, + vec![ + vec![0, 4], + vec![2, 4], + vec![2, 5], + vec![1, 5], + vec![1, 3], + ], + 6, + ); + crate::example_db::specs::satisfaction_example(problem, vec![vec![0, 4, 2, 5, 1, 3]]) + }, + }] +} + +#[cfg(test)] +#[path = "../../unit_tests/models/set/consecutive_sets.rs"] +mod tests; diff --git a/src/models/set/mod.rs b/src/models/set/mod.rs index 555b140bb..8b854248b 100644 --- a/src/models/set/mod.rs +++ b/src/models/set/mod.rs @@ -1,14 +1,17 @@ //! Set-based problems. //! //! This module contains NP-hard problems based on set operations: -//! - [`MinimumSetCovering`]: Minimum weight set cover -//! - [`MaximumSetPacking`]: Maximum weight set packing +//! - [`ConsecutiveSets`]: Consecutive arrangement of subset elements in a string //! - [`ExactCoverBy3Sets`]: Exact cover by 3-element subsets (X3C) +//! - [`MaximumSetPacking`]: Maximum weight set packing +//! - [`MinimumSetCovering`]: Minimum weight set cover +pub(crate) mod consecutive_sets; pub(crate) mod exact_cover_by_3_sets; pub(crate) mod maximum_set_packing; pub(crate) mod minimum_set_covering; +pub use consecutive_sets::ConsecutiveSets; pub use exact_cover_by_3_sets::ExactCoverBy3Sets; pub use maximum_set_packing::MaximumSetPacking; pub use minimum_set_covering::MinimumSetCovering; @@ -16,6 +19,7 @@ pub use minimum_set_covering::MinimumSetCovering; #[cfg(feature = "example-db")] pub(crate) fn canonical_model_example_specs() -> Vec { let mut specs = Vec::new(); + specs.extend(consecutive_sets::canonical_model_example_specs()); specs.extend(exact_cover_by_3_sets::canonical_model_example_specs()); specs.extend(maximum_set_packing::canonical_model_example_specs()); specs.extend(minimum_set_covering::canonical_model_example_specs()); diff --git a/src/unit_tests/models/set/consecutive_sets.rs b/src/unit_tests/models/set/consecutive_sets.rs new file mode 100644 index 000000000..82b010203 --- /dev/null +++ b/src/unit_tests/models/set/consecutive_sets.rs @@ -0,0 +1,96 @@ +use super::*; +use crate::solvers::BruteForce; +use crate::traits::Problem; + +#[test] +fn test_consecutive_sets_creation() { + let problem = ConsecutiveSets::new( + 6, + vec![vec![0, 4], vec![2, 4], vec![2, 5], vec![1, 5], vec![1, 3]], + 6, + ); + assert_eq!(problem.alphabet_size(), 6); + assert_eq!(problem.num_subsets(), 5); + assert_eq!(problem.bound_k(), 6); + assert_eq!(problem.num_variables(), 6); + assert_eq!(problem.dims(), vec![7; 6]); // alphabet_size + 1 = 7 +} + +#[test] +fn test_consecutive_sets_evaluation() { + let problem = ConsecutiveSets::new( + 6, + vec![vec![0, 4], vec![2, 4], vec![2, 5], vec![1, 5], vec![1, 3]], + 6, + ); + // YES: w = [0, 4, 2, 5, 1, 3] + assert!(problem.evaluate(&[0, 4, 2, 5, 1, 3])); + // NO: identity string [0, 1, 2, 3, 4, 5] — {0,4} not adjacent + assert!(!problem.evaluate(&[0, 1, 2, 3, 4, 5])); + // NO: all unused (empty string can't satisfy non-empty subsets) + assert!(!problem.evaluate(&[6, 6, 6, 6, 6, 6])); +} + +#[test] +fn test_consecutive_sets_no_instance() { + // NO instance: alphabet_size=3, subsets=[{0,1},{1,2},{0,2}], bound_k=3 + // In any string of length <= 3 over {0,1,2}, we cannot have all three pairs adjacent. + // E.g., [0,1,2] satisfies {0,1} and {1,2} but not {0,2}. + // Search space: 4^3 = 64 configs, very fast. + let problem = ConsecutiveSets::new(3, vec![vec![0, 1], vec![1, 2], vec![0, 2]], 3); + let solver = BruteForce::new(); + let solutions = solver.find_all_satisfying(&problem); + assert!(solutions.is_empty()); +} + +#[test] +fn test_consecutive_sets_solver() { + // Small YES instance: alphabet_size=3, subsets=[{0,1},{1,2}], bound_k=3 + // Valid string: [0, 1, 2] — {0,1} at positions 0-1, {1,2} at positions 1-2 + let problem = ConsecutiveSets::new(3, vec![vec![0, 1], vec![1, 2]], 3); + let solver = BruteForce::new(); + let solutions = solver.find_all_satisfying(&problem); + assert!(!solutions.is_empty()); + for sol in &solutions { + assert!(problem.evaluate(sol)); + } +} + +#[test] +fn test_consecutive_sets_serialization() { + let problem = ConsecutiveSets::new(6, vec![vec![0, 4], vec![2, 4]], 6); + let json = serde_json::to_string(&problem).unwrap(); + let deserialized: ConsecutiveSets = 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.bound_k(), problem.bound_k()); +} + +#[test] +fn test_consecutive_sets_empty_subsets() { + // Empty collection — trivially satisfiable by any string (even empty) + let problem = ConsecutiveSets::new(3, vec![], 3); + // All unused = empty string is fine + assert!(problem.evaluate(&[3, 3, 3])); + let solver = BruteForce::new(); + let solutions = solver.find_all_satisfying(&problem); + assert!(!solutions.is_empty()); +} + +#[test] +#[should_panic(expected = "outside alphabet")] +fn test_consecutive_sets_element_out_of_range() { + ConsecutiveSets::new(3, vec![vec![0, 5]], 3); +} + +#[test] +#[should_panic(expected = "duplicate elements")] +fn test_consecutive_sets_duplicate_elements() { + ConsecutiveSets::new(3, vec![vec![1, 1]], 3); +} + +#[test] +#[should_panic(expected = "bound_k must be positive")] +fn test_consecutive_sets_zero_bound() { + ConsecutiveSets::new(3, vec![vec![0, 1]], 0); +} From 41dc3df73b0412b4e7a6d8ac8eb8eeb5c7f76946 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Mon, 16 Mar 2026 21:43:21 +0800 Subject: [PATCH 3/9] Add trait_consistency entry and regenerate schemas --- docs/src/reductions/problem_schemas.json | 21 ++ docs/src/reductions/reduction_graph.json | 233 ++++++++++++----------- src/unit_tests/trait_consistency.rs | 4 + 3 files changed, 145 insertions(+), 113 deletions(-) diff --git a/docs/src/reductions/problem_schemas.json b/docs/src/reductions/problem_schemas.json index 5949a528f..9ac27b679 100644 --- a/docs/src/reductions/problem_schemas.json +++ b/docs/src/reductions/problem_schemas.json @@ -89,6 +89,27 @@ } ] }, + { + "name": "ConsecutiveSets", + "description": "Determine if a string exists where each subset's elements appear consecutively", + "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": "bound_k", + "type_name": "usize", + "description": "Maximum string length K" + } + ] + }, { "name": "ExactCoverBy3Sets", "description": "Determine if a collection of 3-element subsets contains an exact cover", diff --git a/docs/src/reductions/reduction_graph.json b/docs/src/reductions/reduction_graph.json index cd80f3f06..ebf3dbbbc 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": "ConsecutiveSets", + "variant": {}, + "category": "set", + "doc_path": "models/set/struct.ConsecutiveSets.html", + "complexity": "alphabet_size^bound_k * num_subsets" + }, { "name": "ExactCoverBy3Sets", "variant": {}, @@ -539,7 +546,7 @@ "edges": [ { "source": 3, - "target": 12, + "target": 13, "overhead": [ { "field": "num_vars", @@ -554,7 +561,7 @@ }, { "source": 4, - "target": 12, + "target": 13, "overhead": [ { "field": "num_vars", @@ -569,7 +576,7 @@ }, { "source": 4, - "target": 54, + "target": 55, "overhead": [ { "field": "num_spins", @@ -583,7 +590,7 @@ "doc_path": "rules/circuit_spinglass/index.html" }, { - "source": 8, + "source": 9, "target": 4, "overhead": [ { @@ -598,8 +605,8 @@ "doc_path": "rules/factoring_circuit/index.html" }, { - "source": 8, - "target": 13, + "source": 9, + "target": 14, "overhead": [ { "field": "num_vars", @@ -613,8 +620,8 @@ "doc_path": "rules/factoring_ilp/index.html" }, { - "source": 12, - "target": 13, + "source": 13, + "target": 14, "overhead": [ { "field": "num_vars", @@ -628,8 +635,8 @@ "doc_path": "rules/ilp_bool_ilp_i32/index.html" }, { - "source": 12, - "target": 49, + "source": 13, + "target": 50, "overhead": [ { "field": "num_vars", @@ -639,8 +646,8 @@ "doc_path": "rules/ilp_qubo/index.html" }, { - "source": 16, - "target": 19, + "source": 17, + "target": 20, "overhead": [ { "field": "num_vertices", @@ -654,8 +661,8 @@ "doc_path": "rules/kcoloring_casts/index.html" }, { - "source": 19, - "target": 12, + "source": 20, + "target": 13, "overhead": [ { "field": "num_vars", @@ -669,8 +676,8 @@ "doc_path": "rules/coloring_ilp/index.html" }, { - "source": 19, - "target": 49, + "source": 20, + "target": 50, "overhead": [ { "field": "num_vars", @@ -680,8 +687,8 @@ "doc_path": "rules/coloring_qubo/index.html" }, { - "source": 20, - "target": 22, + "source": 21, + "target": 23, "overhead": [ { "field": "num_vars", @@ -695,8 +702,8 @@ "doc_path": "rules/ksatisfiability_casts/index.html" }, { - "source": 20, - "target": 49, + "source": 21, + "target": 50, "overhead": [ { "field": "num_vars", @@ -706,8 +713,8 @@ "doc_path": "rules/ksatisfiability_qubo/index.html" }, { - "source": 21, - "target": 22, + "source": 22, + "target": 23, "overhead": [ { "field": "num_vars", @@ -721,8 +728,8 @@ "doc_path": "rules/ksatisfiability_casts/index.html" }, { - "source": 21, - "target": 49, + "source": 22, + "target": 50, "overhead": [ { "field": "num_vars", @@ -732,8 +739,8 @@ "doc_path": "rules/ksatisfiability_qubo/index.html" }, { - "source": 21, - "target": 58, + "source": 22, + "target": 59, "overhead": [ { "field": "num_elements", @@ -743,8 +750,8 @@ "doc_path": "rules/ksatisfiability_subsetsum/index.html" }, { - "source": 22, - "target": 51, + "source": 23, + "target": 52, "overhead": [ { "field": "num_clauses", @@ -762,8 +769,8 @@ "doc_path": "rules/sat_ksat/index.html" }, { - "source": 23, - "target": 49, + "source": 24, + "target": 50, "overhead": [ { "field": "num_vars", @@ -773,8 +780,8 @@ "doc_path": "rules/knapsack_qubo/index.html" }, { - "source": 24, - "target": 12, + "source": 25, + "target": 13, "overhead": [ { "field": "num_vars", @@ -788,8 +795,8 @@ "doc_path": "rules/longestcommonsubsequence_ilp/index.html" }, { - "source": 25, - "target": 54, + "source": 26, + "target": 55, "overhead": [ { "field": "num_spins", @@ -803,8 +810,8 @@ "doc_path": "rules/spinglass_maxcut/index.html" }, { - "source": 27, - "target": 12, + "source": 28, + "target": 13, "overhead": [ { "field": "num_vars", @@ -818,8 +825,8 @@ "doc_path": "rules/maximumclique_ilp/index.html" }, { - "source": 27, - "target": 31, + "source": 28, + "target": 32, "overhead": [ { "field": "num_vertices", @@ -833,8 +840,8 @@ "doc_path": "rules/maximumclique_maximumindependentset/index.html" }, { - "source": 28, - "target": 29, + "source": 29, + "target": 30, "overhead": [ { "field": "num_vertices", @@ -848,8 +855,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 28, - "target": 33, + "source": 29, + "target": 34, "overhead": [ { "field": "num_vertices", @@ -863,8 +870,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 29, - "target": 34, + "source": 30, + "target": 35, "overhead": [ { "field": "num_vertices", @@ -878,8 +885,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 30, - "target": 28, + "source": 31, + "target": 29, "overhead": [ { "field": "num_vertices", @@ -893,8 +900,8 @@ "doc_path": "rules/maximumindependentset_gridgraph/index.html" }, { - "source": 30, - "target": 31, + "source": 31, + "target": 32, "overhead": [ { "field": "num_vertices", @@ -908,8 +915,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 30, - "target": 32, + "source": 31, + "target": 33, "overhead": [ { "field": "num_vertices", @@ -923,8 +930,8 @@ "doc_path": "rules/maximumindependentset_triangular/index.html" }, { - "source": 30, - "target": 36, + "source": 31, + "target": 37, "overhead": [ { "field": "num_sets", @@ -938,8 +945,8 @@ "doc_path": "rules/maximumindependentset_maximumsetpacking/index.html" }, { - "source": 31, - "target": 27, + "source": 32, + "target": 28, "overhead": [ { "field": "num_vertices", @@ -953,8 +960,8 @@ "doc_path": "rules/maximumindependentset_maximumclique/index.html" }, { - "source": 31, - "target": 38, + "source": 32, + "target": 39, "overhead": [ { "field": "num_sets", @@ -968,8 +975,8 @@ "doc_path": "rules/maximumindependentset_maximumsetpacking/index.html" }, { - "source": 31, - "target": 45, + "source": 32, + "target": 46, "overhead": [ { "field": "num_vertices", @@ -983,8 +990,8 @@ "doc_path": "rules/minimumvertexcover_maximumindependentset/index.html" }, { - "source": 32, - "target": 34, + "source": 33, + "target": 35, "overhead": [ { "field": "num_vertices", @@ -998,8 +1005,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 33, - "target": 30, + "source": 34, + "target": 31, "overhead": [ { "field": "num_vertices", @@ -1013,8 +1020,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 33, - "target": 34, + "source": 34, + "target": 35, "overhead": [ { "field": "num_vertices", @@ -1028,8 +1035,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 34, - "target": 31, + "source": 35, + "target": 32, "overhead": [ { "field": "num_vertices", @@ -1043,8 +1050,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 35, - "target": 12, + "source": 36, + "target": 13, "overhead": [ { "field": "num_vars", @@ -1058,8 +1065,8 @@ "doc_path": "rules/maximummatching_ilp/index.html" }, { - "source": 35, - "target": 38, + "source": 36, + "target": 39, "overhead": [ { "field": "num_sets", @@ -1073,8 +1080,8 @@ "doc_path": "rules/maximummatching_maximumsetpacking/index.html" }, { - "source": 36, - "target": 30, + "source": 37, + "target": 31, "overhead": [ { "field": "num_vertices", @@ -1088,8 +1095,8 @@ "doc_path": "rules/maximumindependentset_maximumsetpacking/index.html" }, { - "source": 36, - "target": 38, + "source": 37, + "target": 39, "overhead": [ { "field": "num_sets", @@ -1103,8 +1110,8 @@ "doc_path": "rules/maximumsetpacking_casts/index.html" }, { - "source": 37, - "target": 49, + "source": 38, + "target": 50, "overhead": [ { "field": "num_vars", @@ -1114,8 +1121,8 @@ "doc_path": "rules/maximumsetpacking_qubo/index.html" }, { - "source": 38, - "target": 12, + "source": 39, + "target": 13, "overhead": [ { "field": "num_vars", @@ -1129,8 +1136,8 @@ "doc_path": "rules/maximumsetpacking_ilp/index.html" }, { - "source": 38, - "target": 31, + "source": 39, + "target": 32, "overhead": [ { "field": "num_vertices", @@ -1144,8 +1151,8 @@ "doc_path": "rules/maximumindependentset_maximumsetpacking/index.html" }, { - "source": 38, - "target": 37, + "source": 39, + "target": 38, "overhead": [ { "field": "num_sets", @@ -1159,8 +1166,8 @@ "doc_path": "rules/maximumsetpacking_casts/index.html" }, { - "source": 39, - "target": 12, + "source": 40, + "target": 13, "overhead": [ { "field": "num_vars", @@ -1174,8 +1181,8 @@ "doc_path": "rules/minimumdominatingset_ilp/index.html" }, { - "source": 42, - "target": 12, + "source": 43, + "target": 13, "overhead": [ { "field": "num_vars", @@ -1189,8 +1196,8 @@ "doc_path": "rules/minimumsetcovering_ilp/index.html" }, { - "source": 45, - "target": 31, + "source": 46, + "target": 32, "overhead": [ { "field": "num_vertices", @@ -1204,8 +1211,8 @@ "doc_path": "rules/minimumvertexcover_maximumindependentset/index.html" }, { - "source": 45, - "target": 42, + "source": 46, + "target": 43, "overhead": [ { "field": "num_sets", @@ -1219,8 +1226,8 @@ "doc_path": "rules/minimumvertexcover_minimumsetcovering/index.html" }, { - "source": 49, - "target": 12, + "source": 50, + "target": 13, "overhead": [ { "field": "num_vars", @@ -1234,8 +1241,8 @@ "doc_path": "rules/qubo_ilp/index.html" }, { - "source": 49, - "target": 53, + "source": 50, + "target": 54, "overhead": [ { "field": "num_spins", @@ -1245,7 +1252,7 @@ "doc_path": "rules/spinglass_qubo/index.html" }, { - "source": 51, + "source": 52, "target": 4, "overhead": [ { @@ -1260,8 +1267,8 @@ "doc_path": "rules/sat_circuitsat/index.html" }, { - "source": 51, - "target": 16, + "source": 52, + "target": 17, "overhead": [ { "field": "num_vertices", @@ -1275,8 +1282,8 @@ "doc_path": "rules/sat_coloring/index.html" }, { - "source": 51, - "target": 21, + "source": 52, + "target": 22, "overhead": [ { "field": "num_clauses", @@ -1290,8 +1297,8 @@ "doc_path": "rules/sat_ksat/index.html" }, { - "source": 51, - "target": 30, + "source": 52, + "target": 31, "overhead": [ { "field": "num_vertices", @@ -1305,8 +1312,8 @@ "doc_path": "rules/sat_maximumindependentset/index.html" }, { - "source": 51, - "target": 39, + "source": 52, + "target": 40, "overhead": [ { "field": "num_vertices", @@ -1320,8 +1327,8 @@ "doc_path": "rules/sat_minimumdominatingset/index.html" }, { - "source": 53, - "target": 49, + "source": 54, + "target": 50, "overhead": [ { "field": "num_vars", @@ -1331,8 +1338,8 @@ "doc_path": "rules/spinglass_qubo/index.html" }, { - "source": 54, - "target": 25, + "source": 55, + "target": 26, "overhead": [ { "field": "num_vertices", @@ -1346,8 +1353,8 @@ "doc_path": "rules/spinglass_maxcut/index.html" }, { - "source": 54, - "target": 53, + "source": 55, + "target": 54, "overhead": [ { "field": "num_spins", @@ -1361,8 +1368,8 @@ "doc_path": "rules/spinglass_casts/index.html" }, { - "source": 59, - "target": 12, + "source": 60, + "target": 13, "overhead": [ { "field": "num_vars", @@ -1376,8 +1383,8 @@ "doc_path": "rules/travelingsalesman_ilp/index.html" }, { - "source": 59, - "target": 49, + "source": 60, + "target": 50, "overhead": [ { "field": "num_vars", diff --git a/src/unit_tests/trait_consistency.rs b/src/unit_tests/trait_consistency.rs index 122362efa..01861b154 100644 --- a/src/unit_tests/trait_consistency.rs +++ b/src/unit_tests/trait_consistency.rs @@ -138,6 +138,10 @@ fn test_all_problems_implement_trait_correctly() { &MinimumTardinessSequencing::new(3, vec![2, 3, 1], vec![(0, 2)]), "MinimumTardinessSequencing", ); + check_problem_trait( + &ConsecutiveSets::new(3, vec![vec![0, 1], vec![1, 2]], 3), + "ConsecutiveSets", + ); } #[test] From 64a1fb395ec87ae979af7a2aa16d2eddab094947 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Mon, 16 Mar 2026 22:00:30 +0800 Subject: [PATCH 4/9] Review fixes: prelude re-export, test improvements, fixture regen --- src/example_db/fixtures/examples.json | 1 + src/lib.rs | 4 +++- src/unit_tests/models/set/consecutive_sets.rs | 18 ++++++++++++++++++ 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/example_db/fixtures/examples.json b/src/example_db/fixtures/examples.json index 83015b13d..3abe9f6ba 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":"ConsecutiveSets","variant":{},"instance":{"alphabet_size":6,"bound_k":6,"subsets":[[0,4],[2,4],[2,5],[1,5],[1,3]]},"samples":[{"config":[0,4,2,5,1,3],"metric":true}],"optimal":[{"config":[0,4,2,5,1,3],"metric":true},{"config":[3,1,5,2,4,0],"metric":true}]}, {"problem":"ExactCoverBy3Sets","variant":{},"instance":{"subsets":[[0,1,2],[0,2,4],[3,4,5],[3,5,7],[6,7,8],[1,4,6],[2,5,8]],"universe_size":9},"samples":[{"config":[1,0,1,0,1,0,0],"metric":true}],"optimal":[{"config":[1,0,1,0,1,0,0],"metric":true}]}, {"problem":"Factoring","variant":{},"instance":{"m":2,"n":3,"target":15},"samples":[{"config":[1,1,1,0,1],"metric":{"Valid":0}}],"optimal":[{"config":[1,1,1,0,1],"metric":{"Valid":0}}]}, {"problem":"HamiltonianPath","variant":{"graph":"SimpleGraph"},"instance":{"graph":{"inner":{"edge_property":"undirected","edges":[[0,1,null],[0,2,null],[1,3,null],[2,3,null],[3,4,null],[3,5,null],[4,2,null],[5,1,null]],"node_holes":[],"nodes":[null,null,null,null,null,null]}}},"samples":[{"config":[0,2,4,3,1,5],"metric":true}],"optimal":[{"config":[0,1,5,3,2,4],"metric":true},{"config":[0,1,5,3,4,2],"metric":true},{"config":[0,2,4,3,1,5],"metric":true},{"config":[0,2,4,3,5,1],"metric":true},{"config":[1,0,2,4,3,5],"metric":true},{"config":[1,5,3,4,2,0],"metric":true},{"config":[2,0,1,5,3,4],"metric":true},{"config":[2,4,3,5,1,0],"metric":true},{"config":[3,4,2,0,1,5],"metric":true},{"config":[3,5,1,0,2,4],"metric":true},{"config":[4,2,0,1,3,5],"metric":true},{"config":[4,2,0,1,5,3],"metric":true},{"config":[4,2,3,5,1,0],"metric":true},{"config":[4,3,2,0,1,5],"metric":true},{"config":[4,3,5,1,0,2],"metric":true},{"config":[5,1,0,2,3,4],"metric":true},{"config":[5,1,0,2,4,3],"metric":true},{"config":[5,1,3,4,2,0],"metric":true},{"config":[5,3,1,0,2,4],"metric":true},{"config":[5,3,4,2,0,1],"metric":true}]}, diff --git a/src/lib.rs b/src/lib.rs index 64c77b6fc..da741b3cc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -58,7 +58,9 @@ pub mod prelude { BinPacking, Factoring, FlowShopScheduling, Knapsack, LongestCommonSubsequence, MinimumTardinessSequencing, PaintShop, ShortestCommonSupersequence, SubsetSum, }; - pub use crate::models::set::{ExactCoverBy3Sets, MaximumSetPacking, MinimumSetCovering}; + pub use crate::models::set::{ + ConsecutiveSets, ExactCoverBy3Sets, MaximumSetPacking, MinimumSetCovering, + }; // Core traits pub use crate::rules::{ReduceTo, ReductionResult}; diff --git a/src/unit_tests/models/set/consecutive_sets.rs b/src/unit_tests/models/set/consecutive_sets.rs index 82b010203..f1e3a6f1c 100644 --- a/src/unit_tests/models/set/consecutive_sets.rs +++ b/src/unit_tests/models/set/consecutive_sets.rs @@ -54,6 +54,23 @@ fn test_consecutive_sets_solver() { for sol in &solutions { assert!(problem.evaluate(sol)); } + // Known solution: [0, 1, 2] — {0,1} at window 0-1, {1,2} at window 1-2 + assert!(solutions.contains(&vec![0, 1, 2])); +} + +#[test] +fn test_consecutive_sets_rejects_wrong_config_length() { + let problem = ConsecutiveSets::new(3, vec![vec![0, 1]], 3); + assert!(!problem.evaluate(&[0, 1])); // too short + assert!(!problem.evaluate(&[0, 1, 2, 0])); // too long +} + +#[test] +fn test_consecutive_sets_rejects_internal_unused() { + // Internal "unused" symbol should be rejected + let problem = ConsecutiveSets::new(3, vec![vec![0, 1]], 4); + // [0, 3, 1, 3] has "unused" (3) at position 1, which is internal + assert!(!problem.evaluate(&[0, 3, 1, 3])); } #[test] @@ -64,6 +81,7 @@ fn test_consecutive_sets_serialization() { assert_eq!(deserialized.alphabet_size(), problem.alphabet_size()); assert_eq!(deserialized.num_subsets(), problem.num_subsets()); assert_eq!(deserialized.bound_k(), problem.bound_k()); + assert_eq!(deserialized.subsets(), problem.subsets()); } #[test] From e917778f91000d72e9360152511e4bae92e452a3 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Mon, 16 Mar 2026 22:05:59 +0800 Subject: [PATCH 5/9] Add ConsecutiveSets paper entry with bibliography --- docs/paper/reductions.typ | 20 ++++++++++++++++++++ docs/paper/references.bib | 22 ++++++++++++++++++++++ src/models/set/consecutive_sets.rs | 8 +------- 3 files changed, 43 insertions(+), 7 deletions(-) diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index ce08d7605..d85ddb984 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -91,6 +91,7 @@ "BicliqueCover": [Biclique Cover], "BinPacking": [Bin Packing], "ClosestVectorProblem": [Closest Vector Problem], + "ConsecutiveSets": [Consecutive Sets], "OptimalLinearArrangement": [Optimal Linear Arrangement], "RuralPostman": [Rural Postman], "LongestCommonSubsequence": [Longest Common Subsequence], @@ -1084,6 +1085,25 @@ NP-completeness was established by Garey, Johnson, and Stockmeyer @gareyJohnsonS ] } +#{ + let x = load-model-example("ConsecutiveSets") + let m = x.instance.alphabet_size + let n = x.instance.subsets.len() + let K = x.instance.bound_k + let subs = x.instance.subsets + let sol = x.optimal.at(0).config + let fmt-set(s) = "${" + s.map(e => str(e)).join(", ") + "}$" + [ + #problem-def("ConsecutiveSets")[ + Given a finite alphabet $Sigma$ of size $m$, a collection $cal(C) = {Sigma_1, Sigma_2, dots, Sigma_n}$ of subsets of $Sigma$, and a positive integer $K$, determine whether there exists a string $w in Sigma^*$ with $|w| lt.eq K$ such that, for each $i$, the elements of $Sigma_i$ occur in a consecutive block of $|Sigma_i|$ symbols of $w$. + ][ + This problem arises in information retrieval and file organization (SR18 in Garey and Johnson @garey1979). It generalizes the _consecutive ones property_ from binary matrices to a string-based formulation: given subsets of an alphabet, construct the shortest string where each subset's elements appear contiguously. The problem is NP-complete, as shown by #cite(, form: "prose") via reduction from Hamiltonian Path. The circular variant, where blocks may wrap around from the end of $w$ back to its beginning (considering $w w$), is also NP-complete @boothlueker1976. When $K$ equals the number of distinct symbols appearing in the subsets, the problem reduces to testing a binary matrix for the consecutive ones property, which is solvable in linear time using PQ-tree algorithms @boothlueker1976. + + *Example.* Let $Sigma = {0, 1, dots, #(m - 1)}$, $K = #K$, and $cal(C) = {#range(n).map(i => $Sigma_#(i + 1)$).join(", ")}$ with #range(n).map(i => $Sigma_#(i + 1) = #fmt-set(subs.at(i))$).join(", "). A valid string is $w = (#sol.map(e => str(e)).join(", "))$ with $|w| = #sol.len() = K$: $Sigma_1 = {0, 4}$ appears as the block $(0, 4)$ at positions 0--1, $Sigma_2 = {2, 4}$ appears as $(4, 2)$ at positions 1--2, $Sigma_3 = {2, 5}$ appears as $(2, 5)$ at positions 2--3, $Sigma_4 = {1, 5}$ appears as $(5, 1)$ at positions 3--4, and $Sigma_5 = {1, 3}$ appears as $(1, 3)$ at positions 4--5. + ] + ] +} + #{ let x3c = load-model-example("ExactCoverBy3Sets") let n = x3c.instance.universe_size diff --git a/docs/paper/references.bib b/docs/paper/references.bib index 7186ea641..8bfbe01a2 100644 --- a/docs/paper/references.bib +++ b/docs/paper/references.bib @@ -672,3 +672,25 @@ @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} +} + +@article{boothlueker1976, + author = {Kellogg S. Booth and George S. Lueker}, + title = {Testing for the Consecutive Ones Property, Interval Graphs, and Graph Planarity Using {PQ}-Tree Algorithms}, + journal = {Journal of Computer and System Sciences}, + volume = {13}, + number = {3}, + pages = {335--379}, + year = {1976}, + doi = {10.1016/S0022-0000(76)80045-1} +} diff --git a/src/models/set/consecutive_sets.rs b/src/models/set/consecutive_sets.rs index be041ca8d..ca19e2a98 100644 --- a/src/models/set/consecutive_sets.rs +++ b/src/models/set/consecutive_sets.rs @@ -199,13 +199,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec Date: Mon, 16 Mar 2026 22:06:19 +0800 Subject: [PATCH 6/9] chore: remove plan file after implementation --- docs/plans/2026-03-16-consecutive-sets.md | 91 ----------------------- 1 file changed, 91 deletions(-) delete mode 100644 docs/plans/2026-03-16-consecutive-sets.md diff --git a/docs/plans/2026-03-16-consecutive-sets.md b/docs/plans/2026-03-16-consecutive-sets.md deleted file mode 100644 index 2e94afa88..000000000 --- a/docs/plans/2026-03-16-consecutive-sets.md +++ /dev/null @@ -1,91 +0,0 @@ -# Plan: Add ConsecutiveSets Model (#421) - -## Context - -**ConsecutiveSets** (Garey & Johnson A4 SR18): Given finite alphabet Σ (size `alphabet_size`), collection C = {Σ₁, ..., Σₙ} of subsets of Σ, and positive integer K (`bound_k`), decide if there exists a string w ∈ Σ* with |w| ≤ K such that for each i, the elements of Σᵢ occur in a consecutive block of |Σᵢ| symbols of w. - -- **Type:** Satisfaction problem (Metric = bool) -- **NP-completeness:** Kou 1977, reduction from Hamiltonian Path -- **Category:** `set/` (universe + subsets input structure) -- **Complexity:** `"alphabet_size^bound_k * num_subsets"` (brute-force) - -## Batch 1: Implementation (Steps 1–5.5) - -### Task 1: Create model file `src/models/set/consecutive_sets.rs` - -**Struct:** -```rust -pub struct ConsecutiveSets { - alphabet_size: usize, - subsets: Vec>, - bound_k: usize, -} -``` - -**Constructor** `new(alphabet_size, subsets, bound_k)`: -- Validate: all subset elements < alphabet_size -- Validate: bound_k > 0 -- Validate: no duplicate elements within individual subsets -- Sort each subset for canonical form - -**Getters:** `alphabet_size()`, `num_subsets()`, `bound_k()`, `subsets()` - -**Problem impl:** -- `NAME = "ConsecutiveSets"` -- `type Metric = bool` -- `dims()`: `vec![alphabet_size + 1; bound_k]` — values 0..alphabet_size-1 are symbols, alphabet_size = "unused" -- `evaluate(config)`: - 1. Validate config length == bound_k and all values <= alphabet_size - 2. Build string w: take symbols until trailing "unused" values; reject if any internal "unused" - 3. For each subset Σᵢ: scan all windows of length |Σᵢ| in w, check if window contains exactly the elements of Σᵢ (as a set). If no valid window found for any subset, return false. - 4. Return true if all subsets satisfied. -- `variant()`: `variant_params![]` - -**SatisfactionProblem** marker trait impl. - -**declare_variants!:** -```rust -default sat ConsecutiveSets => "alphabet_size^bound_k * num_subsets", -``` - -**ProblemSchemaEntry** with fields: `alphabet_size` (usize), `subsets` (Vec>), `bound_k` (usize). - -**Canonical example spec** using YES instance from issue: alphabet_size=6, subsets=[{0,4},{2,4},{2,5},{1,5},{1,3}], bound_k=6, solution=[0,4,2,5,1,3]. - -### Task 2: Register model - -- `src/models/set/mod.rs`: Add `pub(crate) mod consecutive_sets;`, `pub use`, and extend `canonical_model_example_specs()` -- `src/models/mod.rs`: Add `ConsecutiveSets` to `pub use set::` re-export - -### Task 3: Write unit tests `src/unit_tests/models/set/consecutive_sets.rs` - -Tests (reference: `exact_cover_by_3_sets.rs`): -- `test_consecutive_sets_creation` — dims, getters, num_variables -- `test_consecutive_sets_evaluation` — YES config [0,4,2,5,1,3] and NO configs -- `test_consecutive_sets_no_instance` — NO instance from issue (conflicting block constraints) -- `test_consecutive_sets_serialization` — serde round-trip -- `test_consecutive_sets_solver` — BruteForce finds satisfying configs, verify known solution present -- `test_consecutive_sets_empty` — edge case: empty subsets collection -- `test_consecutive_sets_invalid_constructor` — #[should_panic] for bad inputs - -Link via `#[path]` in model file. - -### Task 4: Verify build - -Run `make check` (fmt + clippy + test). Fix any issues. - -### Task 5: Run trait_consistency - -Ensure the new model passes all trait consistency checks automatically via `declare_variants!`. - -## Batch 2: Paper Entry (Step 6) - -### Task 6: Write paper entry in `docs/paper/reductions.typ` - -- Add `"ConsecutiveSets": [Consecutive Sets]` to `display-name` dict -- Add `#problem-def("ConsecutiveSets")[definition][body]` with: - - Formal definition from Garey & Johnson - - NP-completeness reference (Kou 1977) - - Connection to consecutive ones property - - Circular variant mention (Booth 1975) -- Run `make paper` to verify compilation From 48010be28a238a6245c7646385700ab7e0fe0d80 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Tue, 17 Mar 2026 01:08:09 +0800 Subject: [PATCH 7/9] fix: avoid per-window sorting in ConsecutiveSets --- src/models/set/consecutive_sets.rs | 31 ++++++++++++++++--- src/unit_tests/models/set/consecutive_sets.rs | 6 ++++ 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/src/models/set/consecutive_sets.rs b/src/models/set/consecutive_sets.rs index ca19e2a98..9bec110ee 100644 --- a/src/models/set/consecutive_sets.rs +++ b/src/models/set/consecutive_sets.rs @@ -150,6 +150,11 @@ impl Problem for ConsecutiveSets { return false; } + let mut subset_membership = vec![0usize; self.alphabet_size]; + let mut seen_in_window = vec![0usize; self.alphabet_size]; + let mut subset_stamp = 1usize; + let mut window_stamp = 1usize; + // 4. Check each subset has a consecutive block for subset in &self.subsets { let subset_len = subset.len(); @@ -160,13 +165,29 @@ impl Problem for ConsecutiveSets { return false; // can't fit } + for &elem in subset { + subset_membership[elem] = subset_stamp; + } + let mut found = false; for start in 0..=(str_len - subset_len) { let window = &w[start..start + subset_len]; - // Check if window is a permutation of subset - let mut window_sorted: Vec = window.to_vec(); - window_sorted.sort(); - if window_sorted == *subset { + let current_window_stamp = window_stamp; + window_stamp += 1; + + // Because subsets are validated to contain unique elements, + // a window matches iff every symbol belongs to the subset and + // appears at most once. + if window.iter().all(|&elem| { + let is_member = subset_membership[elem] == subset_stamp; + let is_new = seen_in_window[elem] != current_window_stamp; + if is_member && is_new { + seen_in_window[elem] = current_window_stamp; + true + } else { + false + } + }) { // subset is already sorted found = true; break; @@ -175,6 +196,8 @@ impl Problem for ConsecutiveSets { if !found { return false; } + + subset_stamp += 1; } true diff --git a/src/unit_tests/models/set/consecutive_sets.rs b/src/unit_tests/models/set/consecutive_sets.rs index f1e3a6f1c..f0eb76a52 100644 --- a/src/unit_tests/models/set/consecutive_sets.rs +++ b/src/unit_tests/models/set/consecutive_sets.rs @@ -73,6 +73,12 @@ fn test_consecutive_sets_rejects_internal_unused() { assert!(!problem.evaluate(&[0, 3, 1, 3])); } +#[test] +fn test_consecutive_sets_rejects_duplicate_window_symbol() { + let problem = ConsecutiveSets::new(2, vec![vec![0, 1]], 2); + assert!(!problem.evaluate(&[0, 0])); +} + #[test] fn test_consecutive_sets_serialization() { let problem = ConsecutiveSets::new(6, vec![vec![0, 4], vec![2, 4]], 6); From 673e83fddec4df9ebc690476ca2ba4136e1ee0e3 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Tue, 17 Mar 2026 01:18:19 +0800 Subject: [PATCH 8/9] test: cover shorter ConsecutiveSets strings --- src/unit_tests/models/set/consecutive_sets.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/unit_tests/models/set/consecutive_sets.rs b/src/unit_tests/models/set/consecutive_sets.rs index f0eb76a52..c7b86d0a4 100644 --- a/src/unit_tests/models/set/consecutive_sets.rs +++ b/src/unit_tests/models/set/consecutive_sets.rs @@ -73,6 +73,12 @@ fn test_consecutive_sets_rejects_internal_unused() { assert!(!problem.evaluate(&[0, 3, 1, 3])); } +#[test] +fn test_consecutive_sets_accepts_shorter_string_with_trailing_unused() { + let problem = ConsecutiveSets::new(3, vec![vec![0, 1]], 4); + assert!(problem.evaluate(&[0, 1, 3, 3])); +} + #[test] fn test_consecutive_sets_rejects_duplicate_window_symbol() { let problem = ConsecutiveSets::new(2, vec![vec![0, 1]], 2); From 206eedf62bd9a5afe7ed8f889030fc3e1362a2d9 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Tue, 17 Mar 2026 01:23:10 +0800 Subject: [PATCH 9/9] docs: clarify ConsecutiveSets config encoding --- src/models/set/consecutive_sets.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/models/set/consecutive_sets.rs b/src/models/set/consecutive_sets.rs index 9bec110ee..8459b3454 100644 --- a/src/models/set/consecutive_sets.rs +++ b/src/models/set/consecutive_sets.rs @@ -33,6 +33,11 @@ inventory::submit! { /// such that the elements of each subset appear as a contiguous block (in any /// order) within w. /// +/// Configurations use `bound_k` positions. Values `0..alphabet_size-1` +/// represent alphabet symbols, and the extra value `alphabet_size` marks +/// unused positions beyond the end of a shorter string. Only trailing unused +/// positions are valid. +/// /// This problem is NP-complete and arises in physical mapping of DNA and in /// consecutive arrangements of hypergraph vertices. /// @@ -55,6 +60,12 @@ inventory::submit! { /// // w = [0, 4, 2, 5, 1, 3] is a valid solution /// assert!(solution.is_some()); /// assert!(problem.evaluate(&solution.unwrap())); +/// +/// // Shorter strings are encoded with trailing `unused = alphabet_size`. +/// let shorter = ConsecutiveSets::new(3, vec![vec![0, 1]], 4); +/// let unused = shorter.alphabet_size(); +/// assert!(shorter.evaluate(&[0, 1, unused, unused])); +/// assert!(!shorter.evaluate(&[0, unused, 1, unused])); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ConsecutiveSets {