diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index 80a0a4d00..877d08217 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -99,6 +99,7 @@ "BoundedComponentSpanningForest": [Bounded Component Spanning Forest], "BinPacking": [Bin Packing], "ClosestVectorProblem": [Closest Vector Problem], + "ConsecutiveSets": [Consecutive Sets], "MinimumMultiwayCut": [Minimum Multiway Cut], "OptimalLinearArrangement": [Optimal Linear Arrangement], "RuralPostman": [Rural Postman], @@ -1395,6 +1396,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 0585a73df..870a7b8f9 100644 --- a/docs/paper/references.bib +++ b/docs/paper/references.bib @@ -797,3 +797,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/example_db/fixtures/examples.json b/src/example_db/fixtures/examples.json index bc223618e..263c1e68d 100644 --- a/src/example_db/fixtures/examples.json +++ b/src/example_db/fixtures/examples.json @@ -8,6 +8,7 @@ {"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":"ComparativeContainment","variant":{"weight":"i32"},"instance":{"r_sets":[[0,1,2,3],[0,1]],"r_weights":[2,5],"s_sets":[[0,1,2,3],[2,3]],"s_weights":[3,6],"universe_size":4},"samples":[{"config":[1,0,0,0],"metric":true}],"optimal":[{"config":[0,1,0,0],"metric":true},{"config":[1,0,0,0],"metric":true},{"config":[1,1,0,0],"metric":true}]}, + {"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":"DirectedTwoCommodityIntegralFlow","variant":{},"instance":{"capacities":[1,1,1,1,1,1,1,1],"graph":{"inner":{"edge_property":"directed","edges":[[0,2,null],[0,3,null],[1,2,null],[1,3,null],[2,4,null],[2,5,null],[3,4,null],[3,5,null]],"node_holes":[],"nodes":[null,null,null,null,null,null]}},"requirement_1":1,"requirement_2":1,"sink_1":4,"sink_2":5,"source_1":0,"source_2":1},"samples":[{"config":[1,0,0,0,1,0,0,0,0,0,0,1,0,0,0,1],"metric":true}],"optimal":[{"config":[0,0,0,1,0,0,1,0,0,0,1,0,0,1,0,0],"metric":true},{"config":[0,0,0,1,0,0,1,0,0,1,0,0,0,0,0,1],"metric":true},{"config":[0,0,0,1,0,0,1,0,0,1,1,0,0,1,0,1],"metric":true},{"config":[0,0,0,1,0,0,1,0,0,1,1,0,1,0,0,1],"metric":true},{"config":[0,0,0,1,0,0,1,0,1,0,0,0,0,1,0,0],"metric":true},{"config":[0,0,0,1,0,0,1,0,1,0,1,0,1,1,0,0],"metric":true},{"config":[0,0,0,1,0,0,1,0,1,1,0,0,0,1,0,1],"metric":true},{"config":[0,0,0,1,0,0,1,0,1,1,0,0,1,0,0,1],"metric":true},{"config":[0,0,0,1,0,0,1,0,1,1,1,0,1,1,0,1],"metric":true},{"config":[0,0,1,0,1,0,0,0,0,0,0,1,0,0,0,1],"metric":true},{"config":[0,0,1,0,1,0,0,0,0,1,0,0,0,0,0,1],"metric":true},{"config":[0,0,1,0,1,0,0,0,0,1,0,1,0,0,1,1],"metric":true},{"config":[0,0,1,0,1,0,0,0,1,0,0,0,0,1,0,0],"metric":true},{"config":[0,0,1,0,1,0,0,0,1,0,0,1,0,1,0,1],"metric":true},{"config":[0,0,1,0,1,0,0,0,1,0,0,1,0,1,1,0],"metric":true},{"config":[0,0,1,0,1,0,0,0,1,1,0,0,0,1,0,1],"metric":true},{"config":[0,0,1,0,1,0,0,0,1,1,0,0,0,1,1,0],"metric":true},{"config":[0,0,1,0,1,0,0,0,1,1,0,1,0,1,1,1],"metric":true},{"config":[0,0,1,1,0,1,1,0,0,1,0,0,0,0,0,1],"metric":true},{"config":[0,0,1,1,0,1,1,0,1,1,0,0,1,0,0,1],"metric":true},{"config":[0,0,1,1,1,0,0,1,1,0,0,0,0,1,0,0],"metric":true},{"config":[0,0,1,1,1,0,0,1,1,1,0,0,0,1,1,0],"metric":true},{"config":[0,0,1,1,1,0,1,0,0,1,0,0,0,0,0,1],"metric":true},{"config":[0,0,1,1,1,0,1,0,1,0,0,0,0,1,0,0],"metric":true},{"config":[0,0,1,1,1,0,1,0,1,1,0,0,0,1,0,1],"metric":true},{"config":[0,1,0,0,0,0,1,0,0,0,0,1,0,0,0,1],"metric":true},{"config":[0,1,0,0,0,0,1,0,0,0,1,0,0,1,0,0],"metric":true},{"config":[0,1,0,0,0,0,1,0,0,0,1,1,0,1,0,1],"metric":true},{"config":[0,1,0,0,0,0,1,0,0,0,1,1,1,0,0,1],"metric":true},{"config":[0,1,0,0,0,0,1,0,1,0,0,0,0,1,0,0],"metric":true},{"config":[0,1,0,0,0,0,1,0,1,0,0,1,0,1,0,1],"metric":true},{"config":[0,1,0,0,0,0,1,0,1,0,0,1,1,0,0,1],"metric":true},{"config":[0,1,0,0,0,0,1,0,1,0,1,0,1,1,0,0],"metric":true},{"config":[0,1,0,0,0,0,1,0,1,0,1,1,1,1,0,1],"metric":true},{"config":[0,1,0,1,0,0,1,1,0,0,1,0,0,1,0,0],"metric":true},{"config":[0,1,0,1,0,0,1,1,1,0,0,0,0,1,0,0],"metric":true},{"config":[0,1,0,1,0,0,1,1,1,0,1,0,1,1,0,0],"metric":true},{"config":[0,1,1,0,0,1,1,0,0,0,0,1,0,0,0,1],"metric":true},{"config":[0,1,1,0,0,1,1,0,1,0,0,1,1,0,0,1],"metric":true},{"config":[0,1,1,0,1,0,0,1,1,0,0,0,0,1,0,0],"metric":true},{"config":[0,1,1,0,1,0,0,1,1,0,0,1,0,1,1,0],"metric":true},{"config":[0,1,1,0,1,0,1,0,0,0,0,1,0,0,0,1],"metric":true},{"config":[0,1,1,0,1,0,1,0,1,0,0,0,0,1,0,0],"metric":true},{"config":[0,1,1,0,1,0,1,0,1,0,0,1,0,1,0,1],"metric":true},{"config":[0,1,1,1,1,0,1,1,1,0,0,0,0,1,0,0],"metric":true},{"config":[1,0,0,0,1,0,0,0,0,0,0,1,0,0,0,1],"metric":true},{"config":[1,0,0,0,1,0,0,0,0,0,1,0,0,1,0,0],"metric":true},{"config":[1,0,0,0,1,0,0,0,0,0,1,1,0,1,0,1],"metric":true},{"config":[1,0,0,0,1,0,0,0,0,0,1,1,0,1,1,0],"metric":true},{"config":[1,0,0,0,1,0,0,0,0,1,0,0,0,0,0,1],"metric":true},{"config":[1,0,0,0,1,0,0,0,0,1,0,1,0,0,1,1],"metric":true},{"config":[1,0,0,0,1,0,0,0,0,1,1,0,0,1,0,1],"metric":true},{"config":[1,0,0,0,1,0,0,0,0,1,1,0,0,1,1,0],"metric":true},{"config":[1,0,0,0,1,0,0,0,0,1,1,1,0,1,1,1],"metric":true},{"config":[1,0,0,1,0,1,1,0,0,1,0,0,0,0,0,1],"metric":true},{"config":[1,0,0,1,0,1,1,0,0,1,1,0,1,0,0,1],"metric":true},{"config":[1,0,0,1,1,0,0,1,0,0,1,0,0,1,0,0],"metric":true},{"config":[1,0,0,1,1,0,0,1,0,1,1,0,0,1,1,0],"metric":true},{"config":[1,0,0,1,1,0,1,0,0,0,1,0,0,1,0,0],"metric":true},{"config":[1,0,0,1,1,0,1,0,0,1,0,0,0,0,0,1],"metric":true},{"config":[1,0,0,1,1,0,1,0,0,1,1,0,0,1,0,1],"metric":true},{"config":[1,0,1,0,1,1,0,0,0,0,0,1,0,0,0,1],"metric":true},{"config":[1,0,1,0,1,1,0,0,0,1,0,0,0,0,0,1],"metric":true},{"config":[1,0,1,0,1,1,0,0,0,1,0,1,0,0,1,1],"metric":true},{"config":[1,0,1,1,1,1,1,0,0,1,0,0,0,0,0,1],"metric":true},{"config":[1,1,0,0,0,1,1,0,0,0,0,1,0,0,0,1],"metric":true},{"config":[1,1,0,0,0,1,1,0,0,0,1,1,1,0,0,1],"metric":true},{"config":[1,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0],"metric":true},{"config":[1,1,0,0,1,0,0,1,0,0,1,1,0,1,1,0],"metric":true},{"config":[1,1,0,0,1,0,1,0,0,0,0,1,0,0,0,1],"metric":true},{"config":[1,1,0,0,1,0,1,0,0,0,1,0,0,1,0,0],"metric":true},{"config":[1,1,0,0,1,0,1,0,0,0,1,1,0,1,0,1],"metric":true},{"config":[1,1,0,1,1,0,1,1,0,0,1,0,0,1,0,0],"metric":true},{"config":[1,1,1,0,1,1,1,0,0,0,0,1,0,0,0,1],"metric":true}]}, {"problem":"ExactCoverBy3Sets","variant":{},"instance":{"subsets":[[0,1,2],[0,2,4],[3,4,5],[3,5,7],[6,7,8],[1,4,6],[2,5,8]],"universe_size":9},"samples":[{"config":[1,0,1,0,1,0,0],"metric":true}],"optimal":[{"config":[1,0,1,0,1,0,0],"metric":true}]}, {"problem":"Factoring","variant":{},"instance":{"m":2,"n":3,"target":15},"samples":[{"config":[1,1,1,0,1],"metric":{"Valid":0}}],"optimal":[{"config":[1,1,1,0,1],"metric":{"Valid":0}}]}, diff --git a/src/lib.rs b/src/lib.rs index 0c0347eb2..3aa8072a1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -63,7 +63,8 @@ pub mod prelude { ShortestCommonSupersequence, StaffScheduling, StringToStringCorrection, SubsetSum, }; pub use crate::models::set::{ - ComparativeContainment, ExactCoverBy3Sets, MaximumSetPacking, MinimumSetCovering, SetBasis, + ComparativeContainment, ConsecutiveSets, ExactCoverBy3Sets, MaximumSetPacking, + MinimumCardinalityKey, MinimumSetCovering, SetBasis, }; // Core traits diff --git a/src/models/mod.rs b/src/models/mod.rs index 2d58e6a27..cd9e00dfe 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -27,6 +27,6 @@ pub use misc::{ ShortestCommonSupersequence, StaffScheduling, StringToStringCorrection, SubsetSum, }; pub use set::{ - ComparativeContainment, ExactCoverBy3Sets, MaximumSetPacking, MinimumCardinalityKey, - MinimumSetCovering, SetBasis, + ComparativeContainment, ConsecutiveSets, ExactCoverBy3Sets, MaximumSetPacking, + MinimumCardinalityKey, MinimumSetCovering, SetBasis, }; diff --git a/src/models/set/consecutive_sets.rs b/src/models/set/consecutive_sets.rs new file mode 100644 index 000000000..8459b3454 --- /dev/null +++ b/src/models/set/consecutive_sets.rs @@ -0,0 +1,246 @@ +//! 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. +/// +/// 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. +/// +/// # 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())); +/// +/// // 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 { + /// 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; + } + + 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(); + if subset_len == 0 { + continue; // empty subset trivially satisfied + } + if subset_len > str_len { + 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]; + 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; + } + } + if !found { + return false; + } + + subset_stamp += 1; + } + + 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 f60870e68..c1583133c 100644 --- a/src/models/set/mod.rs +++ b/src/models/set/mod.rs @@ -1,12 +1,14 @@ //! 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) //! - [`ComparativeContainment`]: Compare containment-weight sums for two set families +//! - [`MaximumSetPacking`]: Maximum weight set packing +//! - [`MinimumSetCovering`]: Minimum weight set cover pub(crate) mod comparative_containment; +pub(crate) mod consecutive_sets; pub(crate) mod exact_cover_by_3_sets; pub(crate) mod maximum_set_packing; pub(crate) mod minimum_cardinality_key; @@ -14,6 +16,7 @@ pub(crate) mod minimum_set_covering; pub(crate) mod set_basis; pub use comparative_containment::ComparativeContainment; +pub use consecutive_sets::ConsecutiveSets; pub use exact_cover_by_3_sets::ExactCoverBy3Sets; pub use maximum_set_packing::MaximumSetPacking; pub use minimum_cardinality_key::MinimumCardinalityKey; @@ -24,6 +27,7 @@ pub use set_basis::SetBasis; pub(crate) fn canonical_model_example_specs() -> Vec { let mut specs = Vec::new(); specs.extend(comparative_containment::canonical_model_example_specs()); + 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..c7b86d0a4 --- /dev/null +++ b/src/unit_tests/models/set/consecutive_sets.rs @@ -0,0 +1,126 @@ +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)); + } + // 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] +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); + assert!(!problem.evaluate(&[0, 0])); +} + +#[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()); + assert_eq!(deserialized.subsets(), problem.subsets()); +} + +#[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); +}