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};
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        category: crate::registry::ProblemCategory::Misc,
22        module_path: module_path!(),
23        description: "Partition X∪Y into m pairs (one from X, one from Y) with pair sums matching targets",
24        fields: &[
25            FieldInfo { name: "sizes_x", type_name: "Vec<i64>", description: "Integer sizes for each element of X" },
26            FieldInfo { name: "sizes_y", type_name: "Vec<i64>", description: "Integer sizes for each element of Y" },
27            FieldInfo { name: "targets", type_name: "Vec<i64>", description: "Target sums for each pair" },
28        ],
29    }
30}
31
32#[derive(Debug, Clone, Serialize)]
33pub struct NumericalMatchingWithTargetSums {
34    sizes_x: Vec<i64>,
35    sizes_y: Vec<i64>,
36    targets: Vec<i64>,
37}
38
39impl NumericalMatchingWithTargetSums {
40    fn validate_inputs(
41        sizes_x: &[i64],
42        sizes_y: &[i64],
43        targets: &[i64],
44    ) -> Result<(), crate::registry::ConstructionError> {
45        let m = sizes_x.len();
46        if m == 0 {
47            return Err(
48                "NumericalMatchingWithTargetSums requires at least one element per set".into(),
49            );
50        }
51        if sizes_y.len() != m {
52            return Err(
53                "NumericalMatchingWithTargetSums requires sizes_x and sizes_y to have the same length"
54                    .into(),
55            );
56        }
57        if targets.len() != m {
58            return Err(
59                "NumericalMatchingWithTargetSums requires targets to have the same length as sizes_x"
60                    .into(),
61            );
62        }
63        Ok(())
64    }
65
66    pub fn try_new(
67        sizes_x: Vec<i64>,
68        sizes_y: Vec<i64>,
69        targets: Vec<i64>,
70    ) -> Result<Self, crate::registry::ConstructionError> {
71        Self::validate_inputs(&sizes_x, &sizes_y, &targets)?;
72        Ok(Self {
73            sizes_x,
74            sizes_y,
75            targets,
76        })
77    }
78
79    /// Create a new Numerical Matching with Target Sums instance.
80    ///
81    /// # Panics
82    ///
83    /// Panics if the input violates the NMTS invariants.
84    pub fn new(sizes_x: Vec<i64>, sizes_y: Vec<i64>, targets: Vec<i64>) -> Self {
85        Self::try_new(sizes_x, sizes_y, targets).unwrap_or_else(|message| panic!("{message}"))
86    }
87
88    /// Number of pairs (m).
89    pub fn num_pairs(&self) -> usize {
90        self.sizes_x.len()
91    }
92
93    /// Integer sizes for each element of X.
94    pub fn sizes_x(&self) -> &[i64] {
95        &self.sizes_x
96    }
97
98    /// Integer sizes for each element of Y.
99    pub fn sizes_y(&self) -> &[i64] {
100        &self.sizes_y
101    }
102
103    /// Target sums for each pair.
104    pub fn targets(&self) -> &[i64] {
105        &self.targets
106    }
107}
108
109#[derive(Deserialize)]
110struct NumericalMatchingWithTargetSumsData {
111    sizes_x: Vec<i64>,
112    sizes_y: Vec<i64>,
113    targets: Vec<i64>,
114}
115
116impl<'de> Deserialize<'de> for NumericalMatchingWithTargetSums {
117    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
118    where
119        D: Deserializer<'de>,
120    {
121        let data = NumericalMatchingWithTargetSumsData::deserialize(deserializer)?;
122        Self::try_new(data.sizes_x, data.sizes_y, data.targets).map_err(D::Error::custom)
123    }
124}
125
126impl Problem for NumericalMatchingWithTargetSums {
127    const NAME: &'static str = "NumericalMatchingWithTargetSums";
128    type Solution = Vec<usize>;
129    type Value = Or;
130
131    crate::problem_parameters![("num_pairs", num_pairs),];
132
133    fn variant() -> Vec<(&'static str, &'static str)> {
134        crate::variant_params![]
135    }
136
137    fn evaluate(&self, config: &Self::Solution) -> Result<Or, crate::traits::EvaluationError> {
138        Ok({
139            Or({
140                let m = self.num_pairs();
141                if config.len() != m {
142                    return Err(crate::traits::EvaluationError::InvalidConfiguration(
143                        "matching permutation length does not match the instance".into(),
144                    ));
145                }
146
147                if config.iter().any(|&index| index >= m) {
148                    return Err(crate::traits::EvaluationError::InvalidConfiguration(
149                        "matching permutation contains an out-of-range index".into(),
150                    ));
151                }
152
153                // Check config is valid permutation of 0..m
154                let mut used = vec![false; m];
155                for &idx in config {
156                    if idx >= m || used[idx] {
157                        return Ok(Or(false));
158                    }
159                    used[idx] = true;
160                }
161
162                // Compute pair sums and compare multisets
163                let mut pair_sums: Vec<i64> = (0..m)
164                    .map(|i| self.sizes_x[i] + self.sizes_y[config[i]])
165                    .collect();
166                let mut sorted_targets = self.targets.clone();
167                pair_sums.sort();
168                sorted_targets.sort();
169                pair_sums == sorted_targets
170            })
171        })
172    }
173}
174
175impl crate::solvers::BruteForceProblem for NumericalMatchingWithTargetSums {
176    fn dimensions(&self) -> Vec<usize> {
177        let m = self.num_pairs();
178        vec![m; m]
179    }
180}
181
182crate::declare_variants! {
183    default NumericalMatchingWithTargetSums => "2^num_pairs",
184}
185
186crate::register_brute_force! {
187    NumericalMatchingWithTargetSums,
188}
189
190#[cfg(feature = "example-db")]
191pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
192    vec![crate::example_db::specs::ModelExampleSpec {
193        id: "numerical_matching_with_target_sums",
194        instance: Box::new(NumericalMatchingWithTargetSums::new(
195            vec![1, 4, 7],
196            vec![2, 5, 3],
197            vec![3, 7, 12],
198        )),
199        optimal_config: serde_json::json!(vec![0, 2, 1]),
200        optimal_value: serde_json::json!(true),
201    }]
202}
203
204#[cfg(test)]
205#[path = "../../unit_tests/models/misc/numerical_matching_with_target_sums.rs"]
206mod tests;