Skip to main content

problemreductions/models/misc/
three_partition.rs

1//! 3-Partition problem implementation.
2//!
3//! Given 3m positive integers that each lie strictly between B/4 and B/2,
4//! determine whether they can be partitioned into m triples that all sum to B.
5
6use 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    /// Create a new 3-Partition instance.
83    ///
84    /// # Panics
85    ///
86    /// Panics if the input violates the classical 3-Partition invariants.
87    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;