use crate::linzip::linzip;
use crate::pgcd::{pgcd, reshape};
use crate::poly::{add_exps, gcd, one, Exps, IntPoly, ModPoly};
use crate::GcdAlgorithm;
use num_bigint::BigInt;
use num_integer::Integer;
use num_traits::One;
use polycore::crt::{residue, symmetric, CrtAccumulator, WangContext};
use polycore::fast::PrimeField;
use polycore::modp::Primes;
use polycore::sample::Rng;
use std::iter::once;
pub(crate) fn gcd_z(f: &IntPoly, g: &IntPoly, algorithm: GcdAlgorithm) -> IntPoly {
gcd_z_observed(f, g, algorithm, |_| {})
}
pub(crate) fn gcd_z_observed(
f: &IntPoly,
g: &IntPoly,
algorithm: GcdAlgorithm,
mut observe: impl FnMut(GcdAlgorithm),
) -> IntPoly {
let mut attempt = |algorithm| {
observe(algorithm);
try_gcd_z(f, g, algorithm)
};
attempt(algorithm)
.or_else(|| {
(algorithm == GcdAlgorithm::HuangGao)
.then(|| attempt(GcdAlgorithm::HuangMonagan))
.flatten()
})
.unwrap_or_else(|| {
attempt(GcdAlgorithm::Zippel).expect("Zippel retries until reconstruction succeeds")
})
}
pub(crate) fn try_gcd_z(f: &IntPoly, g: &IntPoly, algorithm: GcdAlgorithm) -> Option<IntPoly> {
if f.is_zero() || g.is_zero() {
let h = if f.is_zero() { g } else { f };
return Some(if h.is_zero() {
h.clone()
} else {
h.primitive()
});
}
let c = gcd(&f.content(), &g.content());
let (mf, mg) = (f.min_exps(), g.min_exps());
let m: Exps = mf.iter().zip(&mg).map(|(a, b)| *a.min(b)).collect();
let shift = |h: &IntPoly, s: &Exps| {
h.primitive()
.map(|e, x| (e.iter().zip(s).map(|(a, b)| a - b).collect(), x.clone()))
};
let h = gcd_shifted(&shift(f, &mf), &shift(g, &mg), algorithm)?;
Some(h.map(|e, x| (add_exps(e, &m), x * &c)))
}
fn gcd_shifted(f: &IntPoly, g: &IntPoly, algorithm: GcdAlgorithm) -> Option<IntPoly> {
let n = f.n;
let shared = (0..n).filter(|&k| f.degree(k) > 0 && g.degree(k) > 0);
let Some(x0) = shared.max_by_key(|&k| f.degree(k).min(g.degree(k))) else {
return Some(one(n));
};
if algorithm == GcdAlgorithm::HuangGao {
return modular(f, g, Backend::Separated);
}
if algorithm == GcdAlgorithm::HuangMonagan {
return crate::huang_monagan::gcd(f, g);
}
let perm: Vec<usize> = once(x0).chain((0..n).filter(|&k| k != x0)).collect();
let mut back = vec![0; n];
perm.iter().enumerate().for_each(|(i, &k)| back[k] = i);
let (f, g) = (f.permute(&perm), g.permute(&perm));
let (cf, cg) = (content_x0(&f), content_x0(&g));
let cont = gcd_z(&cf, &cg, GcdAlgorithm::Zippel);
let pf = f.div_exact(&cf).expect("content divides");
let pg = g.div_exact(&cg).expect("content divides");
let keep = match algorithm {
GcdAlgorithm::HuMonagan => Some(1),
GcdAlgorithm::HuMonaganBivariate => Some(n.min(2)),
_ => None,
};
let h = if let Some(keep) = keep {
let (a, b) = if pf.terms.len() <= pg.terms.len() {
(&pf, &pg)
} else {
(&pg, &pf)
};
modular(a, b, Backend::HuMonagan(keep))?
} else {
modular(&pf, &pg, Backend::Zippel)?
};
Some(h.mul(&cont).permute(&back))
}
fn content_x0(f: &IntPoly) -> IntPoly {
let mut coeffs = f.coefficients_in_x0().into_iter();
let first = coeffs.next().expect("nonzero");
coeffs
.try_fold(first, |acc, c| {
let h = gcd_z(&acc, &c, GcdAlgorithm::Zippel);
if h.is_constant() {
Err(())
} else {
Ok(h)
}
})
.unwrap_or_else(|()| one(f.n))
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum Backend {
Separated,
Univariate,
HuMonagan(usize),
Zippel,
}
pub(crate) fn univariate(f: &IntPoly, g: &IntPoly) -> Option<IntPoly> {
debug_assert!(f.n == 1 && g.n == 1 && !f.is_zero() && !g.is_zero());
modular(f, g, Backend::Univariate)
}
fn modular(f: &IntPoly, g: &IntPoly, backend: Backend) -> Option<IntPoly> {
let n = f.n;
let mut rng = Rng::new(0x5eed);
let gamma = gcd(f.lc(), g.lc());
let mut skeleton: Option<Vec<Exps>> = None;
let mut acc = CrtAccumulator::new(0);
let mut tried: Option<IntPoly> = None;
let primes: Box<dyn Iterator<Item = u64>> = if let Backend::HuMonagan(keep) = backend {
Box::new(zippel_interp::geometric::Encoding::new(&f.degrees()[keep..])?.primes())
} else {
Box::new(Primes::new())
};
for (attempt, p) in primes.enumerate() {
if matches!(backend, Backend::Univariate | Backend::HuMonagan(_)) && attempt >= 256 {
return None;
}
let gp_ = residue(&gamma, p);
let (fp, gp) = (f.reduce(p), g.reduce(p));
if gp_ == 0 || fp.lm() != f.terms[0].0 || gp.lm() != g.terms[0].0 {
continue;
}
if backend == Backend::Zippel && skeleton.is_none() && coprime(&fp, &gp, p, &mut rng) {
return Some(one(n));
}
let h = if backend == Backend::Separated {
crate::huang_gao::gcd(&fp, &gp, p, &mut rng)?
} else if let Backend::HuMonagan(keep) = backend {
crate::hu_monagan::gcd(&fp, &gp, keep, p, &mut rng)?
} else if backend == Backend::Univariate {
let dense = |f: &ModPoly| {
let mut cs = vec![0; f.degree(0) + 1];
for (e, c) in &f.terms {
cs[e[0] as usize] = *c;
}
cs
};
let h = PrimeField::new(p)
.expect("prime modulus")
.gcd(&dense(&fp), &dense(&gp));
ModPoly {
n: 1,
terms: h
.into_iter()
.enumerate()
.rev()
.filter(|(_, c)| *c != 0)
.map(|(e, c)| (vec![e as u32], c))
.collect(),
}
} else {
let sparse = skeleton
.as_deref()
.filter(|_| n >= 2)
.and_then(|s| linzip(&fp, &gp, s, n - 1, p, &mut rng));
let Some(h) = sparse.or_else(|| pgcd(&fp, &gp, n - 1, p, &mut rng)) else {
continue;
};
h
};
let mut h = h.monic(p);
h.scale(gp_, p);
match reshape(skeleton.as_deref(), &h) {
None => continue,
Some(true) => {
skeleton = Some(h.support());
acc = CrtAccumulator::new(h.terms.len());
}
Some(false) => {}
}
let s = skeleton.as_ref().expect("set above");
let values: Vec<u64> = s
.iter()
.map(|e| {
h.terms
.binary_search_by(|t| e.cmp(&t.0))
.map_or(0, |i| h.terms[i].1)
})
.collect();
if acc.add(p, &values).is_err() {
continue;
}
let Some(h) = candidate(
n,
s,
acc.residues(),
acc.modulus(),
acc.image_count().is_power_of_two(),
)
.filter(|h| tried.as_ref() != Some(h)) else {
continue;
};
if f.div_exact(&h).is_some() && g.div_exact(&h).is_some() {
return Some(h);
}
tried = Some(h);
}
None
}
const SLACK: u64 = 16;
fn candidate(n: usize, s: &[Exps], acc: &[BigInt], m: &BigInt, fractions: bool) -> Option<IntPoly> {
let room = m.bits().saturating_sub(SLACK);
let lift: Vec<BigInt> = acc.iter().map(|a| symmetric(a, m)).collect();
let poly = |cs: Vec<BigInt>| Some(IntPoly::new(n, s.iter().cloned().zip(cs)).primitive());
if lift.iter().all(|a| a.bits() <= room) {
return poly(lift);
}
if !fractions {
return None;
}
let w = WangContext::new(m)?;
let gi = acc[0].modinv(m)?;
let fraction = |a: &BigInt| {
w.reconstruct(&(a * &gi).mod_floor(m))
.filter(|q| q.numer().bits() + q.denom().bits() <= room)
};
fraction(acc.last()?)?;
let qs = acc.iter().map(fraction).collect::<Option<Vec<_>>>()?;
let l = qs.iter().fold(BigInt::one(), |l, q| l.lcm(q.denom()));
poly(qs.iter().map(|q| q.numer() * (&l / q.denom())).collect())
}
fn coprime(f: &ModPoly, g: &ModPoly, p: u64, rng: &mut Rng) -> bool {
let point: Vec<u64> = (0..f.n).map(|_| rng.nonzero(p)).collect();
let (uf, ug) = (f.eval_except(0, &point, p), g.eval_except(0, &point, p));
uf.deg() == f.degree(0) && ug.deg() == g.degree(0) && uf.gcd(&ug).0.len() == 1
}
#[cfg(test)]
mod tests {
use super::*;
use polycore::{Order, Ring};
#[test]
fn hu_monagan_crt_recovers_large_and_initially_missing_coefficients() {
let ring = Ring::new(["x", "y", "z"], Order::Lex);
let prime = zippel_interp::geometric::Encoding::new(&[4, 4])
.unwrap()
.primes()
.next()
.unwrap();
let large = (BigInt::one() << 190) + BigInt::from(321);
let h = ring.parse(&format!("x^3+{large}*x*y+{prime}*z+1")).unwrap();
let a = crate::integral(&(&h * &ring.parse("x+y+2").unwrap())).0;
let b = crate::integral(&(&h * &ring.parse("x+z+3").unwrap())).0;
for keep in [1, 2] {
let g =
modular(&a, &b, Backend::HuMonagan(keep)).expect("Hu–Monagan CRT without fallback");
assert_eq!(g, crate::integral(&h).0);
}
}
}