oxiz_math/polynomial/gcd.rs
1//! Polynomial GCD Algorithms.
2//!
3//! This module implements efficient algorithms for computing the greatest
4//! common divisor (GCD) of multivariate polynomials.
5//!
6//! ## Algorithms
7//!
8//! 1. **Euclidean Algorithm**: Classic algorithm for univariate polynomials
9//! 2. **Subresultant PRS**: Polynomial remainder sequences avoiding coefficient growth
10//! 3. **Modular GCD**: Compute GCD mod p, then lift to full precision
11//! 4. **Sparse GCD**: Optimized for sparse polynomials
12//!
13//! ## Applications
14//!
15//! - Simplification of rational functions
16//! - Factorization preprocessing
17//! - Solving polynomial systems
18//! - Gröbner basis computation
19//!
20//! ## References
21//!
22//! - Knuth: "The Art of Computer Programming Vol. 2" (GCD algorithms)
23//! - Geddes et al.: "Algorithms for Computer Algebra" (1992)
24//! - Z3's `math/polynomial/polynomial_gcd.cpp`
25
26use super::{NULL_VAR, Polynomial, Var};
27#[allow(unused_imports)]
28use crate::prelude::*;
29use num_rational::BigRational;
30use num_traits::{One, Zero};
31
32/// Combine two "maximum variable" values (as returned by
33/// [`Polynomial::max_var`], where [`NULL_VAR`] means "no variables /
34/// constant") the way a remainder/division computation needs: the shared
35/// reduction variable is the higher-indexed *actual* variable appearing in
36/// either operand, and is `NULL_VAR` only when *both* operands are
37/// constant.
38///
39/// A naive `a.max(b)` is wrong here: `NULL_VAR == u32::MAX`, so it would
40/// always "win" over any real variable index, e.g. `combined_var(0,
41/// NULL_VAR)` would incorrectly report "no shared variable" even though one
42/// operand has variable 0. (`Polynomial::gcd_univariate`'s use of a plain
43/// `.max()` is safe only because *its* early-return for `NULL_VAR` --
44/// "either operand is a nonzero constant, so GCD is 1" -- happens to be
45/// correct regardless of which operand was constant; a remainder/quotient
46/// computation is not symmetric like that, so it needs this precise
47/// version.)
48fn combined_var(a: Var, b: Var) -> Var {
49 match (a, b) {
50 (NULL_VAR, NULL_VAR) => NULL_VAR,
51 (NULL_VAR, v) | (v, NULL_VAR) => v,
52 (va, vb) => va.max(vb),
53 }
54}
55
56/// Configuration for GCD computation.
57#[derive(Debug, Clone)]
58pub struct GcdConfig {
59 /// Use modular GCD algorithm.
60 pub use_modular: bool,
61 /// Use subresultant PRS.
62 pub use_subresultant: bool,
63 /// Modulus for modular algorithm.
64 pub modulus: u64,
65 /// Maximum degree to attempt.
66 pub max_degree: u32,
67}
68
69impl Default for GcdConfig {
70 fn default() -> Self {
71 Self {
72 use_modular: true,
73 use_subresultant: true,
74 modulus: 2147483647, // Large prime
75 max_degree: 1000,
76 }
77 }
78}
79
80/// Statistics for GCD computation.
81#[derive(Debug, Clone, Default)]
82pub struct GcdStats {
83 /// GCDs computed.
84 pub gcds_computed: u64,
85 /// Euclidean steps performed.
86 pub euclidean_steps: u64,
87 /// Modular reductions performed.
88 pub modular_reductions: u64,
89 /// Time (microseconds).
90 pub time_us: u64,
91}
92
93/// Polynomial GCD engine.
94pub struct PolynomialGcd {
95 config: GcdConfig,
96 stats: GcdStats,
97}
98
99impl PolynomialGcd {
100 /// Create new GCD engine.
101 pub fn new() -> Self {
102 Self::with_config(GcdConfig::default())
103 }
104
105 /// Create with configuration.
106 pub fn with_config(config: GcdConfig) -> Self {
107 Self {
108 config,
109 stats: GcdStats::default(),
110 }
111 }
112
113 /// Get statistics.
114 pub fn stats(&self) -> &GcdStats {
115 &self.stats
116 }
117
118 /// Compute GCD of two polynomials.
119 pub fn gcd(&mut self, a: &Polynomial, b: &Polynomial) -> Polynomial {
120 #[cfg(feature = "std")]
121 let start = std::time::Instant::now();
122
123 // Handle special cases
124 if a.is_zero() {
125 self.stats.gcds_computed += 1;
126 return b.clone();
127 }
128 if b.is_zero() {
129 self.stats.gcds_computed += 1;
130 return a.clone();
131 }
132
133 // Check degrees
134 if a.total_degree() > self.config.max_degree || b.total_degree() > self.config.max_degree {
135 // Too large - return trivial GCD
136 self.stats.gcds_computed += 1;
137 return Polynomial::constant(BigRational::one());
138 }
139
140 // Use Euclidean algorithm for simple cases
141 let result = if a.total_degree() < 10 && b.total_degree() < 10 {
142 self.euclidean_gcd(a, b)
143 } else if self.config.use_modular {
144 self.modular_gcd(a, b)
145 } else {
146 self.euclidean_gcd(a, b)
147 };
148
149 self.stats.gcds_computed += 1;
150 #[cfg(feature = "std")]
151 {
152 self.stats.time_us += start.elapsed().as_micros() as u64;
153 }
154
155 result
156 }
157
158 /// Euclidean algorithm for polynomial GCD.
159 ///
160 /// Based on repeated polynomial division until remainder is zero.
161 fn euclidean_gcd(&mut self, a: &Polynomial, b: &Polynomial) -> Polynomial {
162 let mut r0 = a.clone();
163 let mut r1 = b.clone();
164
165 // Bound iterations for safety. For genuinely univariate inputs each
166 // step strictly reduces the degree in the shared variable, so this
167 // bound is never the limiting factor there. For inputs that mix
168 // multiple distinct variables -- where `polynomial_remainder`'s
169 // "univariate in one designated variable" approach (see its docs)
170 // cannot always make progress -- the sequence can settle into a
171 // fixed 2-cycle instead of shrinking toward zero; this bound
172 // guarantees the loop still terminates rather than hanging.
173 let max_iters = a.total_degree() as usize + b.total_degree() as usize + 16;
174 let mut iters = 0;
175
176 while !r1.is_zero() && iters < max_iters {
177 iters += 1;
178 self.stats.euclidean_steps += 1;
179
180 // Compute r0 mod r1 (polynomial remainder)
181 let remainder = self.polynomial_remainder(&r0, &r1);
182
183 r0 = r1;
184 r1 = remainder;
185 }
186
187 // Normalize by making leading coefficient positive
188 self.normalize_polynomial(r0)
189 }
190
191 /// Modular GCD algorithm.
192 ///
193 /// 1. Compute GCD mod p for a prime p
194 /// 2. Lift to full precision
195 fn modular_gcd(&mut self, a: &Polynomial, b: &Polynomial) -> Polynomial {
196 self.stats.modular_reductions += 1;
197
198 // Simplified implementation - full version would:
199 // 1. Reduce polynomials modulo p
200 // 2. Compute GCD in Z_p[x]
201 // 3. Lift using Hensel lifting or Chinese remainder theorem
202
203 // For now, fall back to Euclidean
204 self.euclidean_gcd(a, b)
205 }
206
207 /// Compute polynomial remainder: a mod b (exact rational polynomial long
208 /// division, not the placeholder that used to always return zero).
209 ///
210 /// Division by the zero polynomial is mathematically undefined; rather
211 /// than panicking on a crafted degenerate `b` (see finding R3), this
212 /// treats it as "no reduction possible" and returns `a` unchanged.
213 ///
214 /// # Multivariate limitation
215 ///
216 /// This performs *univariate* long division in the highest-indexed
217 /// variable appearing in either operand, reusing
218 /// [`Polynomial::exact_div_rem_univariate`]. It is exact when `a` and
219 /// `b` are both univariate in that variable. For genuinely multivariate
220 /// polynomials (terms mixing two or more distinct variables), the
221 /// result is not guaranteed to be the true multivariate remainder --
222 /// see [`Polynomial::exact_div_rem_univariate`]'s docs for why. Callers
223 /// that need an exact n-variate remainder should use
224 /// [`super::gcd_multivariate`] / [`super::gcd_multivariate_advanced`]
225 /// instead.
226 fn polynomial_remainder(&self, a: &Polynomial, b: &Polynomial) -> Polynomial {
227 if b.is_zero() {
228 return a.clone();
229 }
230 if a.is_zero() {
231 return Polynomial::zero();
232 }
233
234 let var = combined_var(a.max_var(), b.max_var());
235 if var == NULL_VAR {
236 // Both operands are non-zero constants: over a field, a mod b
237 // is always exactly 0.
238 return Polynomial::zero();
239 }
240
241 a.exact_div_rem_univariate(b, var).1
242 }
243
244 /// Normalize polynomial (make leading coefficient = 1, i.e. monic).
245 fn normalize_polynomial(&self, poly: Polynomial) -> Polynomial {
246 if poly.is_zero() {
247 return poly;
248 }
249
250 let leading_coeff = poly.leading_coeff();
251 if leading_coeff.is_one() || leading_coeff.is_zero() {
252 return poly;
253 }
254
255 // Divide every coefficient by the leading coefficient (exact over
256 // BigRational) so the result is monic, the standard GCD
257 // normalization convention.
258 poly.scale(&leading_coeff.recip())
259 }
260
261 /// Compute GCD of multiple polynomials.
262 pub fn gcd_multiple(&mut self, polys: &[Polynomial]) -> Polynomial {
263 if polys.is_empty() {
264 return Polynomial::zero();
265 }
266
267 let mut result = polys[0].clone();
268
269 for poly in &polys[1..] {
270 result = self.gcd(&result, poly);
271
272 // Early termination if GCD becomes 1
273 if result.total_degree() == 0 && !result.is_zero() {
274 break;
275 }
276 }
277
278 result
279 }
280
281 /// Compute LCM (least common multiple) of two polynomials.
282 ///
283 /// LCM(a,b) = (a * b) / GCD(a,b), computed via exact polynomial
284 /// division (previously this omitted the division entirely and
285 /// returned the plain product `a * b`).
286 pub fn lcm(&mut self, a: &Polynomial, b: &Polynomial) -> Polynomial {
287 if a.is_zero() || b.is_zero() {
288 return Polynomial::zero();
289 }
290
291 let gcd = self.gcd(a, b);
292
293 if gcd.is_zero() {
294 return Polynomial::zero();
295 }
296
297 let product = a.mul(b);
298 let var = combined_var(product.max_var(), gcd.max_var());
299 if var == NULL_VAR {
300 // Both product and gcd are constants: LCM of nonzero constants
301 // is conventionally 1 (any nonzero constant divides any other).
302 return Polynomial::one();
303 }
304
305 let (quotient, _remainder) = product.exact_div_rem_univariate(&gcd, var);
306 quotient
307 }
308}
309
310impl Default for PolynomialGcd {
311 fn default() -> Self {
312 Self::new()
313 }
314}
315
316#[cfg(test)]
317mod tests {
318 use super::*;
319 use num_bigint::BigInt;
320 use num_rational::BigRational;
321
322 #[test]
323 fn test_gcd_creation() {
324 let gcd_engine = PolynomialGcd::new();
325 assert_eq!(gcd_engine.stats().gcds_computed, 0);
326 }
327
328 #[test]
329 fn test_gcd_zero() {
330 let mut gcd_engine = PolynomialGcd::new();
331
332 let a = Polynomial::constant(BigRational::from(BigInt::from(42)));
333 let b = Polynomial::zero();
334
335 let result = gcd_engine.gcd(&a, &b);
336
337 // GCD of a polynomial with zero is the non-zero polynomial
338 assert!(!result.is_zero());
339 assert_eq!(gcd_engine.stats().gcds_computed, 1);
340 }
341
342 #[test]
343 fn test_gcd_constants() {
344 let mut gcd_engine = PolynomialGcd::new();
345
346 let a = Polynomial::constant(BigRational::from(BigInt::from(12)));
347 let b = Polynomial::constant(BigRational::from(BigInt::from(18)));
348
349 let result = gcd_engine.gcd(&a, &b);
350
351 assert!(!result.is_zero());
352 }
353
354 #[test]
355 fn test_gcd_config() {
356 let config = GcdConfig {
357 use_modular: false,
358 use_subresultant: false,
359 ..Default::default()
360 };
361
362 let gcd_engine = PolynomialGcd::with_config(config);
363 assert!(!gcd_engine.config.use_modular);
364 }
365
366 #[test]
367 fn test_gcd_multiple() {
368 let mut gcd_engine = PolynomialGcd::new();
369
370 let polys = vec![
371 Polynomial::constant(BigRational::from(BigInt::from(12))),
372 Polynomial::constant(BigRational::from(BigInt::from(18))),
373 Polynomial::constant(BigRational::from(BigInt::from(24))),
374 ];
375
376 let result = gcd_engine.gcd_multiple(&polys);
377
378 assert!(!result.is_zero());
379 }
380
381 #[test]
382 fn test_lcm() {
383 let mut gcd_engine = PolynomialGcd::new();
384
385 let a = Polynomial::constant(BigRational::from(BigInt::from(4)));
386 let b = Polynomial::constant(BigRational::from(BigInt::from(6)));
387
388 let result = gcd_engine.lcm(&a, &b);
389
390 assert!(!result.is_zero());
391 }
392
393 // -- Regression tests for MATH-2 (PolynomialGcd was a broken stub) --
394
395 /// Build the univariate polynomial `x^2` (variable 0).
396 fn poly_x_squared() -> Polynomial {
397 Polynomial::from_coeffs_int(&[(1, &[(0, 2)])])
398 }
399
400 /// Build the univariate polynomial `x + 1` (variable 0).
401 fn poly_x_plus_1() -> Polynomial {
402 Polynomial::from_coeffs_int(&[(1, &[(0, 1)]), (1, &[])])
403 }
404
405 /// Build the univariate polynomial `x - 1` (variable 0).
406 fn poly_x_minus_1() -> Polynomial {
407 Polynomial::from_coeffs_int(&[(1, &[(0, 1)]), (-1, &[])])
408 }
409
410 #[test]
411 fn test_polynomial_remainder_is_real_division_not_stub() {
412 // Regression test for MATH-2: `polynomial_remainder` used to
413 // unconditionally return `Polynomial::zero()` whenever
414 // `deg(a) >= deg(b)`. x^2 mod (x+1) is the constant 1 (long
415 // division: x^2 = (x-1)(x+1) + 1), never 0.
416 let gcd_engine = PolynomialGcd::new();
417 let a = poly_x_squared();
418 let b = poly_x_plus_1();
419
420 let remainder = gcd_engine.polynomial_remainder(&a, &b);
421
422 assert!(!remainder.is_zero(), "stub would wrongly return zero here");
423 assert!(remainder.is_constant());
424 assert_eq!(remainder.constant_value(), BigRational::one());
425 }
426
427 #[test]
428 fn test_gcd_coprime_polynomials_is_a_unit() {
429 // Regression test for MATH-2: gcd(x^2, x+1) must be a nonzero
430 // constant (they are coprime). The old stub's broken
431 // `polynomial_remainder` made `euclidean_gcd` terminate after one
432 // step and return `x+1` -- a non-unit -- as if it were the GCD.
433 let mut gcd_engine = PolynomialGcd::new();
434 let a = poly_x_squared();
435 let b = poly_x_plus_1();
436
437 let result = gcd_engine.gcd(&a, &b);
438
439 assert!(!result.is_zero());
440 assert_eq!(
441 result.total_degree(),
442 0,
443 "gcd of coprime polynomials must be a nonzero constant, got {result:?}"
444 );
445 }
446
447 #[test]
448 fn test_gcd_shared_linear_factor() {
449 // gcd((x-1)(x+1), (x-1)(x+2)) == (a unit multiple of) x-1.
450 let mut gcd_engine = PolynomialGcd::new();
451 let a = poly_x_minus_1().mul(&poly_x_plus_1()); // x^2 - 1
452 let b = poly_x_minus_1().mul(&Polynomial::from_coeffs_int(&[(1, &[(0, 1)]), (2, &[])])); // x^2 + x - 2
453
454 let result = gcd_engine.gcd(&a, &b);
455
456 assert_eq!(result.total_degree(), 1, "expected a linear common factor");
457 // The result must exactly divide both inputs (remainder zero).
458 assert!(gcd_engine.polynomial_remainder(&a, &result).is_zero());
459 assert!(gcd_engine.polynomial_remainder(&b, &result).is_zero());
460 // Normalization makes it monic: leading coefficient exactly 1.
461 assert_eq!(result.leading_coeff(), BigRational::one());
462 }
463
464 #[test]
465 fn test_normalize_polynomial_makes_monic() {
466 // Regression test for MATH-2: `normalize_polynomial`'s coefficient
467 // loop used to be an empty no-op, so `2x + 4` stayed `2x + 4`
468 // instead of becoming monic `x + 2`.
469 let gcd_engine = PolynomialGcd::new();
470 let poly = Polynomial::from_coeffs_int(&[(2, &[(0, 1)]), (4, &[])]); // 2x + 4
471
472 let normalized = gcd_engine.normalize_polynomial(poly);
473
474 assert_eq!(normalized.leading_coeff(), BigRational::one());
475 assert_eq!(
476 normalized,
477 Polynomial::from_coeffs_int(&[(1, &[(0, 1)]), (2, &[])]) // x + 2
478 );
479 }
480
481 #[test]
482 fn test_normalize_polynomial_zero_and_already_monic_unchanged() {
483 let gcd_engine = PolynomialGcd::new();
484
485 let zero = Polynomial::zero();
486 assert!(gcd_engine.normalize_polynomial(zero).is_zero());
487
488 let monic = poly_x_plus_1();
489 assert_eq!(gcd_engine.normalize_polynomial(monic.clone()), monic);
490 }
491
492 #[test]
493 fn test_lcm_of_coprime_polynomials_equals_product() {
494 // Regression test for MATH-2: `lcm` used to just return `a * b`
495 // unconditionally (never dividing by the GCD). For coprime a, b
496 // this happens to coincide with the correct answer, so exercise it
497 // to confirm the *new* division-based implementation still gets
498 // the right answer here too.
499 let mut gcd_engine = PolynomialGcd::new();
500 let a = poly_x_minus_1(); // x - 1
501 let b = poly_x_plus_1(); // x + 1
502
503 let result = gcd_engine.lcm(&a, &b);
504
505 assert_eq!(
506 result,
507 Polynomial::from_coeffs_int(&[(1, &[(0, 2)]), (-1, &[])])
508 ); // x^2 - 1
509 }
510
511 #[test]
512 fn test_lcm_divides_product_by_true_gcd() {
513 // lcm(a, b) * gcd(a, b) must equal a * b up to a unit factor; check
514 // this via exact divisibility instead of a hard-coded polynomial,
515 // to catch a regression to the old "just multiply" stub for an
516 // input with a nontrivial common factor.
517 let mut gcd_engine = PolynomialGcd::new();
518 let a = poly_x_minus_1().mul(&poly_x_plus_1()); // x^2 - 1
519 let b = poly_x_minus_1().mul(&Polynomial::from_coeffs_int(&[(1, &[(0, 1)]), (2, &[])])); // x^2 + x - 2
520
521 let lcm = gcd_engine.lcm(&a, &b);
522 let product = a.mul(&b);
523
524 // lcm must be strictly smaller-degree than the naive product
525 // whenever a, b share a nontrivial factor (product has degree 4,
526 // lcm should have degree 3: (x-1)(x+1)(x+2)).
527 assert_eq!(lcm.total_degree(), 3);
528 assert!(gcd_engine.polynomial_remainder(&product, &lcm).is_zero());
529 }
530
531 #[test]
532 fn test_polynomial_remainder_zero_divisor_no_panic() {
533 // Regression test for R3: division by the zero polynomial must not
534 // panic; `polynomial_remainder` treats it as "no reduction
535 // possible" and returns the dividend unchanged.
536 let gcd_engine = PolynomialGcd::new();
537 let a = poly_x_plus_1();
538 let zero = Polynomial::zero();
539
540 let remainder = gcd_engine.polynomial_remainder(&a, &zero);
541 assert_eq!(remainder, a);
542 }
543
544 #[test]
545 fn test_gcd_disjoint_variables_terminates() {
546 // Regression test: `euclidean_gcd` must terminate (not hang) even
547 // for polynomials in entirely different variables, where the
548 // univariate-in-one-variable remainder computation cannot always
549 // reduce the degree (see `polynomial_remainder`'s documented
550 // multivariate limitation). This is guarded by an iteration bound;
551 // this test simply confirms the call returns instead of looping
552 // forever.
553 let mut gcd_engine = PolynomialGcd::new();
554 let a = Polynomial::from_coeffs_int(&[(1, &[(0, 1)]), (1, &[])]); // x0 + 1
555 let b = Polynomial::from_coeffs_int(&[(1, &[(1, 1)]), (1, &[])]); // x1 + 1
556
557 let _ = gcd_engine.gcd(&a, &b); // must return, not hang
558 }
559
560 #[test]
561 fn test_gcd_constant_dividend_nonconstant_divisor_remainder() {
562 // Regression test for the `combined_var` fix: when the *dividend*
563 // is a nonzero constant and the divisor is non-constant, the
564 // remainder must be the dividend itself (deg(dividend) <
565 // deg(divisor)), not incorrectly forced to zero by treating a
566 // one-sided constant as "both operands are constant".
567 let gcd_engine = PolynomialGcd::new();
568 let five = Polynomial::constant(BigRational::from(BigInt::from(5)));
569 let b = poly_x_plus_1();
570
571 let remainder = gcd_engine.polynomial_remainder(&five, &b);
572 assert_eq!(remainder, five);
573 }
574}