1use ocas_domain::number_theory::{crt::crt_many, primes_from, symmetric_mod};
12use ocas_domain::{Domain, EuclideanDomain, FiniteField, Integer, IntegerDomain};
13
14use crate::dense::DenseUnivariatePolynomial;
15use crate::factor::finite_field::FpPoly;
16
17pub type ZPoly = DenseUnivariatePolynomial<IntegerDomain>;
19
20const MAX_PRIMES: usize = 10_000;
24
25fn reduce_mod_field(p: &ZPoly, field: &FiniteField) -> FpPoly {
27 let coeffs = p
28 .coeffs()
29 .iter()
30 .map(|c| field.element(c.to_bigint()))
31 .collect();
32 FpPoly::from_coeffs(field.clone(), coeffs)
33}
34
35fn div_exact_z(dividend: &ZPoly, divisor: &ZPoly) -> Option<ZPoly> {
39 if divisor.is_zero() {
40 return None;
41 }
42 let dom = IntegerDomain;
43 if dividend.is_zero() {
44 return Some(ZPoly::new(dom));
45 }
46 let div_deg = divisor.degree()?;
47 let div_lc = divisor.leading_coeff()?.clone();
48 let mut remainder = dividend.clone();
49 let mut qcoeffs: Vec<Integer> = Vec::new();
50 while let Some(deg) = remainder.degree() {
51 if deg < div_deg {
52 break;
53 }
54 let lc = remainder.leading_coeff()?.clone();
55 let q = dom.div(&lc, &div_lc)?;
57 let t = deg - div_deg;
58 if qcoeffs.len() <= t {
59 qcoeffs.resize(t + 1, Integer::from(0));
60 }
61 qcoeffs[t] = q.clone();
62 let mut sub_coeffs = vec![Integer::from(0); t];
63 sub_coeffs.extend(divisor.coeffs().iter().map(|c| &q * c));
64 remainder = remainder.sub(&ZPoly::from_coeffs(dom, sub_coeffs));
65 }
66 if remainder.is_zero() {
67 Some(ZPoly::from_coeffs(dom, qcoeffs))
68 } else {
69 None
70 }
71}
72
73fn reconstruct(images: &[(Integer, FpPoly)], deg: usize) -> Option<ZPoly> {
76 let mut coeffs = Vec::with_capacity(deg + 1);
77 for i in 0..=deg {
78 let cs: Vec<(Integer, Integer)> = images
79 .iter()
80 .map(|(p, g)| {
81 let c = g
82 .coeff(i)
83 .map(|e| Integer::from(e.value().clone()))
84 .unwrap_or_else(|| Integer::from(0));
85 (c, p.clone())
86 })
87 .collect();
88 let (r, m) = crt_many(&cs)?;
89 coeffs.push(symmetric_mod(&r, &m));
90 }
91 Some(ZPoly::from_coeffs(IntegerDomain, coeffs))
92}
93
94pub fn gcd_modular_z(a: &ZPoly, b: &ZPoly) -> ZPoly {
117 let dom = IntegerDomain;
118 if a.is_zero() {
119 return b.primitive_part();
120 }
121 if b.is_zero() {
122 return a.primitive_part();
123 }
124 let ap = a.primitive_part();
125 let bp = b.primitive_part();
126 let gamma = dom.gcd(
129 ap.leading_coeff().expect("nonzero polynomial"),
130 bp.leading_coeff().expect("nonzero polynomial"),
131 );
132
133 let mut best_deg: Option<usize> = None;
134 let mut images: Vec<(Integer, FpPoly)> = Vec::new();
135 let mut prime_iter = primes_from(&Integer::from(1_073_741_824)); for _ in 0..MAX_PRIMES {
137 let p = prime_iter.next().expect("primes are inexhaustible");
138 if gamma.mod_floor(&p).is_zero() {
139 continue;
140 }
141 let field = FiniteField::new(p.to_bigint());
142 let fa = reduce_mod_field(&ap, &field);
143 let fb = reduce_mod_field(&bp, &field);
144 let g = fa.gcd(&fb);
145 let Some(deg) = g.degree() else {
146 continue; };
148 let lc = g.leading_coeff().expect("nonzero gcd").clone();
150 let inv_lc = field.inv(&lc).expect("field element invertible");
151 let gamma_p = field.element(gamma.to_bigint());
152 let scale = field.mul(&inv_lc, &gamma_p);
153 let g_scaled = g.mul_scalar(&scale);
154
155 match best_deg {
156 None => {
157 best_deg = Some(deg);
158 images.push((p, g_scaled));
159 }
160 Some(bd) if deg < bd => {
161 best_deg = Some(deg);
163 images.clear();
164 images.push((p, g_scaled));
165 }
166 Some(bd) if deg == bd => images.push((p, g_scaled)),
167 _ => continue, }
169 let deg = best_deg.expect("set above");
170 if deg == 0 {
171 return ZPoly::from_coeffs(dom, vec![Integer::from(1)]);
173 }
174 if let Some(candidate) = reconstruct(&images, deg) {
176 let cand = candidate.primitive_part();
177 if cand.degree() == Some(deg)
178 && div_exact_z(&ap, &cand).is_some()
179 && div_exact_z(&bp, &cand).is_some()
180 {
181 return cand;
182 }
183 }
184 }
185 a.gcd(b)
187}
188
189#[cfg(test)]
190mod tests {
191 use super::*;
192
193 fn i(v: i64) -> Integer {
194 Integer::from(v)
195 }
196
197 fn zpoly(coeffs: &[i64]) -> ZPoly {
198 ZPoly::from_coeffs(IntegerDomain, coeffs.iter().map(|&v| i(v)).collect())
199 }
200
201 fn rand_poly(deg: usize, bound: i64, seed: &mut u64) -> ZPoly {
204 let mut coeffs = Vec::with_capacity(deg + 1);
205 for _ in 0..=deg {
206 *seed = seed
207 .wrapping_mul(6364136223846793005)
208 .wrapping_add(1442695040888963407);
209 let v = ((*seed >> 33) as i64) % (2 * bound) - bound;
210 coeffs.push(i(v));
211 }
212 if coeffs[deg].is_zero() {
214 coeffs[deg] = i(1);
215 }
216 ZPoly::from_coeffs(IntegerDomain, coeffs)
217 }
218
219 fn monic_sign(p: &ZPoly) -> ZPoly {
221 if p.lcoeff().is_negative() {
222 p.neg()
223 } else {
224 p.clone()
225 }
226 }
227
228 #[test]
229 fn small_cases_match_prs() {
230 let a = zpoly(&[-1, 0, 1]);
231 let b = zpoly(&[1, 2, 1]);
232 let g = gcd_modular_z(&a, &b);
233 assert_eq!(g.coeffs(), &[i(1), i(1)]);
234
235 let g2 = gcd_modular_z(&zpoly(&[-1, 0, 1]), &zpoly(&[1, 1]));
236 assert_eq!(g2.coeffs(), &[i(1), i(1)]);
237
238 let g3 = gcd_modular_z(&zpoly(&[1, 1]), &zpoly(&[2, 1]));
240 assert_eq!(g3.degree(), Some(0));
241
242 let g4 = gcd_modular_z(&a, &a.zero());
244 assert_eq!(g4.coeffs(), a.primitive_part().coeffs());
245
246 let g5 = gcd_modular_z(&zpoly(&[2, 2]), &zpoly(&[4, 4]));
248 assert_eq!(g5.coeffs(), &[i(1), i(1)]);
249 }
250
251 #[test]
252 fn constructed_common_factor() {
253 let g = zpoly(&[10, 6, 5, 3]);
255 let a = g.mul(&zpoly(&[-1, 2])); let b = g.mul(&zpoly(&[7, 1])); let got = gcd_modular_z(&a, &b);
258 assert_eq!(got, monic_sign(&g).primitive_part());
259 }
260
261 #[test]
262 fn gcd_is_common_divisor_and_primitive() {
263 let mut seed = 42u64;
264 for _ in 0..20 {
265 let g = rand_poly(4, 10, &mut seed).primitive_part();
266 let a = g.mul(&rand_poly(3, 10, &mut seed));
267 let b = g.mul(&rand_poly(5, 10, &mut seed));
268 let got = gcd_modular_z(&a, &b);
269 assert!(div_exact_z(&a, &got).is_some(), "gcd must divide a");
270 assert!(div_exact_z(&b, &got).is_some(), "gcd must divide b");
271 assert!(got.content().is_one(), "gcd must be primitive");
272 assert!(div_exact_z(&got, &g).is_some(), "gcd must contain g");
273 }
274 }
275
276 #[test]
277 fn consistency_with_prs_on_small_polys() {
278 let mut seed = 7u64;
279 for _ in 0..30 {
280 let a = rand_poly(6, 8, &mut seed);
281 let b = rand_poly(5, 8, &mut seed);
282 let got = monic_sign(&gcd_modular_z(&a, &b));
283 let want = monic_sign(&a.gcd(&b));
284 assert_eq!(got, want, "a={a:?} b={b:?}");
285 }
286 }
287
288 #[test]
289 fn big_coefficients_no_explosion() {
290 let mut seed = 99u64;
293 let mut big = rand_poly(12, 1_000_000, &mut seed);
294 big = big.mul(&big);
296 let a = big.mul(&rand_poly(6, 100, &mut seed));
297 let b = big.mul(&rand_poly(8, 100, &mut seed));
298 let got = gcd_modular_z(&a, &b);
299 assert!(div_exact_z(&a, &got).is_some());
300 assert!(div_exact_z(&b, &got).is_some());
301 assert_eq!(got.degree(), big.primitive_part().degree());
302 }
303
304 #[test]
307 #[ignore = "performance acceptance: run with --release --ignored"]
308 fn modular_gcd_degree_50_100_digit_coeffs() {
309 fn big_rand(digits: usize, seed: &mut u64) -> Integer {
310 let mut s = String::from("9");
311 for _ in 0..digits {
312 *seed = seed
313 .wrapping_mul(6364136223846793005)
314 .wrapping_add(1442695040888963407);
315 s.push((b'0' + ((*seed >> 33) % 10) as u8) as char);
316 }
317 Integer::from(s.parse::<num_bigint::BigInt>().unwrap())
318 }
319 let mut seed = 12345u64;
320 let mk = |deg: usize, seed: &mut u64| {
321 let coeffs: Vec<Integer> = (0..=deg).map(|_| big_rand(50, seed)).collect();
322 ZPoly::from_coeffs(IntegerDomain, coeffs)
323 };
324 let g = mk(25, &mut seed).primitive_part();
325 let r1 = mk(25, &mut seed);
326 let r2 = mk(25, &mut seed);
327 let a = g.mul(&r1);
328 let b = g.mul(&r2);
329 assert_eq!(a.degree(), Some(50));
330 let start = std::time::Instant::now();
331 let got = gcd_modular_z(&a, &b);
332 let elapsed = start.elapsed();
333 eprintln!("deg-50 / 100-digit modular gcd took {elapsed:?}");
334 assert!(div_exact_z(&a, &got).is_some(), "gcd must divide a");
335 assert!(div_exact_z(&b, &got).is_some(), "gcd must divide b");
336 assert_eq!(got.degree(), g.degree());
337 assert!(
338 elapsed.as_secs() < 120,
339 "modular gcd took {elapsed:?} (soft limit 120s)"
340 );
341 }
342}