diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index 57127ed8e..80e4e0f43 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -136,10 +136,11 @@ "SchedulingWithIndividualDeadlines": [Scheduling With Individual Deadlines], "StaffScheduling": [Staff Scheduling], "MinimumTardinessSequencing": [Minimum Tardiness Sequencing], - "SequencingToMinimizeWeightedTardiness": [Sequencing to Minimize Weighted Tardiness], - "SequencingToMinimizeMaximumCumulativeCost": [Sequencing to Minimize Maximum Cumulative Cost], "ConsecutiveOnesSubmatrix": [Consecutive Ones Submatrix], + "SequencingToMinimizeMaximumCumulativeCost": [Sequencing to Minimize Maximum Cumulative Cost], + "SequencingToMinimizeWeightedTardiness": [Sequencing to Minimize Weighted Tardiness], "SumOfSquaresPartition": [Sum of Squares Partition], + "TwoDimensionalConsecutiveSets": [2-Dimensional Consecutive Sets], "SequencingWithinIntervals": [Sequencing Within Intervals], "DirectedTwoCommodityIntegralFlow": [Directed Two-Commodity Integral Flow], "ConjunctiveBooleanQuery": [Conjunctive Boolean Query], @@ -1906,6 +1907,52 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76], ] } +#{ + let x = load-model-example("TwoDimensionalConsecutiveSets") + let n = x.instance.alphabet_size + let subs = x.instance.subsets + let m = subs.len() + 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)) + // 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). Multiple valid partitions exist for this instance, differing only by unused or shifted group labels. + + #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 03f283277..103aa5614 100644 --- a/docs/paper/references.bib +++ b/docs/paper/references.bib @@ -944,20 +944,25 @@ @article{papadimitriou1982 doi = {10.1145/322307.322309} } -@book{papadimitriou-steiglitz1982, - author = {Christos H. Papadimitriou and Kenneth Steiglitz}, - title = {Combinatorial Optimization: Algorithms and Complexity}, - publisher = {Prentice-Hall}, - address = {Englewood Cliffs, NJ}, - year = {1982} +@article{booth1976, + author = {Booth, Kellogg S. and Lueker, George S.}, + 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} } -@techreport{Heidari2022, - author = {Heidari, Shahrokh and Dinneen, Michael J. and Delmas, Patrice}, - title = {An Equivalent {QUBO} Model to the Minimum Multi-Way Cut Problem}, - institution = {Centre for Discrete Mathematics and Theoretical Computer Science, University of Auckland}, - year = {2022}, - number = {CDMTCS-565} +@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} } @phdthesis{booth1975, @@ -967,25 +972,25 @@ @phdthesis{booth1975 year = {1975} } -@article{booth1976, - author = {Booth, Kellogg S. and Lueker, George S.}, - 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} +@article{chopra1996, + author = {Sunil Chopra and Jonathan H. Owen}, + title = {Extended formulations for the A-cut problem}, + journal = {Mathematical Programming}, + volume = {73}, + pages = {7--30}, + year = {1996}, + doi = {10.1007/BF02592096} } -@article{lawler1972, - author = {Eugene L. Lawler}, - title = {A Procedure for Computing the $K$ Best Solutions to Discrete Optimization Problems and Its Application to the Shortest Path Problem}, - journal = {Management Science}, - volume = {18}, - number = {7}, - pages = {401--405}, +@article{coffman1972, + author = {Edward G. Coffman and Ronald L. Graham}, + title = {Optimal Scheduling for Two-Processor Systems}, + journal = {Acta Informatica}, + volume = {1}, + number = {3}, + pages = {200--213}, year = {1972}, - doi = {10.1287/mnsc.18.7.401} + doi = {10.1007/BF00288685} } @article{eppstein1992, @@ -999,14 +1004,23 @@ @article{eppstein1992 doi = {10.1007/BF01994880} } -@article{chopra1996, - author = {Sunil Chopra and Jonathan H. Owen}, - title = {Extended formulations for the A-cut problem}, - journal = {Mathematical Programming}, - volume = {73}, - pages = {7--30}, - year = {1996}, - doi = {10.1007/BF02592096} +@techreport{Heidari2022, + author = {Heidari, Shahrokh and Dinneen, Michael J. and Delmas, Patrice}, + title = {An Equivalent {QUBO} Model to the Minimum Multi-Way Cut Problem}, + institution = {Centre for Discrete Mathematics and Theoretical Computer Science, University of Auckland}, + year = {2022}, + number = {CDMTCS-565} +} + +@article{hu1961, + author = {Te Chiang Hu}, + title = {Parallel Sequencing and Assembly Line Problems}, + journal = {Operations Research}, + volume = {9}, + number = {6}, + pages = {841--848}, + year = {1961}, + doi = {10.1287/opre.9.6.841} } @article{kou1977, @@ -1020,26 +1034,15 @@ @article{kou1977 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} -} - -@article{ullman1975, - author = {Jeffrey D. Ullman}, - title = {NP-Complete Scheduling Problems}, - journal = {Journal of Computer and System Sciences}, - volume = {10}, - number = {3}, - pages = {384--393}, - year = {1975}, - doi = {10.1016/S0022-0000(75)80008-0} +@article{lawler1972, + author = {Eugene L. Lawler}, + title = {A Procedure for Computing the $K$ Best Solutions to Discrete Optimization Problems and Its Application to the Shortest Path Problem}, + journal = {Management Science}, + volume = {18}, + number = {7}, + pages = {401--405}, + year = {1972}, + doi = {10.1287/mnsc.18.7.401} } @article{lenstra1978, @@ -1053,26 +1056,24 @@ @article{lenstra1978 doi = {10.1287/opre.26.1.22} } -@article{coffman1972, - author = {Edward G. Coffman and Ronald L. Graham}, - title = {Optimal Scheduling for Two-Processor Systems}, - journal = {Acta Informatica}, - volume = {1}, - number = {3}, - pages = {200--213}, - year = {1972}, - doi = {10.1007/BF00288685} +@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} } -@article{hu1961, - author = {Te Chiang Hu}, - title = {Parallel Sequencing and Assembly Line Problems}, - journal = {Operations Research}, - volume = {9}, - number = {6}, - pages = {841--848}, - year = {1961}, - doi = {10.1287/opre.9.6.841} +@book{papadimitriou-steiglitz1982, + author = {Christos H. Papadimitriou and Kenneth Steiglitz}, + title = {Combinatorial Optimization: Algorithms and Complexity}, + publisher = {Prentice-Hall}, + address = {Englewood Cliffs, NJ}, + year = {1982} } @inproceedings{papadimitriou1979, @@ -1083,3 +1084,14 @@ @inproceedings{papadimitriou1979 year = {1979}, doi = {10.1145/800135.804393} } + +@article{ullman1975, + author = {Jeffrey D. Ullman}, + title = {NP-Complete Scheduling Problems}, + journal = {Journal of Computer and System Sciences}, + volume = {10}, + number = {3}, + pages = {384--393}, + year = {1975}, + doi = {10.1016/S0022-0000(75)80008-0} +} diff --git a/docs/src/cli.md b/docs/src/cli.md index 1b8588b94..c07a4e756 100644 --- a/docs/src/cli.md +++ b/docs/src/cli.md @@ -502,6 +502,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 2c9eac07b..aeece57f1 100644 --- a/problemreductions-cli/src/cli.rs +++ b/problemreductions-cli/src/cli.rs @@ -243,8 +243,9 @@ Flags by problem type: ComparativeContainment --universe, --r-sets, --s-sets [--r-weights] [--s-weights] X3C (ExactCoverBy3Sets) --universe, --sets (3 elements each) SetBasis --universe, --sets, --k - PrimeAttributeName --universe, --deps, --query MinimumCardinalityKey --num-attributes, --dependencies, --k + PrimeAttributeName --universe, --deps, --query + TwoDimensionalConsecutiveSets --alphabet-size, --sets BicliqueCover --left, --right, --biedges, --k BalancedCompleteBipartiteSubgraph --left, --right, --biedges, --k BiconnectivityAugmentation --graph, --potential-edges, --budget [--num-vertices] @@ -307,8 +308,9 @@ 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 MinimumCardinalityKey --num-attributes 6 --dependencies \"0,1>2;0,2>3;1,3>4;2,4>5\" --k 2 pred create PrimeAttributeName --universe 6 --deps \"0,1>2,3,4,5;2,3>0,1,4,5\" --query 3 - pred create MinimumCardinalityKey --num-attributes 6 --dependencies \"0,1>2;0,2>3;1,3>4;2,4>5\" --k 2")] + 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)] @@ -547,7 +549,7 @@ pub struct CreateArgs { /// Number of available workers for StaffScheduling #[arg(long)] pub num_workers: Option, - /// Alphabet size for LCS, SCS, or StringToStringCorrection (optional; inferred from the input strings if omitted) + /// Alphabet size for LCS, SCS, StringToStringCorrection, or TwoDimensionalConsecutiveSets (optional; inferred from the input strings if omitted) #[arg(long)] pub alphabet_size: Option, /// Number of attributes for AdditionalKey or MinimumCardinalityKey @@ -597,6 +599,7 @@ Examples: pred solve reduced.json -o solution.json # save result to file pred create MIS --graph 0-1,1-2 | pred solve - # read from stdin when an ILP path exists pred create StringToStringCorrection --source-string \"0,1,2,3,1,0\" --target-string \"0,1,3,2,1\" --bound 2 | 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 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 9bfe04bbc..35a8d1659 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -445,15 +445,18 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str { "--universe 4 --r-sets \"0,1,2,3;0,1\" --s-sets \"0,1,2,3;2,3\" --r-weights 2,5 --s-weights 3,6" } "SetBasis" => "--universe 4 --sets \"0,1;1,2;0,2;0,1,2\" --k 3", - "PrimeAttributeName" => { - "--universe 6 --deps \"0,1>2,3,4,5;2,3>0,1,4,5\" --query 3" - } "LongestCommonSubsequence" => { "--strings \"010110;100101;001011\" --bound 3 --alphabet-size 2" } "MinimumCardinalityKey" => { "--num-attributes 6 --dependencies \"0,1>2;0,2>3;1,3>4;2,4>5\" --k 2" } + "PrimeAttributeName" => { + "--universe 6 --deps \"0,1>2,3,4,5;2,3>0,1,4,5\" --query 3" + } + "TwoDimensionalConsecutiveSets" => { + "--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", "SequencingToMinimizeMaximumCumulativeCost" => { "--costs 2,-1,3,-2,1,-3 --precedence-pairs \"0>2,1>2,1>3,2>4,3>5,4>5\" --bound 4" @@ -1794,6 +1797,27 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { ) } + // TwoDimensionalConsecutiveSets + "TwoDimensionalConsecutiveSets" => { + let alphabet_size = args.alphabet_size.or(args.universe).ok_or_else(|| { + anyhow::anyhow!( + "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\"" + ) + })?; + let sets = parse_sets(args)?; + ( + ser( + problemreductions::models::set::TwoDimensionalConsecutiveSets::try_new( + alphabet_size, + sets, + ) + .map_err(anyhow::Error::msg)?, + )?, + resolved_variant.clone(), + ) + } + // BicliqueCover "BicliqueCover" => { let usage = "pred create BicliqueCover --left 2 --right 2 --biedges 0-0,0-1,1-1 --k 2"; diff --git a/problemreductions-cli/tests/cli_tests.rs b/problemreductions-cli/tests/cli_tests.rs index dcaddf9c8..ff72ffb52 100644 --- a/problemreductions-cli/tests/cli_tests.rs +++ b/problemreductions-cli/tests/cli_tests.rs @@ -1558,6 +1558,79 @@ fn test_create_minimum_cardinality_key_missing_num_attributes_message() { assert!(!stderr.contains("--num-vertices"), "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_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/models/mod.rs b/src/models/mod.rs index 7ab5dc5a2..ac954567d 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -39,4 +39,5 @@ pub use misc::{ pub use set::{ ComparativeContainment, ConsecutiveSets, ExactCoverBy3Sets, MaximumSetPacking, MinimumCardinalityKey, MinimumSetCovering, PrimeAttributeName, SetBasis, + TwoDimensionalConsecutiveSets, }; diff --git a/src/models/set/mod.rs b/src/models/set/mod.rs index 8fd225b23..b0a101a42 100644 --- a/src/models/set/mod.rs +++ b/src/models/set/mod.rs @@ -16,6 +16,7 @@ pub(crate) mod minimum_cardinality_key; pub(crate) mod minimum_set_covering; pub(crate) mod prime_attribute_name; pub(crate) mod set_basis; +pub(crate) mod two_dimensional_consecutive_sets; pub use comparative_containment::ComparativeContainment; pub use consecutive_sets::ConsecutiveSets; @@ -25,6 +26,7 @@ pub use minimum_cardinality_key::MinimumCardinalityKey; pub use minimum_set_covering::MinimumSetCovering; pub use prime_attribute_name::PrimeAttributeName; pub use set_basis::SetBasis; +pub use two_dimensional_consecutive_sets::TwoDimensionalConsecutiveSets; #[cfg(feature = "example-db")] pub(crate) fn canonical_model_example_specs() -> Vec { @@ -37,5 +39,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)] +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>, +} + +#[derive(Debug, Deserialize)] +struct TwoDimensionalConsecutiveSetsUnchecked { + alphabet_size: usize, + subsets: Vec>, +} + +fn validate(alphabet_size: usize, subsets: &[Vec]) -> Result<(), String> { + if alphabet_size == 0 { + return Err("Alphabet size must be positive".to_string()); + } + + for (i, subset) in subsets.iter().enumerate() { + let mut seen = HashSet::new(); + for &elem in subset { + if elem >= alphabet_size { + return Err(format!( + "Subset {} contains element {} which is outside alphabet of size {}", + i, elem, alphabet_size + )); + } + if !seen.insert(elem) { + return Err(format!("Subset {} contains duplicate element {}", i, elem)); + } + } + } + + Ok(()) +} + +impl<'de> Deserialize<'de> for TwoDimensionalConsecutiveSets { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + 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 { + validate(alphabet_size, &subsets)?; + let subsets = subsets.into_iter().map(|mut s| { s.sort(); s }).collect(); + Ok(Self { + alphabet_size, + subsets, + }) + } + + /// 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 { + Self::try_new(alphabet_size, subsets).unwrap_or_else(|message| panic!("{message}")) + } + + /// 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; + } + + // 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; + } + 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(); + 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", + 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), + }] +} + +#[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..ffbb7208e --- /dev/null +++ b/src/unit_tests/models/set/two_dimensional_consecutive_sets.rs @@ -0,0 +1,167 @@ +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_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: + // 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_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 + 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]]); +}