Skip to main content

problemreductions/rules/
ksatisfiability_quadraticcongruences.rs

1//! Reduction from KSatisfiability (3-SAT) to Quadratic Congruences.
2//!
3//! This follows the Manders-Adleman construction in its doubled-coefficient
4//! form, matching the verified reference vectors for issue #553.
5
6use std::collections::{BTreeMap, BTreeSet};
7
8use crate::models::algebraic::QuadraticCongruences;
9use crate::models::formula::KSatisfiability;
10use crate::reduction;
11use crate::rules::traits::{ReduceTo, ReductionResult};
12use crate::variant::K3;
13use num_bigint::{BigInt, BigUint};
14use num_traits::{One, Signed, Zero};
15
16#[derive(Debug, Clone)]
17pub struct Reduction3SATToQuadraticCongruences {
18    target: QuadraticCongruences,
19    source_num_vars: usize,
20    active_to_source: Vec<usize>,
21    standard_clause_count: usize,
22    h: BigUint,
23    prime_powers: Vec<BigUint>,
24}
25
26impl ReductionResult for Reduction3SATToQuadraticCongruences {
27    type Source = KSatisfiability<K3>;
28    type Target = QuadraticCongruences;
29
30    fn target_problem(&self) -> &Self::Target {
31        &self.target
32    }
33
34    fn extract_solution(&self, target_solution: &[usize]) -> Vec<usize> {
35        let mut source_assignment = vec![0; self.source_num_vars];
36        let Some(x) = self.target.decode_witness(target_solution) else {
37            return source_assignment;
38        };
39        if x > self.h {
40            return source_assignment;
41        }
42
43        let h_minus_x = &self.h - &x;
44        let h_plus_x = &self.h + &x;
45        let mut alpha = vec![0i8; self.prime_powers.len()];
46
47        for (j, prime_power) in self.prime_powers.iter().enumerate() {
48            if (&h_minus_x % prime_power).is_zero() {
49                alpha[j] = 1;
50            } else if (&h_plus_x % prime_power).is_zero() {
51                alpha[j] = -1;
52            }
53        }
54
55        for (active_index, &source_index) in self.active_to_source.iter().enumerate() {
56            let alpha_index = 2 * self.standard_clause_count + active_index + 1;
57            source_assignment[source_index] = if alpha.get(alpha_index) == Some(&-1) {
58                1
59            } else {
60                0
61            };
62        }
63
64        source_assignment
65    }
66}
67
68#[cfg_attr(not(any(test, feature = "example-db")), allow(dead_code))]
69#[derive(Debug, Clone)]
70struct MandersAdlemanConstruction {
71    target: QuadraticCongruences,
72    source_num_vars: usize,
73    active_to_source: Vec<usize>,
74    remapped_clause_set: BTreeSet<Vec<i32>>,
75    standard_clauses: Vec<Vec<i32>>,
76    standard_clause_count: usize,
77    active_var_count: usize,
78    #[cfg_attr(not(test), allow(dead_code))]
79    doubled_coefficients: Vec<BigInt>,
80    #[cfg_attr(not(test), allow(dead_code))]
81    tau_2: BigInt,
82    thetas: Vec<BigUint>,
83    h: BigUint,
84    prime_powers: Vec<BigUint>,
85}
86
87fn is_prime(candidate: u64) -> bool {
88    if candidate < 2 {
89        return false;
90    }
91    if candidate == 2 {
92        return true;
93    }
94    if candidate.is_multiple_of(2) {
95        return false;
96    }
97
98    let mut divisor = 3u64;
99    while divisor * divisor <= candidate {
100        if candidate.is_multiple_of(divisor) {
101            return false;
102        }
103        divisor += 2;
104    }
105    true
106}
107
108fn admissible_primes(count: usize) -> Vec<u64> {
109    let mut primes = Vec::with_capacity(count);
110    let mut candidate = 13u64;
111    while primes.len() < count {
112        if is_prime(candidate) {
113            primes.push(candidate);
114        }
115        candidate += 1;
116    }
117    primes
118}
119
120fn pow_biguint_u64(base: u64, exp: usize) -> BigUint {
121    let mut result = BigUint::one();
122    let factor = BigUint::from(base);
123    for _ in 0..exp {
124        result *= &factor;
125    }
126    result
127}
128
129fn bigint_mod_to_biguint(value: &BigInt, modulus: &BigUint) -> BigUint {
130    let modulus_bigint = BigInt::from(modulus.clone());
131    let reduced = ((value % &modulus_bigint) + &modulus_bigint) % &modulus_bigint;
132    reduced
133        .to_biguint()
134        .expect("nonnegative reduced residue must fit BigUint")
135}
136
137fn modular_inverse(value: &BigUint, modulus: &BigUint) -> BigUint {
138    let mut t = BigInt::zero();
139    let mut new_t = BigInt::one();
140    let mut r = BigInt::from(modulus.clone());
141    let mut new_r = BigInt::from(value.clone() % modulus);
142
143    while !new_r.is_zero() {
144        let quotient = &r / &new_r;
145        let next_t = &t - &quotient * &new_t;
146        let next_r = &r - &quotient * &new_r;
147        t = new_t;
148        new_t = next_t;
149        r = new_r;
150        new_r = next_r;
151    }
152
153    assert_eq!(r, BigInt::one(), "value and modulus must be coprime");
154    if t.is_negative() {
155        t += BigInt::from(modulus.clone());
156    }
157    t.to_biguint().expect("inverse must be nonnegative")
158}
159
160fn normalize_clause(clause: &[i32]) -> Option<Vec<i32>> {
161    let mut lits = BTreeSet::new();
162    for &lit in clause {
163        if lits.contains(&-lit) {
164            return None;
165        }
166        lits.insert(lit);
167    }
168    Some(lits.into_iter().collect())
169}
170
171fn preprocess_formula(source: &KSatisfiability<K3>) -> (Vec<Vec<i32>>, Vec<usize>) {
172    let mut seen = BTreeSet::new();
173    let mut normalized_clauses = Vec::new();
174    let mut active_vars = BTreeSet::new();
175
176    for clause in source.clauses() {
177        let Some(normalized) = normalize_clause(&clause.literals) else {
178            continue;
179        };
180        if normalized.len() != 3 {
181            panic!(
182                "3-SAT -> QuadraticCongruences requires each non-tautological clause to use three distinct literals; got {:?}",
183                clause.literals
184            );
185        }
186
187        let distinct_vars: BTreeSet<_> = normalized
188            .iter()
189            .map(|lit| lit.unsigned_abs() as usize)
190            .collect();
191        if distinct_vars.len() != 3 {
192            panic!(
193                "3-SAT -> QuadraticCongruences requires each non-tautological clause to use three distinct variables; got {:?}",
194                clause.literals
195            );
196        }
197
198        if seen.insert(normalized.clone()) {
199            for &lit in &normalized {
200                active_vars.insert(lit.unsigned_abs() as usize);
201            }
202            normalized_clauses.push(normalized);
203        }
204    }
205
206    (normalized_clauses, active_vars.into_iter().collect())
207}
208
209fn build_standard_clauses(num_active_vars: usize) -> (Vec<Vec<i32>>, BTreeMap<Vec<i32>, usize>) {
210    let mut clauses = Vec::new();
211    let mut index = BTreeMap::new();
212
213    if num_active_vars < 3 {
214        return (clauses, index);
215    }
216
217    for i in 1..=num_active_vars - 2 {
218        for j in i + 1..=num_active_vars - 1 {
219            for k in j + 1..=num_active_vars {
220                for s1 in [1i32, -1] {
221                    for s2 in [1i32, -1] {
222                        for s3 in [1i32, -1] {
223                            let mut clause = vec![s1 * i as i32, s2 * j as i32, s3 * k as i32];
224                            clause.sort_unstable();
225                            index.insert(clause.clone(), clauses.len() + 1);
226                            clauses.push(clause);
227                        }
228                    }
229                }
230            }
231        }
232    }
233
234    (clauses, index)
235}
236
237fn pow8_table(max_power: usize) -> Vec<BigUint> {
238    let mut table = Vec::with_capacity(max_power + 1);
239    table.push(BigUint::one());
240    for _ in 0..max_power {
241        let next = table.last().expect("pow8 table is nonempty") * BigUint::from(8u32);
242        table.push(next);
243    }
244    table
245}
246
247fn build_construction(source: &KSatisfiability<K3>) -> MandersAdlemanConstruction {
248    let (normalized_clauses, active_vars) = preprocess_formula(source);
249    let active_var_count = active_vars.len();
250    let var_map: BTreeMap<usize, usize> = active_vars
251        .iter()
252        .enumerate()
253        .map(|(new_index, &old_index)| (old_index, new_index + 1))
254        .collect();
255
256    let remapped_clauses = normalized_clauses
257        .iter()
258        .map(|clause| {
259            let mut remapped = clause
260                .iter()
261                .map(|&lit| {
262                    let var = lit.unsigned_abs() as usize;
263                    let new_var = *var_map
264                        .get(&var)
265                        .expect("active variable must be present in the remapping");
266                    if lit > 0 {
267                        new_var as i32
268                    } else {
269                        -(new_var as i32)
270                    }
271                })
272                .collect::<Vec<_>>();
273            remapped.sort_unstable();
274            remapped
275        })
276        .collect::<Vec<_>>();
277    let remapped_clause_set = remapped_clauses.iter().cloned().collect::<BTreeSet<_>>();
278
279    let (standard_clauses, standard_index) = build_standard_clauses(active_var_count);
280    let standard_clause_count = standard_clauses.len();
281    let aux_dimension = 2 * standard_clause_count + active_var_count;
282    let pow8 = pow8_table(standard_clause_count + 1);
283
284    let mut tau_phi = BigInt::zero();
285    for clause in &remapped_clauses {
286        if let Some(&j) = standard_index.get(clause) {
287            tau_phi -= BigInt::from(pow8[j].clone());
288        }
289    }
290
291    let mut positive_occurrences = vec![BigInt::zero(); active_var_count + 1];
292    let mut negative_occurrences = vec![BigInt::zero(); active_var_count + 1];
293    for (j, clause) in standard_clauses.iter().enumerate() {
294        let weight = BigInt::from(pow8[j + 1].clone());
295        for &lit in clause {
296            let var = lit.unsigned_abs() as usize;
297            if lit > 0 {
298                positive_occurrences[var] += &weight;
299            } else {
300                negative_occurrences[var] += &weight;
301            }
302        }
303    }
304
305    let mut doubled_coefficients = vec![BigInt::zero(); aux_dimension + 1];
306    doubled_coefficients[0] = BigInt::from(2u32);
307    for k in 1..=standard_clause_count {
308        let weight = BigInt::from(pow8[k].clone());
309        doubled_coefficients[2 * k - 1] = -weight.clone();
310        doubled_coefficients[2 * k] = -(weight * BigInt::from(2u32));
311    }
312    for i in 1..=active_var_count {
313        doubled_coefficients[2 * standard_clause_count + i] =
314            &positive_occurrences[i] - &negative_occurrences[i];
315    }
316
317    let sum_coefficients = doubled_coefficients
318        .iter()
319        .cloned()
320        .fold(BigInt::zero(), |acc, value| acc + value);
321    let sum_negative_occurrences = negative_occurrences
322        .iter()
323        .skip(1)
324        .cloned()
325        .fold(BigInt::zero(), |acc, value| acc + value);
326    let tau_2 = BigInt::from(2u32) * tau_phi
327        + sum_coefficients
328        + BigInt::from(2u32) * sum_negative_occurrences;
329    let mod_val = BigUint::from(2u32) * pow8[standard_clause_count + 1].clone();
330
331    let primes = admissible_primes(aux_dimension + 1);
332    let prime_powers = primes
333        .iter()
334        .map(|&prime| pow_biguint_u64(prime, aux_dimension + 1))
335        .collect::<Vec<_>>();
336    let k = prime_powers
337        .iter()
338        .cloned()
339        .fold(BigUint::one(), |acc, value| acc * value);
340
341    let mut thetas = Vec::with_capacity(aux_dimension + 1);
342    for j in 0..=aux_dimension {
343        let other = &k / &prime_powers[j];
344        let lcm = &other * &mod_val;
345        let residue = bigint_mod_to_biguint(&doubled_coefficients[j], &mod_val);
346        let inverse = modular_inverse(&(other.clone() % &mod_val), &mod_val);
347        let mut theta = (&other * ((&residue * inverse) % &mod_val)) % &lcm;
348        if theta.is_zero() {
349            theta = lcm.clone();
350        }
351        let prime = BigUint::from(primes[j]);
352        while (&theta % &prime).is_zero() {
353            theta += &lcm;
354        }
355        thetas.push(theta);
356    }
357
358    let h = thetas
359        .iter()
360        .cloned()
361        .fold(BigUint::zero(), |acc, value| acc + value);
362    let beta = &mod_val * &k;
363    let inverse_factor = &mod_val + &k;
364    let inverse = modular_inverse(&(inverse_factor % &beta), &beta);
365    let tau_2_squared = (&tau_2 * &tau_2)
366        .to_biguint()
367        .expect("squared doubled target must be nonnegative");
368    let alpha = (&inverse * ((&k * tau_2_squared) + (&mod_val * (&h * &h)))) % &beta;
369    let target = QuadraticCongruences::new(alpha, beta, &h + BigUint::one());
370
371    MandersAdlemanConstruction {
372        target,
373        source_num_vars: source.num_vars(),
374        active_to_source: active_vars.into_iter().map(|var| var - 1).collect(),
375        remapped_clause_set,
376        standard_clauses,
377        standard_clause_count,
378        active_var_count,
379        doubled_coefficients,
380        tau_2,
381        thetas,
382        h,
383        prime_powers,
384    }
385}
386
387#[cfg(any(test, feature = "example-db"))]
388fn build_alphas(
389    construction: &MandersAdlemanConstruction,
390    assignment: &[usize],
391) -> Option<Vec<i8>> {
392    if assignment.len() != construction.source_num_vars {
393        return None;
394    }
395
396    let mut active_assignment = vec![0i8; construction.active_var_count + 1];
397    for (active_index, &source_index) in construction.active_to_source.iter().enumerate() {
398        active_assignment[active_index + 1] = if assignment[source_index] == 0 { 0 } else { 1 };
399    }
400
401    let mut alphas =
402        vec![0i8; 2 * construction.standard_clause_count + construction.active_var_count + 1];
403    alphas[0] = 1;
404
405    for i in 1..=construction.active_var_count {
406        alphas[2 * construction.standard_clause_count + i] = 1 - 2 * active_assignment[i];
407    }
408
409    for k in 1..=construction.standard_clause_count {
410        let clause = &construction.standard_clauses[k - 1];
411        let mut y = 0i32;
412        for &lit in clause {
413            let var = lit.unsigned_abs() as usize;
414            if lit > 0 {
415                y += i32::from(active_assignment[var]);
416            } else {
417                y += 1 - i32::from(active_assignment[var]);
418            }
419        }
420        if construction.remapped_clause_set.contains(clause) {
421            y -= 1;
422        }
423
424        match 3 - 2 * y {
425            3 => {
426                alphas[2 * k - 1] = 1;
427                alphas[2 * k] = 1;
428            }
429            1 => {
430                alphas[2 * k - 1] = -1;
431                alphas[2 * k] = 1;
432            }
433            -1 => {
434                alphas[2 * k - 1] = 1;
435                alphas[2 * k] = -1;
436            }
437            -3 => {
438                alphas[2 * k - 1] = -1;
439                alphas[2 * k] = -1;
440            }
441            _ => return None,
442        }
443    }
444
445    Some(alphas)
446}
447
448#[cfg(any(test, feature = "example-db"))]
449fn witness_value_from_alphas(alphas: &[i8], thetas: &[BigUint]) -> BigUint {
450    let signed_sum = alphas
451        .iter()
452        .zip(thetas)
453        .fold(BigInt::zero(), |acc, (&alpha, theta)| {
454            if alpha == 1 {
455                acc + BigInt::from(theta.clone())
456            } else {
457                acc - BigInt::from(theta.clone())
458            }
459        });
460    signed_sum
461        .abs()
462        .to_biguint()
463        .expect("absolute witness must be nonnegative")
464}
465
466#[cfg(any(test, feature = "example-db"))]
467fn witness_config_for_assignment(
468    source: &KSatisfiability<K3>,
469    assignment: &[usize],
470) -> Option<Vec<usize>> {
471    let construction = build_construction(source);
472    let alphas = build_alphas(&construction, assignment)?;
473    let witness = witness_value_from_alphas(&alphas, &construction.thetas);
474    construction.target.encode_witness(&witness)
475}
476
477#[cfg(test)]
478fn exhaustive_alpha_solution(source: &KSatisfiability<K3>) -> Option<Vec<i8>> {
479    let construction = build_construction(source);
480    let n = construction.doubled_coefficients.len();
481    if n >= usize::BITS as usize {
482        return None;
483    }
484
485    for bits in 0usize..(1usize << n) {
486        let alphas = (0..n)
487            .map(|j| if (bits >> j) & 1 == 1 { 1i8 } else { -1i8 })
488            .collect::<Vec<_>>();
489        let sum = construction
490            .doubled_coefficients
491            .iter()
492            .zip(&alphas)
493            .fold(BigInt::zero(), |acc, (coefficient, &alpha)| {
494                acc + coefficient * BigInt::from(alpha)
495            });
496        if sum == construction.tau_2 {
497            return Some(alphas);
498        }
499    }
500
501    None
502}
503
504#[reduction(overhead = {
505    bit_length_a = "(num_vars + num_clauses)^2 * log(num_vars + num_clauses + 1)",
506    bit_length_b = "(num_vars + num_clauses)^2 * log(num_vars + num_clauses + 1)",
507    bit_length_c = "(num_vars + num_clauses)^2 * log(num_vars + num_clauses + 1)",
508})]
509impl ReduceTo<QuadraticCongruences> for KSatisfiability<K3> {
510    type Result = Reduction3SATToQuadraticCongruences;
511
512    fn reduce_to(&self) -> Self::Result {
513        let construction = build_construction(self);
514        Reduction3SATToQuadraticCongruences {
515            target: construction.target,
516            source_num_vars: construction.source_num_vars,
517            active_to_source: construction.active_to_source,
518            standard_clause_count: construction.standard_clause_count,
519            h: construction.h,
520            prime_powers: construction.prime_powers,
521        }
522    }
523}
524
525#[cfg(feature = "example-db")]
526pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::RuleExampleSpec> {
527    use crate::export::SolutionPair;
528
529    vec![crate::example_db::specs::RuleExampleSpec {
530        id: "ksatisfiability_to_quadraticcongruences",
531        build: || {
532            let source = KSatisfiability::<K3>::new(
533                3,
534                vec![crate::models::formula::CNFClause::new(vec![1, 2, 3])],
535            );
536            let target_config = witness_config_for_assignment(&source, &[1, 0, 0])
537                .expect("canonical satisfying assignment should lift to a QC witness");
538            crate::example_db::specs::rule_example_with_witness::<_, QuadraticCongruences>(
539                source,
540                SolutionPair {
541                    source_config: vec![1, 0, 0],
542                    target_config,
543                },
544            )
545        },
546    }]
547}
548
549#[cfg(test)]
550#[path = "../unit_tests/rules/ksatisfiability_quadraticcongruences.rs"]
551mod tests;