Skip to main content

problemreductions/models/misc/
sum_of_squares_partition.rs

1//! Sum of Squares Partition problem implementation.
2//!
3//! Given a finite set of positive integers and K groups, find a partition
4//! into K groups that minimizes the sum of squared group sums.
5//! NP-hard in the strong sense (Garey & Johnson, SP19).
6
7use crate::registry::{FieldInfo, ProblemSchemaEntry};
8use crate::traits::Problem;
9use crate::types::Min;
10use serde::de::Error;
11use serde::{Deserialize, Deserializer, Serialize};
12
13inventory::submit! {
14    ProblemSchemaEntry {
15        name: "SumOfSquaresPartition",
16        display_name: "Sum of Squares Partition",
17        aliases: &[],
18        dimensions: &[],
19        module_path: module_path!(),
20        description: "Partition positive integers into K groups minimizing the sum of squared group sums",
21        fields: &[
22            FieldInfo { name: "sizes", type_name: "Vec<i64>", description: "Positive integer size s(a) for each element a in A" },
23            FieldInfo { name: "num_groups", type_name: "usize", description: "Number of groups K in the partition" },
24        ],
25    }
26}
27
28/// The Sum of Squares Partition problem (Garey & Johnson SP19).
29///
30/// Given a finite set `A` with sizes `s(a) ∈ Z⁺` for each `a ∈ A`
31/// and a positive integer `K ≤ |A|` (number of groups), find a
32/// partition of `A` into `K` disjoint sets `A_1, ..., A_K` that
33/// minimizes:
34///
35/// `∑_{i=1}^{K} (∑_{a ∈ A_i} s(a))²`
36///
37/// # Representation
38///
39/// Each element has a variable in `{0, ..., K-1}` representing its
40/// group assignment. The value is the sum of squared group sums.
41///
42/// # Example
43///
44/// ```
45/// use problemreductions::models::misc::SumOfSquaresPartition;
46/// use problemreductions::{Problem, Solver, BruteForce};
47///
48/// // 6 elements with sizes [5, 3, 8, 2, 7, 1], K=3 groups
49/// let problem = SumOfSquaresPartition::new(vec![5, 3, 8, 2, 7, 1], 3);
50/// let solver = BruteForce::new();
51/// let solution = solver.find_witness(&problem);
52/// assert!(solution.is_some());
53/// ```
54#[derive(Debug, Clone, Serialize)]
55pub struct SumOfSquaresPartition {
56    /// Positive integer sizes for each element.
57    sizes: Vec<i64>,
58    /// Number of groups K.
59    num_groups: usize,
60}
61
62impl SumOfSquaresPartition {
63    fn validate_inputs(sizes: &[i64], num_groups: usize) -> Result<(), String> {
64        if sizes.iter().any(|&size| size <= 0) {
65            return Err("All sizes must be positive (> 0)".to_string());
66        }
67        if num_groups == 0 {
68            return Err("Number of groups must be positive".to_string());
69        }
70        if num_groups > sizes.len() {
71            return Err("Number of groups must not exceed number of elements".to_string());
72        }
73        Ok(())
74    }
75
76    /// Create a new SumOfSquaresPartition instance, returning validation errors.
77    pub fn try_new(sizes: Vec<i64>, num_groups: usize) -> Result<Self, String> {
78        Self::validate_inputs(&sizes, num_groups)?;
79        Ok(Self { sizes, num_groups })
80    }
81
82    /// Create a new SumOfSquaresPartition instance.
83    ///
84    /// # Panics
85    ///
86    /// Panics if any size is not positive (must be > 0), if `num_groups` is 0,
87    /// or if `num_groups` exceeds the number of elements.
88    pub fn new(sizes: Vec<i64>, num_groups: usize) -> Self {
89        Self::try_new(sizes, num_groups).unwrap_or_else(|message| panic!("{message}"))
90    }
91
92    /// Returns the element sizes.
93    pub fn sizes(&self) -> &[i64] {
94        &self.sizes
95    }
96
97    /// Returns the number of groups K.
98    pub fn num_groups(&self) -> usize {
99        self.num_groups
100    }
101
102    /// Returns the number of elements |A|.
103    pub fn num_elements(&self) -> usize {
104        self.sizes.len()
105    }
106
107    /// Compute the sum of squared group sums for a given configuration.
108    ///
109    /// Returns `None` if the configuration is invalid (wrong length or
110    /// out-of-range group index), or if arithmetic overflows `i64`.
111    pub fn sum_of_squares(&self, config: &[usize]) -> Option<i64> {
112        if config.len() != self.sizes.len() {
113            return None;
114        }
115        let mut group_sums = vec![0i128; self.num_groups];
116        for (i, &g) in config.iter().enumerate() {
117            if g >= self.num_groups {
118                return None;
119            }
120            group_sums[g] = group_sums[g].checked_add(i128::from(self.sizes[i]))?;
121        }
122        group_sums
123            .into_iter()
124            .try_fold(0i128, |total, group_sum| {
125                let square = group_sum.checked_mul(group_sum)?;
126                total.checked_add(square)
127            })
128            .and_then(|total| i64::try_from(total).ok())
129    }
130}
131
132#[derive(Deserialize)]
133struct SumOfSquaresPartitionData {
134    sizes: Vec<i64>,
135    num_groups: usize,
136}
137
138impl<'de> Deserialize<'de> for SumOfSquaresPartition {
139    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
140    where
141        D: Deserializer<'de>,
142    {
143        let data = SumOfSquaresPartitionData::deserialize(deserializer)?;
144        Self::try_new(data.sizes, data.num_groups).map_err(D::Error::custom)
145    }
146}
147
148impl Problem for SumOfSquaresPartition {
149    const NAME: &'static str = "SumOfSquaresPartition";
150    type Value = Min<i64>;
151
152    fn variant() -> Vec<(&'static str, &'static str)> {
153        crate::variant_params![]
154    }
155
156    fn dims(&self) -> Vec<usize> {
157        vec![self.num_groups; self.sizes.len()]
158    }
159
160    fn evaluate(&self, config: &[usize]) -> Min<i64> {
161        Min(self.sum_of_squares(config))
162    }
163}
164
165crate::declare_variants! {
166    default SumOfSquaresPartition => "num_groups^num_elements",
167}
168
169#[cfg(feature = "example-db")]
170pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
171    vec![crate::example_db::specs::ModelExampleSpec {
172        id: "sum_of_squares_partition",
173        // sizes=[5,3,8,2,7,1], K=3
174        // Optimal: groups {8},{2,7},{5,3,1} -> sums 8,9,9 -> 64+81+81=226
175        instance: Box::new(SumOfSquaresPartition::new(vec![5, 3, 8, 2, 7, 1], 3)),
176        optimal_config: vec![2, 2, 0, 1, 1, 0],
177        optimal_value: serde_json::json!(226),
178    }]
179}
180
181#[cfg(test)]
182#[path = "../../unit_tests/models/misc/sum_of_squares_partition.rs"]
183mod tests;