Skip to main content

problemreductions/models/algebraic/
simultaneous_incongruences.rs

1//! Simultaneous Incongruences problem implementation.
2//!
3//! Given a list of pairs (aᵢ, bᵢ) with bᵢ > 0 and 1 ≤ aᵢ ≤ bᵢ, determine whether
4//! there exists a non-negative integer x such that x ≢ aᵢ (mod bᵢ) for all i.
5
6use crate::registry::{FieldInfo, ProblemSchemaEntry};
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: "SimultaneousIncongruences",
15        display_name: "Simultaneous Incongruences",
16        aliases: &[],
17        dimensions: &[],
18        category: crate::registry::ProblemCategory::Algebraic,
19        module_path: module_path!(),
20        description: "Decide whether there exists x with x ≢ aᵢ (mod bᵢ) for all i",
21        fields: &[
22            FieldInfo {
23                name: "pairs",
24                type_name: "Vec<(i64, i64)>",
25                description: "Pairs (aᵢ, bᵢ) with bᵢ > 0 and 1 ≤ aᵢ ≤ bᵢ",
26            },
27        ],
28    }
29}
30
31/// Simultaneous Incongruences problem.
32///
33/// Given a list of pairs (aᵢ, bᵢ) with bᵢ > 0 and 1 ≤ aᵢ ≤ bᵢ, determine whether
34/// there exists a non-negative integer x such that x ≢ aᵢ (mod bᵢ) for all i simultaneously.
35///
36/// The search space is x ∈ {0, …, L−1} where L = lcm(b₁, …, bₙ) (one full period).
37/// `config[0]` encodes x directly.
38///
39/// # Example
40///
41/// ```
42/// use problemreductions::models::algebraic::SimultaneousIncongruences;
43/// use problemreductions::{Problem, BruteForce};
44///
45/// // pairs: [(2,2),(1,3),(2,5),(3,7)] — lcm=210, x=5 is a solution
46/// let problem = SimultaneousIncongruences::new(vec![(2,2),(1,3),(2,5),(3,7)]).unwrap();
47/// let solver = BruteForce::new();
48/// let witness = solver.solve(&problem).unwrap();
49/// assert!(witness.is_some());
50/// ```
51#[derive(Debug, Clone, Serialize)]
52pub struct SimultaneousIncongruences {
53    /// Incongruence pairs (aᵢ, bᵢ).
54    pairs: Vec<(i64, i64)>,
55}
56
57fn gcd(mut a: i64, mut b: i64) -> i64 {
58    while b != 0 {
59        let t = b;
60        b = a % b;
61        a = t;
62    }
63    a
64}
65
66impl SimultaneousIncongruences {
67    fn validate_inputs(pairs: &[(i64, i64)]) -> Result<(), crate::registry::ConstructionError> {
68        for (i, &(a, b)) in pairs.iter().enumerate() {
69            if b <= 0 {
70                return Err(format!("Modulus b at index {i} must be positive (got b={b})").into());
71            }
72            if a <= 0 {
73                return Err(format!("Residue a at index {i} must be at least 1 (got a=0)").into());
74            }
75            if a > b {
76                return Err(format!(
77                    "Residue a ({a}) must not exceed modulus b ({b}) at index {i}"
78                )
79                .into());
80            }
81        }
82        pairs.iter().try_fold(1i64, |lcm, &(_, modulus)| {
83            (lcm / gcd(lcm, modulus))
84                .checked_mul(modulus)
85                .ok_or_else(|| "Least common multiple of moduli exceeds i64 range".to_string())
86        })?;
87        Ok(())
88    }
89
90    /// Create a new `SimultaneousIncongruences` instance, returning an error
91    /// if any pair is invalid.
92    pub fn new(pairs: Vec<(i64, i64)>) -> Result<Self, crate::registry::ConstructionError> {
93        Self::validate_inputs(&pairs)?;
94        Ok(Self { pairs })
95    }
96
97    /// Get the number of incongruence pairs.
98    pub fn num_pairs(&self) -> usize {
99        self.pairs.len()
100    }
101
102    /// Get the incongruence pairs.
103    pub fn pairs(&self) -> &[(i64, i64)] {
104        &self.pairs
105    }
106
107    /// Compute the LCM of all moduli.
108    pub fn lcm_moduli(&self) -> i64 {
109        self.pairs.iter().fold(1i64, |lcm, &(_, modulus)| {
110            (lcm / gcd(lcm, modulus)) * modulus
111        })
112    }
113}
114
115#[derive(Deserialize)]
116struct SimultaneousIncongruencesData {
117    pairs: Vec<(i64, i64)>,
118}
119
120impl<'de> Deserialize<'de> for SimultaneousIncongruences {
121    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
122    where
123        D: Deserializer<'de>,
124    {
125        let data = SimultaneousIncongruencesData::deserialize(deserializer)?;
126        Self::new(data.pairs).map_err(D::Error::custom)
127    }
128}
129
130impl Problem for SimultaneousIncongruences {
131    const NAME: &'static str = "SimultaneousIncongruences";
132    type Solution = i64;
133    type Value = Or;
134
135    crate::problem_parameters![("num_pairs", num_pairs),];
136
137    fn variant() -> Vec<(&'static str, &'static str)> {
138        crate::variant_params![]
139    }
140
141    fn evaluate(&self, solution: &Self::Solution) -> Result<Or, crate::traits::EvaluationError> {
142        Ok({
143            // x is a solution iff x % bᵢ ≠ aᵢ % bᵢ for every pair.
144            Or(self.pairs.iter().all(|&(a, b)| solution % b != a % b))
145        })
146    }
147}
148
149impl crate::solvers::BruteForceProblem for SimultaneousIncongruences {
150    fn dimensions(&self) -> Vec<usize> {
151        let lcm = usize::try_from(self.lcm_moduli()).expect("validated positive LCM fits usize");
152        vec![lcm]
153    }
154}
155
156crate::declare_variants! {
157    default SimultaneousIncongruences => "num_pairs",
158}
159
160crate::register_brute_force! {
161    SimultaneousIncongruences decode |_, indices: Vec<usize>| i64::try_from(indices[0]).expect("enumerated incongruence value fits i64"),
162}
163
164#[cfg(feature = "example-db")]
165pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
166    vec![crate::example_db::specs::ModelExampleSpec {
167        id: "simultaneous_incongruences",
168        instance: Box::new(
169            SimultaneousIncongruences::new(vec![(2, 2), (1, 3), (2, 5), (3, 7)]).unwrap(),
170        ),
171        // x=5: 5%2=1≠0(=2%2), 5%3=2≠1, 5%5=0≠2, 5%7=5≠3 ✓
172        optimal_config: serde_json::json!(5),
173        optimal_value: serde_json::json!(true),
174    }]
175}
176
177#[cfg(test)]
178#[path = "../../unit_tests/models/algebraic/simultaneous_incongruences.rs"]
179mod tests;