diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index 720b777a1..a49177a39 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -111,6 +111,7 @@ "LongestCommonSubsequence": [Longest Common Subsequence], "ExactCoverBy3Sets": [Exact Cover by 3-Sets], "SubsetSum": [Subset Sum], + "Partition": [Partition], "MinimumFeedbackArcSet": [Minimum Feedback Arc Set], "MinimumFeedbackVertexSet": [Minimum Feedback Vertex Set], "MinimumCutIntoBoundedSets": [Minimum Cut Into Bounded Sets], @@ -2933,6 +2934,14 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76], ] } +#problem-def("Partition")[ + Given a finite set $A = {a_0, dots, a_(n-1)}$ with sizes $s(a_i) in ZZ^+$, determine whether there exists a subset $A' subset.eq A$ such that $sum_(a in A') s(a) = sum_(a in A without A') s(a)$. +][ + One of Karp's 21 NP-complete problems @karp1972, listed as SP12 in Garey & Johnson @garey1979. Partition is the special case of Subset Sum where the target equals half the total sum. Though NP-complete, it is only _weakly_ NP-hard: a dynamic-programming algorithm runs in $O(n dot B_"total")$ pseudo-polynomial time, where $B_"total" = sum_i s(a_i)$. The best known exact algorithm is the $O^*(2^(n slash 2))$ meet-in-the-middle approach of Schroeppel and Shamir (1981). + + *Example.* Let $A = {3, 1, 1, 2, 2, 1}$ ($n = 6$, total sum $= 10$). Setting $A' = {3, 2}$ (indices 0, 3) gives sum $3 + 2 = 5 = 10 slash 2$, and $A without A' = {1, 1, 2, 1}$ also sums to 5. Hence a balanced partition exists. +] + #{ let x = load-model-example("ShortestCommonSupersequence") let alpha-size = x.instance.alphabet_size diff --git a/src/lib.rs b/src/lib.rs index 3e0aa179a..304388298 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -62,7 +62,7 @@ pub mod prelude { pub use crate::models::misc::{ BinPacking, CbqRelation, ConjunctiveBooleanQuery, ConjunctiveQueryFoldability, Factoring, FlowShopScheduling, Knapsack, LongestCommonSubsequence, MinimumTardinessSequencing, - MultiprocessorScheduling, PaintShop, QueryArg, RectilinearPictureCompression, + MultiprocessorScheduling, PaintShop, Partition, QueryArg, RectilinearPictureCompression, ResourceConstrainedScheduling, SequencingToMinimizeMaximumCumulativeCost, SequencingWithReleaseTimesAndDeadlines, SequencingWithinIntervals, ShortestCommonSupersequence, StaffScheduling, StringToStringCorrection, SubsetSum, diff --git a/src/models/misc/mod.rs b/src/models/misc/mod.rs index 679c07e5f..0fdc4c03d 100644 --- a/src/models/misc/mod.rs +++ b/src/models/misc/mod.rs @@ -11,6 +11,7 @@ //! - [`LongestCommonSubsequence`]: Longest Common Subsequence //! - [`MinimumTardinessSequencing`]: Minimize tardy tasks in single-machine scheduling //! - [`PaintShop`]: Minimize color switches in paint shop scheduling +//! - [`Partition`]: Partition a multiset into two equal-sum subsets //! - [`PartiallyOrderedKnapsack`]: Knapsack with precedence constraints //! - [`PrecedenceConstrainedScheduling`]: Schedule unit tasks on processors by deadline //! - [`RectilinearPictureCompression`]: Cover 1-entries with bounded rectangles @@ -34,6 +35,7 @@ mod minimum_tardiness_sequencing; mod multiprocessor_scheduling; pub(crate) mod paintshop; pub(crate) mod partially_ordered_knapsack; +pub(crate) mod partition; mod precedence_constrained_scheduling; mod rectilinear_picture_compression; pub(crate) mod resource_constrained_scheduling; @@ -57,6 +59,7 @@ pub use minimum_tardiness_sequencing::MinimumTardinessSequencing; pub use multiprocessor_scheduling::MultiprocessorScheduling; pub use paintshop::PaintShop; pub use partially_ordered_knapsack::PartiallyOrderedKnapsack; +pub use partition::Partition; pub use precedence_constrained_scheduling::PrecedenceConstrainedScheduling; pub use rectilinear_picture_compression::RectilinearPictureCompression; pub use resource_constrained_scheduling::ResourceConstrainedScheduling; @@ -78,6 +81,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec", description: "Positive integer size for each element" }, + ], + } +} + +/// The Partition problem. +/// +/// Given a finite set `A` with `n` positive integer sizes, determine whether +/// there exists a subset `A' ⊆ A` such that `∑_{a ∈ A'} s(a) = ∑_{a ∈ A\A'} s(a)`. +/// +/// # Representation +/// +/// Each element has a binary variable: `x_i = 1` if element `i` is in the +/// second subset, `0` if in the first. The problem is satisfiable iff +/// `∑_{i: x_i=1} sizes[i] = total_sum / 2`. +/// +/// # Example +/// +/// ``` +/// use problemreductions::models::misc::Partition; +/// use problemreductions::{Problem, Solver, BruteForce}; +/// +/// let problem = Partition::new(vec![3, 1, 1, 2, 2, 1]); +/// let solver = BruteForce::new(); +/// let solution = solver.find_satisfying(&problem); +/// assert!(solution.is_some()); +/// ``` +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Partition { + sizes: Vec, +} + +impl Partition { + /// Create a new Partition instance. + /// + /// # Panics + /// + /// Panics if `sizes` is empty or any size is zero. + pub fn new(sizes: Vec) -> Self { + assert!(!sizes.is_empty(), "Partition requires at least one element"); + assert!( + sizes.iter().all(|&s| s > 0), + "All sizes must be positive (> 0)" + ); + Self { sizes } + } + + /// Returns the element sizes. + pub fn sizes(&self) -> &[u64] { + &self.sizes + } + + /// Returns the number of elements. + pub fn num_elements(&self) -> usize { + self.sizes.len() + } + + /// Returns the total sum of all sizes. + pub fn total_sum(&self) -> u64 { + self.sizes.iter().sum() + } +} + +impl Problem for Partition { + const NAME: &'static str = "Partition"; + type Metric = bool; + + fn variant() -> Vec<(&'static str, &'static str)> { + crate::variant_params![] + } + + fn dims(&self) -> Vec { + vec![2; self.num_elements()] + } + + fn evaluate(&self, config: &[usize]) -> bool { + if config.len() != self.num_elements() { + return false; + } + if config.iter().any(|&v| v >= 2) { + return false; + } + let selected_sum: u64 = config + .iter() + .enumerate() + .filter(|(_, &x)| x == 1) + .map(|(i, _)| self.sizes[i]) + .sum(); + selected_sum * 2 == self.total_sum() + } +} + +impl SatisfactionProblem for Partition {} + +crate::declare_variants! { + default sat Partition => "2^(num_elements / 2)", +} + +#[cfg(feature = "example-db")] +pub(crate) fn canonical_model_example_specs() -> Vec { + vec![crate::example_db::specs::ModelExampleSpec { + id: "partition", + instance: Box::new(Partition::new(vec![3, 1, 1, 2, 2, 1])), + optimal_config: vec![1, 0, 0, 1, 0, 0], + optimal_value: serde_json::json!(true), + }] +} + +#[cfg(test)] +#[path = "../../unit_tests/models/misc/partition.rs"] +mod tests; diff --git a/src/models/mod.rs b/src/models/mod.rs index fd316527d..acc253567 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -29,7 +29,7 @@ pub use misc::PartiallyOrderedKnapsack; pub use misc::{ BinPacking, CbqRelation, ConjunctiveBooleanQuery, ConjunctiveQueryFoldability, Factoring, FlowShopScheduling, Knapsack, LongestCommonSubsequence, MinimumTardinessSequencing, - MultiprocessorScheduling, PaintShop, PrecedenceConstrainedScheduling, QueryArg, + MultiprocessorScheduling, PaintShop, Partition, PrecedenceConstrainedScheduling, QueryArg, RectilinearPictureCompression, ResourceConstrainedScheduling, SequencingToMinimizeMaximumCumulativeCost, SequencingWithReleaseTimesAndDeadlines, SequencingWithinIntervals, ShortestCommonSupersequence, StaffScheduling, diff --git a/src/unit_tests/models/misc/partition.rs b/src/unit_tests/models/misc/partition.rs new file mode 100644 index 000000000..308d27d49 --- /dev/null +++ b/src/unit_tests/models/misc/partition.rs @@ -0,0 +1,108 @@ +use crate::models::misc::Partition; +use crate::solvers::{BruteForce, Solver}; +use crate::traits::Problem; + +#[test] +fn test_partition_basic() { + let problem = Partition::new(vec![3, 1, 1, 2, 2, 1]); + assert_eq!(problem.num_elements(), 6); + assert_eq!(problem.sizes(), &[3, 1, 1, 2, 2, 1]); + assert_eq!(problem.total_sum(), 10); + assert_eq!(problem.dims(), vec![2; 6]); +} + +#[test] +fn test_partition_evaluate_satisfying() { + let problem = Partition::new(vec![3, 1, 1, 2, 2, 1]); + // A' = {3, 2} (indices 0, 3), sum = 5 = 10/2 + assert!(problem.evaluate(&[1, 0, 0, 1, 0, 0])); +} + +#[test] +fn test_partition_evaluate_unsatisfying() { + let problem = Partition::new(vec![3, 1, 1, 2, 2, 1]); + // All in first subset, selected sum = 0 != 5 + assert!(!problem.evaluate(&[0, 0, 0, 0, 0, 0])); + // All in second subset, selected sum = 10 != 5 + assert!(!problem.evaluate(&[1, 1, 1, 1, 1, 1])); +} + +#[test] +fn test_partition_evaluate_wrong_length() { + let problem = Partition::new(vec![3, 1, 1, 2, 2, 1]); + assert!(!problem.evaluate(&[1, 0, 0])); + assert!(!problem.evaluate(&[1, 0, 0, 1, 0, 0, 0])); +} + +#[test] +fn test_partition_evaluate_invalid_value() { + let problem = Partition::new(vec![3, 1, 1, 2, 2, 1]); + assert!(!problem.evaluate(&[2, 0, 0, 0, 0, 0])); +} + +#[test] +fn test_partition_odd_total() { + // Total = 7 (odd), no equal partition possible + let problem = Partition::new(vec![3, 1, 2, 1]); + let solver = BruteForce::new(); + assert!(solver.find_satisfying(&problem).is_none()); +} + +#[test] +fn test_partition_solver() { + let problem = Partition::new(vec![3, 1, 1, 2, 2, 1]); + let solver = BruteForce::new(); + let solution = solver.find_satisfying(&problem); + assert!(solution.is_some()); + assert!(problem.evaluate(&solution.unwrap())); +} + +#[test] +fn test_partition_solver_all() { + let problem = Partition::new(vec![3, 1, 1, 2, 2, 1]); + let solver = BruteForce::new(); + let solutions = solver.find_all_satisfying(&problem); + // 10 satisfying configs for {3,1,1,2,2,1} with target half-sum 5 + assert_eq!(solutions.len(), 10); + for sol in &solutions { + assert!(problem.evaluate(sol)); + } +} + +#[test] +fn test_partition_single_element() { + // Single element can never be partitioned equally + let problem = Partition::new(vec![5]); + let solver = BruteForce::new(); + assert!(solver.find_satisfying(&problem).is_none()); +} + +#[test] +fn test_partition_two_equal_elements() { + let problem = Partition::new(vec![4, 4]); + let solver = BruteForce::new(); + let solution = solver.find_satisfying(&problem); + assert!(solution.is_some()); + assert!(problem.evaluate(&solution.unwrap())); +} + +#[test] +fn test_partition_serialization() { + let problem = Partition::new(vec![3, 1, 1, 2, 2, 1]); + let json = serde_json::to_value(&problem).unwrap(); + let restored: Partition = serde_json::from_value(json).unwrap(); + assert_eq!(restored.sizes(), problem.sizes()); + assert_eq!(restored.num_elements(), problem.num_elements()); +} + +#[test] +#[should_panic(expected = "All sizes must be positive")] +fn test_partition_zero_size_panics() { + Partition::new(vec![3, 0, 1]); +} + +#[test] +#[should_panic(expected = "at least one element")] +fn test_partition_empty_panics() { + Partition::new(vec![]); +} diff --git a/src/unit_tests/trait_consistency.rs b/src/unit_tests/trait_consistency.rs index 2fc6c2886..a8a39728c 100644 --- a/src/unit_tests/trait_consistency.rs +++ b/src/unit_tests/trait_consistency.rs @@ -88,6 +88,7 @@ fn test_all_problems_implement_trait_correctly() { "BalancedCompleteBipartiteSubgraph", ); check_problem_trait(&Factoring::new(6, 2, 2), "Factoring"); + check_problem_trait(&Partition::new(vec![3, 1, 1, 2, 2, 1]), "Partition"); check_problem_trait( &QuadraticAssignment::new(vec![vec![0, 1], vec![1, 0]], vec![vec![0, 1], vec![1, 0]]), "QuadraticAssignment",