zippel_gcd/lib.rs
1//! Sparse multivariate polynomial GCD, cofactors and LCM over the rationals.
2//!
3//! [`gcd()`] automatically tries Huang–Gao, Huang–Monagan, then Zippel.
4//! [`gcd_with_algorithm`] chooses the first algorithm in a documented fallback
5//! chain. [`cofactors`] and [`lcm`] provide the same choice through their
6//! `_with_algorithm` variants.
7//!
8//! Call an algorithm directly to control recovery:
9//!
10//! | Function | Algorithm | Result |
11//! | --- | --- | --- |
12//! | [`zippel()`] | Zippel modular GCD with LINZIP | `Poly` |
13//! | [`huang_gao()`] | Huang–Gao separated Hensel lifting; asymptotic SOTA (2026) | `Option<Poly>` |
14//! | [`huang_monagan()`] | Huang–Monagan prime substitution | `Option<Poly>` |
15//! | [`hu_monagan()`] | Hu–Monagan GCD/cofactor interpolation | `Option<Poly>` |
16//! | [`hu_monagan_bivariate()`] | Hu–Monagan with bivariate images | `Option<Poly>` |
17//!
18//! Named sparse methods return `None` when bounded recovery fails; they never
19//! switch to another recovery backend. They share integer normalization, CRT,
20//! exact certification and, for Hu–Monagan, Zippel content extraction.
21//! Every successful result has the normalization and zero conventions of [`gcd()`].
22//!
23//! Huang–Gao's [2026 paper](https://arxiv.org/abs/2609.08074v1) is the
24//! state of the art in asymptotic sparse integer GCD complexity. This bounded,
25//! exactly certified implementation does not claim the paper's full complexity
26//! bound or the fastest practical runtime for every input.
27//!
28//! ```
29//! use polycore::{Order, Ring};
30//! use zippel_gcd::{gcd, huang_gao, huang_monagan, hu_monagan,
31//! hu_monagan_bivariate, zippel};
32//!
33//! let ring = Ring::new(["x", "y"], Order::Lex);
34//! let a = ring.parse("(x + y)*(x + 1)").unwrap();
35//! let b = ring.parse("(x + y)*(y + 2)").unwrap();
36//! let expected = ring.parse("x + y").unwrap();
37//! assert_eq!(gcd(&a, &b), expected); // Automatic recovery and fallback.
38//! assert_eq!(zippel(&a, &b), expected); // Explicit Zippel.
39//! for algorithm in [huang_gao, huang_monagan, hu_monagan, hu_monagan_bivariate] {
40//! assert_eq!(algorithm(&a, &b).unwrap(), expected);
41//! }
42//! ```
43#![allow(
44 clippy::multiple_crate_versions,
45 clippy::redundant_pub_crate,
46 clippy::cast_possible_truncation,
47 clippy::missing_panics_doc,
48 clippy::many_single_char_names,
49 clippy::similar_names,
50 clippy::module_name_repetitions
51)]
52
53mod frac;
54mod gcd;
55mod hu_monagan;
56mod huang_gao;
57mod huang_monagan;
58mod images;
59mod linzip;
60mod modular_division;
61mod pgcd;
62mod poly;
63
64use num_bigint::BigInt;
65use num_integer::Integer;
66use num_rational::BigRational;
67use num_traits::One;
68use poly::IntPoly;
69use polycore::Monomial;
70
71pub use frac::Frac;
72
73/// Preferred algorithm for [`gcd_with_algorithm`], [`cofactors_with_algorithm`]
74/// and [`lcm_with_algorithm`], including each variant's documented fallback.
75///
76/// To run a single recovery backend without fallback, use its named function:
77/// [`zippel()`], [`huang_gao()`], [`huang_monagan()`], [`hu_monagan()`] or
78/// [`hu_monagan_bivariate()`].
79#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
80pub enum GcdAlgorithm {
81 /// Huang–Gao separated Hensel lifting (asymptotic SOTA, 2026), then
82 /// Huang–Monagan, then Zippel. See [`huang_gao()`] for the paper and scope.
83 #[default]
84 HuangGao,
85 /// Integer prime substitution and valuation decoding, then Zippel.
86 ///
87 /// Follows Algorithms 1 and 6 of Huang–Monagan,
88 /// <https://arxiv.org/abs/2609.10626v1>, using the practical modular univariate
89 /// GCD and small-prime sampling of Section 6. Trial division by the chosen
90 /// primes recovers exponents; exact division and degree certificates reject
91 /// unlucky substitutions.
92 ///
93 /// Tries two substitutions per term-bound guess from 2 through 64. Images
94 /// are limited to degree 65,536, estimated coefficient size 65,536 bits and
95 /// 64 million coefficient bits in total. Degree certificates use at most
96 /// four million coefficient slots; each univariate CRT uses at most 256
97 /// primes. Exhausting a budget invokes the Zippel fallback.
98 HuangMonagan,
99 /// Hu–Monagan geometric interpolation of a GCD and cofactor together.
100 /// Uses univariate images, smooth-subgroup discrete logarithms and
101 /// Ben-Or/Tiwari recovery, then exact certification and a Zippel fallback.
102 ///
103 /// Both Hu–Monagan variants allow three orbits of at most 256 samples per
104 /// prime, 256 CRT primes, a 48-bit exponent box, retained degrees up to
105 /// 4,096 and two million coefficient slots per interpolant. Exact modular
106 /// division permits 100,000 term reductions. Exceeding a budget falls back.
107 HuMonagan,
108 /// Hu–Monagan simultaneous GCD/cofactor recovery using bivariate images.
109 /// Keeps two variables symbolic; otherwise uses the same recovery and
110 /// budgets as [`Self::HuMonagan`]. In one variable, uses univariate images.
111 HuMonaganBivariate,
112 /// Recursive modular interpolation with LINZIP support reuse.
113 Zippel,
114}
115
116type Poly = polycore::Poly<BigRational>;
117
118/// The top-level recovery chain actually tried, in order.
119///
120/// Shared preprocessing
121/// (including recursive content extraction) is not included. The last entry
122/// identifies the successful attempt, which can also finish during preprocessing.
123#[derive(Clone, Debug, PartialEq, Eq)]
124pub struct GcdReport {
125 pub attempted: Vec<GcdAlgorithm>,
126}
127
128/// Compute a GCD with the same fallback chain as [`gcd_with_algorithm`], recording
129/// which attempts ran. This does not add a work limit to the Zippel fallback.
130#[must_use]
131pub fn gcd_with_report(f: &Poly, g: &Poly, algorithm: GcdAlgorithm) -> (Poly, GcdReport) {
132 assert_eq!(f.nvars, g.nvars, "polynomials must share a ring");
133 let mut report = GcdReport {
134 attempted: Vec::new(),
135 };
136 let h = gcd::gcd_z_observed(&integral(f).0, &integral(g).0, algorithm, |a| {
137 report.attempted.push(a);
138 });
139 (normalized(&h, f), report)
140}
141
142/// `f = F / d` with `F` integral.
143fn integral(f: &Poly) -> (IntPoly, BigInt) {
144 let d = f
145 .terms
146 .iter()
147 .fold(BigInt::one(), |d, (_, c)| d.lcm(c.denom()));
148 let terms = f
149 .terms
150 .iter()
151 .map(|(m, c)| (m.exps().to_vec(), c.numer() * (&d / c.denom())));
152 (IntPoly::new(f.nvars, terms), d)
153}
154
155fn rational(h: &IntPoly, d: &BigInt, like: &Poly) -> Poly {
156 let terms = h.terms.iter().map(|(e, c)| {
157 (
158 Monomial::new(e.clone()),
159 BigRational::new(c.clone(), d.clone()),
160 )
161 });
162 Poly::new(terms.collect(), like.nvars, like.order.clone())
163}
164
165/// Primitive over Z with a positive leading coefficient in `like`'s monomial order.
166fn normalized(h: &IntPoly, like: &Poly) -> Poly {
167 rational(h, &BigInt::one(), like).primitive()
168}
169
170fn gcd_int(f: &Poly, g: &Poly, algorithm: GcdAlgorithm) -> IntPoly {
171 assert_eq!(f.nvars, g.nvars, "polynomials must share a ring");
172 gcd::gcd_z(&integral(f).0, &integral(g).0, algorithm)
173}
174
175/// Automatic GCD: Huang–Gao, then Huang–Monagan, then Zippel.
176///
177/// Returns a primitive integer polynomial with positive leading coefficient
178/// in the input monomial order. `gcd(0, 0) = 0`. Use the named algorithm
179/// functions to run a specific recovery backend without fallback.
180#[must_use]
181pub fn gcd(f: &Poly, g: &Poly) -> Poly {
182 gcd_with_algorithm(f, g, GcdAlgorithm::default())
183}
184
185fn recover(f: &Poly, g: &Poly, algorithm: GcdAlgorithm) -> Option<Poly> {
186 assert_eq!(f.nvars, g.nvars, "polynomials must share a ring");
187 gcd::try_gcd_z(&integral(f).0, &integral(g).0, algorithm).map(|h| normalized(&h, f))
188}
189
190/// Zippel GCD by recursive modular interpolation with LINZIP support reuse.
191///
192/// Uses only the Zippel recovery backend, with retries until exact reconstruction
193/// succeeds. Has the normalization and zero conventions of [`gcd()`].
194#[must_use]
195pub fn zippel(f: &Poly, g: &Poly) -> Poly {
196 recover(f, g, GcdAlgorithm::Zippel).expect("Zippel retries until reconstruction succeeds")
197}
198
199/// Huang–Gao GCD by derivative-aided separated Hensel lifting (asymptotic SOTA, 2026).
200///
201/// Runs Huang–Gao recovery only; returns `None` if bounded recovery fails.
202/// Every `Some` result is exactly certified and normalized like [`gcd()`].
203/// Use [`gcd()`] for automatic fallback.
204///
205/// The [2026 paper](https://arxiv.org/abs/2609.08074v1) gives expected integer
206/// bit complexity `O~(n T D log(Hi) log(Ho))`, linear in each fundamental parameter.
207/// The SOTA designation concerns that asymptotic result; this bounded implementation
208/// does not claim the full bound or the best practical runtime on every input.
209#[must_use]
210pub fn huang_gao(f: &Poly, g: &Poly) -> Option<Poly> {
211 recover(f, g, GcdAlgorithm::HuangGao)
212}
213
214/// Huang–Monagan GCD by integer prime substitution and valuation decoding.
215///
216/// Runs Huang–Monagan recovery only; returns `None` if bounded recovery fails.
217/// Every `Some` result is exactly certified and normalized like [`gcd()`].
218/// See [`GcdAlgorithm::HuangMonagan`] for the paper and recovery budgets.
219#[must_use]
220pub fn huang_monagan(f: &Poly, g: &Poly) -> Option<Poly> {
221 recover(f, g, GcdAlgorithm::HuangMonagan)
222}
223
224/// Hu–Monagan GCD/cofactor interpolation using univariate images.
225///
226/// Runs Hu–Monagan recovery only; returns `None` if bounded recovery fails.
227/// Shared preprocessing uses Zippel to extract polynomial contents. Every `Some`
228/// result is exactly certified and normalized like [`gcd()`]. See
229/// [`GcdAlgorithm::HuMonagan`] for recovery budgets.
230#[must_use]
231pub fn hu_monagan(f: &Poly, g: &Poly) -> Option<Poly> {
232 recover(f, g, GcdAlgorithm::HuMonagan)
233}
234
235/// Hu–Monagan GCD/cofactor interpolation using bivariate images.
236///
237/// Retains two symbolic variables, or one for a univariate ring. Has the same
238/// normalization, content preprocessing and `None` behavior as [`hu_monagan()`],
239/// without switching recovery backends.
240#[must_use]
241pub fn hu_monagan_bivariate(f: &Poly, g: &Poly) -> Option<Poly> {
242 recover(f, g, GcdAlgorithm::HuMonaganBivariate)
243}
244
245/// GCD and cofactors `(h, f / h, g / h)` using the automatic Huang–Gao-first chain.
246///
247/// `h = gcd(f, g)`. Both cofactors are zero when both inputs are zero.
248#[must_use]
249pub fn cofactors(f: &Poly, g: &Poly) -> (Poly, Poly, Poly) {
250 cofactors_with_algorithm(f, g, GcdAlgorithm::default())
251}
252
253/// GCD using a chosen first algorithm and its documented fallback chain.
254///
255/// [`GcdAlgorithm::HuangGao`] tries Huang–Gao, Huang–Monagan, then Zippel.
256/// Other sparse variants try the selected method, then Zippel; the Zippel
257/// variant uses only Zippel. Named functions such as [`huang_gao()`] give
258/// direct access without fallback. Normalizes the result like [`gcd()`].
259///
260/// ```
261/// use polycore::{Order, Ring};
262/// use zippel_gcd::{gcd_with_algorithm, GcdAlgorithm};
263/// let ring = Ring::new(["x", "y"], Order::Lex);
264/// let a = ring.parse("(x + y)*(x + 1)").unwrap();
265/// let b = ring.parse("(x + y)*(y + 2)").unwrap();
266/// let h = gcd_with_algorithm(&a, &b, GcdAlgorithm::HuangMonagan);
267/// assert_eq!(h, ring.parse("x + y").unwrap());
268/// ```
269#[must_use]
270pub fn gcd_with_algorithm(f: &Poly, g: &Poly, algorithm: GcdAlgorithm) -> Poly {
271 normalized(&gcd_int(f, g, algorithm), f)
272}
273
274/// GCD and cofactors using a chosen first algorithm and its documented fallback chain.
275///
276/// See [`gcd_with_algorithm`] for the exact chain and [`cofactors`] for the result.
277#[must_use]
278pub fn cofactors_with_algorithm(f: &Poly, g: &Poly, algorithm: GcdAlgorithm) -> (Poly, Poly, Poly) {
279 let h = gcd_int(f, g, algorithm);
280 let zero = Poly::zero(f.nvars, f.order.clone());
281 if h.is_zero() {
282 return (zero.clone(), zero.clone(), zero);
283 }
284 let h = normalized(&h, f);
285 let hi = integral(&h).0;
286 let (hf, df) = integral(f);
287 let (hg, dg) = integral(g);
288 let qf = hf.div_exact(&hi).expect("gcd divides f");
289 let qg = hg.div_exact(&hi).expect("gcd divides g");
290 (h, rational(&qf, &df, f), rational(&qg, &dg, g))
291}
292
293/// LCM using the automatic Huang–Gao-first GCD chain.
294///
295/// Normalized like [`gcd()`]. `lcm(f, 0) = 0`.
296#[must_use]
297pub fn lcm(f: &Poly, g: &Poly) -> Poly {
298 lcm_with_algorithm(f, g, GcdAlgorithm::default())
299}
300
301/// LCM using a chosen first algorithm and its documented fallback chain.
302///
303/// See [`gcd_with_algorithm`] for the exact chain. Normalizes the result like [`gcd()`].
304#[must_use]
305pub fn lcm_with_algorithm(f: &Poly, g: &Poly, algorithm: GcdAlgorithm) -> Poly {
306 let h = gcd_int(f, g, algorithm);
307 if f.is_zero() || g.is_zero() {
308 return Poly::zero(f.nvars, f.order.clone());
309 }
310 let (hg, _) = integral(g);
311 normalized(
312 &integral(f).0.mul(&hg.div_exact(&h).expect("gcd divides g")),
313 f,
314 )
315}