problemreductions/models/misc/
sum_of_squares_partition.rs1use 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#[derive(Debug, Clone, Serialize)]
55pub struct SumOfSquaresPartition {
56 sizes: Vec<i64>,
58 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 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 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 pub fn sizes(&self) -> &[i64] {
94 &self.sizes
95 }
96
97 pub fn num_groups(&self) -> usize {
99 self.num_groups
100 }
101
102 pub fn num_elements(&self) -> usize {
104 self.sizes.len()
105 }
106
107 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 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;