Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions docs/paper/reductions.typ
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
4 changes: 4 additions & 0 deletions src/models/misc/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -78,6 +81,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::M
specs.extend(longest_common_subsequence::canonical_model_example_specs());
specs.extend(multiprocessor_scheduling::canonical_model_example_specs());
specs.extend(paintshop::canonical_model_example_specs());
specs.extend(partition::canonical_model_example_specs());
specs.extend(rectilinear_picture_compression::canonical_model_example_specs());
specs.extend(sequencing_within_intervals::canonical_model_example_specs());
specs.extend(staff_scheduling::canonical_model_example_specs());
Expand Down
130 changes: 130 additions & 0 deletions src/models/misc/partition.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
//! Partition problem implementation.
//!
//! Given a finite set of positive integers, determine whether it can be
//! partitioned into two subsets of equal sum. One of Karp's original 21
//! NP-complete problems (1972), Garey & Johnson SP12.

use crate::registry::{FieldInfo, ProblemSchemaEntry};
use crate::traits::{Problem, SatisfactionProblem};
use serde::{Deserialize, Serialize};

inventory::submit! {
ProblemSchemaEntry {
name: "Partition",
display_name: "Partition",
aliases: &[],
dimensions: &[],
module_path: module_path!(),
description: "Determine whether a multiset of positive integers can be partitioned into two subsets of equal sum",
fields: &[
FieldInfo { name: "sizes", type_name: "Vec<u64>", 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<u64>,
}

impl Partition {
/// Create a new Partition instance.
///
/// # Panics
///
/// Panics if `sizes` is empty or any size is zero.
pub fn new(sizes: Vec<u64>) -> 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<usize> {
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()
Comment on lines +102 to +108
}
Comment on lines +102 to +109
}

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<crate::example_db::specs::ModelExampleSpec> {
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;
2 changes: 1 addition & 1 deletion src/models/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
108 changes: 108 additions & 0 deletions src/unit_tests/models/misc/partition.rs
Original file line number Diff line number Diff line change
@@ -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![]);
}
1 change: 1 addition & 0 deletions src/unit_tests/trait_consistency.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading