Skip to content

Commit 14d8ea0

Browse files
GiggleLiuisPANN
andauthored
Fix #210: [Model] Partition (#664)
* Add plan for #210: [Model] Partition * feat: implement Partition model for #210 * fix: add Partition to prelude, strengthen solution count test * chore: remove plan file after implementation * fix: update Partition example spec to new ModelExampleSpec API --------- Co-authored-by: Xiwei Pan <xiwei.pan@connect.hkust-gz.edu.cn>
1 parent 0592724 commit 14d8ea0

7 files changed

Lines changed: 254 additions & 2 deletions

File tree

docs/paper/reductions.typ

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@
111111
"LongestCommonSubsequence": [Longest Common Subsequence],
112112
"ExactCoverBy3Sets": [Exact Cover by 3-Sets],
113113
"SubsetSum": [Subset Sum],
114+
"Partition": [Partition],
114115
"MinimumFeedbackArcSet": [Minimum Feedback Arc Set],
115116
"MinimumFeedbackVertexSet": [Minimum Feedback Vertex Set],
116117
"MinimumCutIntoBoundedSets": [Minimum Cut Into Bounded Sets],
@@ -2933,6 +2934,14 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76],
29332934
]
29342935
}
29352936

2937+
#problem-def("Partition")[
2938+
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)$.
2939+
][
2940+
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).
2941+
2942+
*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.
2943+
]
2944+
29362945
#{
29372946
let x = load-model-example("ShortestCommonSupersequence")
29382947
let alpha-size = x.instance.alphabet_size

src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ pub mod prelude {
6262
pub use crate::models::misc::{
6363
BinPacking, CbqRelation, ConjunctiveBooleanQuery, ConjunctiveQueryFoldability, Factoring,
6464
FlowShopScheduling, Knapsack, LongestCommonSubsequence, MinimumTardinessSequencing,
65-
MultiprocessorScheduling, PaintShop, QueryArg, RectilinearPictureCompression,
65+
MultiprocessorScheduling, PaintShop, Partition, QueryArg, RectilinearPictureCompression,
6666
ResourceConstrainedScheduling, SequencingToMinimizeMaximumCumulativeCost,
6767
SequencingWithReleaseTimesAndDeadlines, SequencingWithinIntervals,
6868
ShortestCommonSupersequence, StaffScheduling, StringToStringCorrection, SubsetSum,

src/models/misc/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
//! - [`LongestCommonSubsequence`]: Longest Common Subsequence
1212
//! - [`MinimumTardinessSequencing`]: Minimize tardy tasks in single-machine scheduling
1313
//! - [`PaintShop`]: Minimize color switches in paint shop scheduling
14+
//! - [`Partition`]: Partition a multiset into two equal-sum subsets
1415
//! - [`PartiallyOrderedKnapsack`]: Knapsack with precedence constraints
1516
//! - [`PrecedenceConstrainedScheduling`]: Schedule unit tasks on processors by deadline
1617
//! - [`RectilinearPictureCompression`]: Cover 1-entries with bounded rectangles
@@ -34,6 +35,7 @@ mod minimum_tardiness_sequencing;
3435
mod multiprocessor_scheduling;
3536
pub(crate) mod paintshop;
3637
pub(crate) mod partially_ordered_knapsack;
38+
pub(crate) mod partition;
3739
mod precedence_constrained_scheduling;
3840
mod rectilinear_picture_compression;
3941
pub(crate) mod resource_constrained_scheduling;
@@ -57,6 +59,7 @@ pub use minimum_tardiness_sequencing::MinimumTardinessSequencing;
5759
pub use multiprocessor_scheduling::MultiprocessorScheduling;
5860
pub use paintshop::PaintShop;
5961
pub use partially_ordered_knapsack::PartiallyOrderedKnapsack;
62+
pub use partition::Partition;
6063
pub use precedence_constrained_scheduling::PrecedenceConstrainedScheduling;
6164
pub use rectilinear_picture_compression::RectilinearPictureCompression;
6265
pub use resource_constrained_scheduling::ResourceConstrainedScheduling;
@@ -78,6 +81,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::M
7881
specs.extend(longest_common_subsequence::canonical_model_example_specs());
7982
specs.extend(multiprocessor_scheduling::canonical_model_example_specs());
8083
specs.extend(paintshop::canonical_model_example_specs());
84+
specs.extend(partition::canonical_model_example_specs());
8185
specs.extend(rectilinear_picture_compression::canonical_model_example_specs());
8286
specs.extend(sequencing_within_intervals::canonical_model_example_specs());
8387
specs.extend(staff_scheduling::canonical_model_example_specs());

src/models/misc/partition.rs

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
//! Partition problem implementation.
2+
//!
3+
//! Given a finite set of positive integers, determine whether it can be
4+
//! partitioned into two subsets of equal sum. One of Karp's original 21
5+
//! NP-complete problems (1972), Garey & Johnson SP12.
6+
7+
use crate::registry::{FieldInfo, ProblemSchemaEntry};
8+
use crate::traits::{Problem, SatisfactionProblem};
9+
use serde::{Deserialize, Serialize};
10+
11+
inventory::submit! {
12+
ProblemSchemaEntry {
13+
name: "Partition",
14+
display_name: "Partition",
15+
aliases: &[],
16+
dimensions: &[],
17+
module_path: module_path!(),
18+
description: "Determine whether a multiset of positive integers can be partitioned into two subsets of equal sum",
19+
fields: &[
20+
FieldInfo { name: "sizes", type_name: "Vec<u64>", description: "Positive integer size for each element" },
21+
],
22+
}
23+
}
24+
25+
/// The Partition problem.
26+
///
27+
/// Given a finite set `A` with `n` positive integer sizes, determine whether
28+
/// there exists a subset `A' ⊆ A` such that `∑_{a ∈ A'} s(a) = ∑_{a ∈ A\A'} s(a)`.
29+
///
30+
/// # Representation
31+
///
32+
/// Each element has a binary variable: `x_i = 1` if element `i` is in the
33+
/// second subset, `0` if in the first. The problem is satisfiable iff
34+
/// `∑_{i: x_i=1} sizes[i] = total_sum / 2`.
35+
///
36+
/// # Example
37+
///
38+
/// ```
39+
/// use problemreductions::models::misc::Partition;
40+
/// use problemreductions::{Problem, Solver, BruteForce};
41+
///
42+
/// let problem = Partition::new(vec![3, 1, 1, 2, 2, 1]);
43+
/// let solver = BruteForce::new();
44+
/// let solution = solver.find_satisfying(&problem);
45+
/// assert!(solution.is_some());
46+
/// ```
47+
#[derive(Debug, Clone, Serialize, Deserialize)]
48+
pub struct Partition {
49+
sizes: Vec<u64>,
50+
}
51+
52+
impl Partition {
53+
/// Create a new Partition instance.
54+
///
55+
/// # Panics
56+
///
57+
/// Panics if `sizes` is empty or any size is zero.
58+
pub fn new(sizes: Vec<u64>) -> Self {
59+
assert!(!sizes.is_empty(), "Partition requires at least one element");
60+
assert!(
61+
sizes.iter().all(|&s| s > 0),
62+
"All sizes must be positive (> 0)"
63+
);
64+
Self { sizes }
65+
}
66+
67+
/// Returns the element sizes.
68+
pub fn sizes(&self) -> &[u64] {
69+
&self.sizes
70+
}
71+
72+
/// Returns the number of elements.
73+
pub fn num_elements(&self) -> usize {
74+
self.sizes.len()
75+
}
76+
77+
/// Returns the total sum of all sizes.
78+
pub fn total_sum(&self) -> u64 {
79+
self.sizes.iter().sum()
80+
}
81+
}
82+
83+
impl Problem for Partition {
84+
const NAME: &'static str = "Partition";
85+
type Metric = bool;
86+
87+
fn variant() -> Vec<(&'static str, &'static str)> {
88+
crate::variant_params![]
89+
}
90+
91+
fn dims(&self) -> Vec<usize> {
92+
vec![2; self.num_elements()]
93+
}
94+
95+
fn evaluate(&self, config: &[usize]) -> bool {
96+
if config.len() != self.num_elements() {
97+
return false;
98+
}
99+
if config.iter().any(|&v| v >= 2) {
100+
return false;
101+
}
102+
let selected_sum: u64 = config
103+
.iter()
104+
.enumerate()
105+
.filter(|(_, &x)| x == 1)
106+
.map(|(i, _)| self.sizes[i])
107+
.sum();
108+
selected_sum * 2 == self.total_sum()
109+
}
110+
}
111+
112+
impl SatisfactionProblem for Partition {}
113+
114+
crate::declare_variants! {
115+
default sat Partition => "2^(num_elements / 2)",
116+
}
117+
118+
#[cfg(feature = "example-db")]
119+
pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
120+
vec![crate::example_db::specs::ModelExampleSpec {
121+
id: "partition",
122+
instance: Box::new(Partition::new(vec![3, 1, 1, 2, 2, 1])),
123+
optimal_config: vec![1, 0, 0, 1, 0, 0],
124+
optimal_value: serde_json::json!(true),
125+
}]
126+
}
127+
128+
#[cfg(test)]
129+
#[path = "../../unit_tests/models/misc/partition.rs"]
130+
mod tests;

src/models/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ pub use misc::PartiallyOrderedKnapsack;
2929
pub use misc::{
3030
BinPacking, CbqRelation, ConjunctiveBooleanQuery, ConjunctiveQueryFoldability, Factoring,
3131
FlowShopScheduling, Knapsack, LongestCommonSubsequence, MinimumTardinessSequencing,
32-
MultiprocessorScheduling, PaintShop, PrecedenceConstrainedScheduling, QueryArg,
32+
MultiprocessorScheduling, PaintShop, Partition, PrecedenceConstrainedScheduling, QueryArg,
3333
RectilinearPictureCompression, ResourceConstrainedScheduling,
3434
SequencingToMinimizeMaximumCumulativeCost, SequencingWithReleaseTimesAndDeadlines,
3535
SequencingWithinIntervals, ShortestCommonSupersequence, StaffScheduling,
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
use crate::models::misc::Partition;
2+
use crate::solvers::{BruteForce, Solver};
3+
use crate::traits::Problem;
4+
5+
#[test]
6+
fn test_partition_basic() {
7+
let problem = Partition::new(vec![3, 1, 1, 2, 2, 1]);
8+
assert_eq!(problem.num_elements(), 6);
9+
assert_eq!(problem.sizes(), &[3, 1, 1, 2, 2, 1]);
10+
assert_eq!(problem.total_sum(), 10);
11+
assert_eq!(problem.dims(), vec![2; 6]);
12+
}
13+
14+
#[test]
15+
fn test_partition_evaluate_satisfying() {
16+
let problem = Partition::new(vec![3, 1, 1, 2, 2, 1]);
17+
// A' = {3, 2} (indices 0, 3), sum = 5 = 10/2
18+
assert!(problem.evaluate(&[1, 0, 0, 1, 0, 0]));
19+
}
20+
21+
#[test]
22+
fn test_partition_evaluate_unsatisfying() {
23+
let problem = Partition::new(vec![3, 1, 1, 2, 2, 1]);
24+
// All in first subset, selected sum = 0 != 5
25+
assert!(!problem.evaluate(&[0, 0, 0, 0, 0, 0]));
26+
// All in second subset, selected sum = 10 != 5
27+
assert!(!problem.evaluate(&[1, 1, 1, 1, 1, 1]));
28+
}
29+
30+
#[test]
31+
fn test_partition_evaluate_wrong_length() {
32+
let problem = Partition::new(vec![3, 1, 1, 2, 2, 1]);
33+
assert!(!problem.evaluate(&[1, 0, 0]));
34+
assert!(!problem.evaluate(&[1, 0, 0, 1, 0, 0, 0]));
35+
}
36+
37+
#[test]
38+
fn test_partition_evaluate_invalid_value() {
39+
let problem = Partition::new(vec![3, 1, 1, 2, 2, 1]);
40+
assert!(!problem.evaluate(&[2, 0, 0, 0, 0, 0]));
41+
}
42+
43+
#[test]
44+
fn test_partition_odd_total() {
45+
// Total = 7 (odd), no equal partition possible
46+
let problem = Partition::new(vec![3, 1, 2, 1]);
47+
let solver = BruteForce::new();
48+
assert!(solver.find_satisfying(&problem).is_none());
49+
}
50+
51+
#[test]
52+
fn test_partition_solver() {
53+
let problem = Partition::new(vec![3, 1, 1, 2, 2, 1]);
54+
let solver = BruteForce::new();
55+
let solution = solver.find_satisfying(&problem);
56+
assert!(solution.is_some());
57+
assert!(problem.evaluate(&solution.unwrap()));
58+
}
59+
60+
#[test]
61+
fn test_partition_solver_all() {
62+
let problem = Partition::new(vec![3, 1, 1, 2, 2, 1]);
63+
let solver = BruteForce::new();
64+
let solutions = solver.find_all_satisfying(&problem);
65+
// 10 satisfying configs for {3,1,1,2,2,1} with target half-sum 5
66+
assert_eq!(solutions.len(), 10);
67+
for sol in &solutions {
68+
assert!(problem.evaluate(sol));
69+
}
70+
}
71+
72+
#[test]
73+
fn test_partition_single_element() {
74+
// Single element can never be partitioned equally
75+
let problem = Partition::new(vec![5]);
76+
let solver = BruteForce::new();
77+
assert!(solver.find_satisfying(&problem).is_none());
78+
}
79+
80+
#[test]
81+
fn test_partition_two_equal_elements() {
82+
let problem = Partition::new(vec![4, 4]);
83+
let solver = BruteForce::new();
84+
let solution = solver.find_satisfying(&problem);
85+
assert!(solution.is_some());
86+
assert!(problem.evaluate(&solution.unwrap()));
87+
}
88+
89+
#[test]
90+
fn test_partition_serialization() {
91+
let problem = Partition::new(vec![3, 1, 1, 2, 2, 1]);
92+
let json = serde_json::to_value(&problem).unwrap();
93+
let restored: Partition = serde_json::from_value(json).unwrap();
94+
assert_eq!(restored.sizes(), problem.sizes());
95+
assert_eq!(restored.num_elements(), problem.num_elements());
96+
}
97+
98+
#[test]
99+
#[should_panic(expected = "All sizes must be positive")]
100+
fn test_partition_zero_size_panics() {
101+
Partition::new(vec![3, 0, 1]);
102+
}
103+
104+
#[test]
105+
#[should_panic(expected = "at least one element")]
106+
fn test_partition_empty_panics() {
107+
Partition::new(vec![]);
108+
}

src/unit_tests/trait_consistency.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ fn test_all_problems_implement_trait_correctly() {
8888
"BalancedCompleteBipartiteSubgraph",
8989
);
9090
check_problem_trait(&Factoring::new(6, 2, 2), "Factoring");
91+
check_problem_trait(&Partition::new(vec![3, 1, 1, 2, 2, 1]), "Partition");
9192
check_problem_trait(
9293
&QuadraticAssignment::new(vec![vec![0, 1], vec![1, 0]], vec![vec![0, 1], vec![1, 0]]),
9394
"QuadraticAssignment",

0 commit comments

Comments
 (0)