problemreductions/models/misc/
three_partition.rs1use crate::registry::{FieldInfo, ProblemSchemaEntry, ProblemSizeFieldEntry};
7use crate::traits::Problem;
8use crate::types::Or;
9use serde::de::Error as _;
10use serde::{Deserialize, Deserializer, Serialize};
11
12inventory::submit! {
13 ProblemSchemaEntry {
14 name: "ThreePartition",
15 display_name: "3-Partition",
16 aliases: &["3Partition", "3-Partition"],
17 dimensions: &[],
18 module_path: module_path!(),
19 description: "Partition 3m bounded positive integers into m triples whose sums all equal B",
20 fields: &[
21 FieldInfo { name: "sizes", type_name: "Vec<u64>", description: "Positive integer sizes s(a) for each element a in A" },
22 FieldInfo { name: "bound", type_name: "u64", description: "Target sum B for each triple" },
23 ],
24 }
25}
26
27inventory::submit! {
28 ProblemSizeFieldEntry {
29 name: "ThreePartition",
30 fields: &["num_elements", "num_groups"],
31 }
32}
33
34#[derive(Debug, Clone, Serialize)]
35pub struct ThreePartition {
36 sizes: Vec<u64>,
37 bound: u64,
38}
39
40impl ThreePartition {
41 fn validate_inputs(sizes: &[u64], bound: u64) -> Result<(), String> {
42 if sizes.is_empty() {
43 return Err("ThreePartition requires at least one element".to_string());
44 }
45 if !sizes.len().is_multiple_of(3) {
46 return Err(
47 "ThreePartition requires the number of elements to be a multiple of 3".to_string(),
48 );
49 }
50 if bound == 0 {
51 return Err("ThreePartition requires a positive bound".to_string());
52 }
53 if sizes.contains(&0) {
54 return Err("All sizes must be positive (> 0)".to_string());
55 }
56
57 let bound128 = u128::from(bound);
58 for &size in sizes {
59 let size = u128::from(size);
60 if !(4 * size > bound128 && 2 * size < bound128) {
61 return Err("Every size must lie strictly between B/4 and B/2".to_string());
62 }
63 }
64
65 let total_sum: u128 = sizes.iter().map(|&size| u128::from(size)).sum();
66 let expected_sum = u128::from(bound) * (sizes.len() as u128 / 3);
67 if total_sum != expected_sum {
68 return Err("Total sum of sizes must equal m * bound".to_string());
69 }
70 if total_sum > u128::from(u64::MAX) {
71 return Err("Total sum exceeds u64 range".to_string());
72 }
73
74 Ok(())
75 }
76
77 pub fn try_new(sizes: Vec<u64>, bound: u64) -> Result<Self, String> {
78 Self::validate_inputs(&sizes, bound)?;
79 Ok(Self { sizes, bound })
80 }
81
82 pub fn new(sizes: Vec<u64>, bound: u64) -> Self {
88 Self::try_new(sizes, bound).unwrap_or_else(|message| panic!("{message}"))
89 }
90
91 pub fn sizes(&self) -> &[u64] {
92 &self.sizes
93 }
94
95 pub fn bound(&self) -> u64 {
96 self.bound
97 }
98
99 pub fn num_elements(&self) -> usize {
100 self.sizes.len()
101 }
102
103 pub fn num_groups(&self) -> usize {
104 self.sizes.len() / 3
105 }
106
107 pub fn total_sum(&self) -> u64 {
108 self.sizes
109 .iter()
110 .copied()
111 .reduce(|acc, value| {
112 acc.checked_add(value)
113 .expect("validated sum must fit in u64")
114 })
115 .unwrap_or(0)
116 }
117
118 fn group_counts_and_sums(&self, config: &[usize]) -> Option<(Vec<usize>, Vec<u128>)> {
119 if config.len() != self.num_elements() {
120 return None;
121 }
122
123 let mut counts = vec![0usize; self.num_groups()];
124 let mut sums = vec![0u128; self.num_groups()];
125
126 for (index, &group) in config.iter().enumerate() {
127 if group >= self.num_groups() {
128 return None;
129 }
130 counts[group] += 1;
131 sums[group] += u128::from(self.sizes[index]);
132 }
133
134 Some((counts, sums))
135 }
136}
137
138#[derive(Deserialize)]
139struct ThreePartitionData {
140 sizes: Vec<u64>,
141 bound: u64,
142}
143
144impl<'de> Deserialize<'de> for ThreePartition {
145 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
146 where
147 D: Deserializer<'de>,
148 {
149 let data = ThreePartitionData::deserialize(deserializer)?;
150 Self::try_new(data.sizes, data.bound).map_err(D::Error::custom)
151 }
152}
153
154impl Problem for ThreePartition {
155 const NAME: &'static str = "ThreePartition";
156 type Value = Or;
157
158 fn variant() -> Vec<(&'static str, &'static str)> {
159 crate::variant_params![]
160 }
161
162 fn dims(&self) -> Vec<usize> {
163 vec![self.num_groups(); self.num_elements()]
164 }
165
166 fn evaluate(&self, config: &[usize]) -> Or {
167 Or({
168 let Some((counts, sums)) = self.group_counts_and_sums(config) else {
169 return Or(false);
170 };
171
172 let target = u128::from(self.bound);
173 counts.into_iter().all(|count| count == 3) && sums.into_iter().all(|sum| sum == target)
174 })
175 }
176}
177
178crate::declare_variants! {
179 default ThreePartition => "3^num_elements",
180}
181
182#[cfg(feature = "example-db")]
183pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
184 vec![crate::example_db::specs::ModelExampleSpec {
185 id: "three_partition",
186 instance: Box::new(ThreePartition::new(vec![4, 5, 6, 4, 6, 5], 15)),
187 optimal_config: vec![0, 0, 0, 1, 1, 1],
188 optimal_value: serde_json::json!(true),
189 }]
190}
191
192#[cfg(test)]
193#[path = "../../unit_tests/models/misc/three_partition.rs"]
194mod tests;