Skip to main content

problemreductions/models/misc/
numerical_matching_with_target_sums.rs

1//! Numerical Matching with Target Sums problem implementation.
2//!
3//! Given two disjoint sets X and Y each with m elements, integer sizes
4//! s(x) for x ∈ X and s(y) for y ∈ Y, and a multiset of m target values
5//! B_1, …, B_m, decide whether X ∪ Y can be partitioned into m pairs,
6//! each containing one element from X and one from Y, such that the
7//! multiset of pair sums {s(x_i) + s(y_{π(i)})} equals the target multiset.
8
9use crate::registry::{FieldInfo, ProblemSchemaEntry, ProblemSizeFieldEntry};
10use crate::traits::Problem;
11use crate::types::Or;
12use serde::de::Error as _;
13use serde::{Deserialize, Deserializer, Serialize};
14
15inventory::submit! {
16    ProblemSchemaEntry {
17        name: "NumericalMatchingWithTargetSums",
18        display_name: "Numerical Matching with Target Sums",
19        aliases: &["NMTS"],
20        dimensions: &[],
21        module_path: module_path!(),
22        description: "Partition X∪Y into m pairs (one from X, one from Y) with pair sums matching targets",
23        fields: &[
24            FieldInfo { name: "sizes_x", type_name: "Vec<i64>", description: "Integer sizes for each element of X" },
25            FieldInfo { name: "sizes_y", type_name: "Vec<i64>", description: "Integer sizes for each element of Y" },
26            FieldInfo { name: "targets", type_name: "Vec<i64>", description: "Target sums for each pair" },
27        ],
28    }
29}
30
31inventory::submit! {
32    ProblemSizeFieldEntry {
33        name: "NumericalMatchingWithTargetSums",
34        fields: &["num_pairs"],
35    }
36}
37
38#[derive(Debug, Clone, Serialize)]
39pub struct NumericalMatchingWithTargetSums {
40    sizes_x: Vec<i64>,
41    sizes_y: Vec<i64>,
42    targets: Vec<i64>,
43}
44
45impl NumericalMatchingWithTargetSums {
46    fn validate_inputs(sizes_x: &[i64], sizes_y: &[i64], targets: &[i64]) -> Result<(), String> {
47        let m = sizes_x.len();
48        if m == 0 {
49            return Err(
50                "NumericalMatchingWithTargetSums requires at least one element per set".to_string(),
51            );
52        }
53        if sizes_y.len() != m {
54            return Err(
55                "NumericalMatchingWithTargetSums requires sizes_x and sizes_y to have the same length"
56                    .to_string(),
57            );
58        }
59        if targets.len() != m {
60            return Err(
61                "NumericalMatchingWithTargetSums requires targets to have the same length as sizes_x"
62                    .to_string(),
63            );
64        }
65        Ok(())
66    }
67
68    pub fn try_new(
69        sizes_x: Vec<i64>,
70        sizes_y: Vec<i64>,
71        targets: Vec<i64>,
72    ) -> Result<Self, String> {
73        Self::validate_inputs(&sizes_x, &sizes_y, &targets)?;
74        Ok(Self {
75            sizes_x,
76            sizes_y,
77            targets,
78        })
79    }
80
81    /// Create a new Numerical Matching with Target Sums instance.
82    ///
83    /// # Panics
84    ///
85    /// Panics if the input violates the NMTS invariants.
86    pub fn new(sizes_x: Vec<i64>, sizes_y: Vec<i64>, targets: Vec<i64>) -> Self {
87        Self::try_new(sizes_x, sizes_y, targets).unwrap_or_else(|message| panic!("{message}"))
88    }
89
90    /// Number of pairs (m).
91    pub fn num_pairs(&self) -> usize {
92        self.sizes_x.len()
93    }
94
95    /// Integer sizes for each element of X.
96    pub fn sizes_x(&self) -> &[i64] {
97        &self.sizes_x
98    }
99
100    /// Integer sizes for each element of Y.
101    pub fn sizes_y(&self) -> &[i64] {
102        &self.sizes_y
103    }
104
105    /// Target sums for each pair.
106    pub fn targets(&self) -> &[i64] {
107        &self.targets
108    }
109}
110
111#[derive(Deserialize)]
112struct NumericalMatchingWithTargetSumsData {
113    sizes_x: Vec<i64>,
114    sizes_y: Vec<i64>,
115    targets: Vec<i64>,
116}
117
118impl<'de> Deserialize<'de> for NumericalMatchingWithTargetSums {
119    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
120    where
121        D: Deserializer<'de>,
122    {
123        let data = NumericalMatchingWithTargetSumsData::deserialize(deserializer)?;
124        Self::try_new(data.sizes_x, data.sizes_y, data.targets).map_err(D::Error::custom)
125    }
126}
127
128impl Problem for NumericalMatchingWithTargetSums {
129    const NAME: &'static str = "NumericalMatchingWithTargetSums";
130    type Value = Or;
131
132    fn variant() -> Vec<(&'static str, &'static str)> {
133        crate::variant_params![]
134    }
135
136    fn dims(&self) -> Vec<usize> {
137        let m = self.num_pairs();
138        vec![m; m]
139    }
140
141    fn evaluate(&self, config: &[usize]) -> Or {
142        Or({
143            let m = self.num_pairs();
144            if config.len() != m {
145                return Or(false);
146            }
147
148            // Check config is valid permutation of 0..m
149            let mut used = vec![false; m];
150            for &idx in config {
151                if idx >= m || used[idx] {
152                    return Or(false);
153                }
154                used[idx] = true;
155            }
156
157            // Compute pair sums and compare multisets
158            let mut pair_sums: Vec<i64> = (0..m)
159                .map(|i| self.sizes_x[i] + self.sizes_y[config[i]])
160                .collect();
161            let mut sorted_targets = self.targets.clone();
162            pair_sums.sort();
163            sorted_targets.sort();
164            pair_sums == sorted_targets
165        })
166    }
167}
168
169crate::declare_variants! {
170    default NumericalMatchingWithTargetSums => "2^num_pairs",
171}
172
173#[cfg(feature = "example-db")]
174pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
175    vec![crate::example_db::specs::ModelExampleSpec {
176        id: "numerical_matching_with_target_sums",
177        instance: Box::new(NumericalMatchingWithTargetSums::new(
178            vec![1, 4, 7],
179            vec![2, 5, 3],
180            vec![3, 7, 12],
181        )),
182        optimal_config: vec![0, 2, 1],
183        optimal_value: serde_json::json!(true),
184    }]
185}
186
187#[cfg(test)]
188#[path = "../../unit_tests/models/misc/numerical_matching_with_target_sums.rs"]
189mod tests;