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