use num_bigint::BigInt;
use num_rational::Ratio;
use num_traits::{Signed, Zero};
use crate::poly::Poly;
#[derive(Debug, Clone)]
pub(crate) struct SturmChain {
chain: Vec<Poly>,
}
#[allow(dead_code)] impl SturmChain {
pub fn new(p: &Poly) -> Self {
if p.is_zero() {
return SturmChain {
chain: vec![Poly::zero()],
};
}
let p0 = p.square_free_part();
let p1 = p0.derivative();
if p1.is_zero() {
return SturmChain { chain: vec![p0] };
}
let mut chain = vec![p0.clone(), p1.clone()];
let mut prev = p0;
let mut curr = p1;
loop {
let rem = prev.rem(&curr);
if rem.is_zero() {
break;
}
let neg_rem = (-&rem).primitive_part();
chain.push(neg_rem.clone());
prev = curr;
curr = neg_rem;
}
SturmChain { chain }
}
pub fn sign_variations_at(&self, x: &Ratio<BigInt>) -> usize {
let signs: Vec<i8> = self
.chain
.iter()
.map(|p| {
let v = p.eval(x);
if v.is_positive() {
1
} else if v.is_negative() {
-1
} else {
0
}
})
.collect();
count_sign_changes(&signs)
}
pub fn sign_variations_at_pos_inf(&self) -> usize {
let signs: Vec<i8> = self.chain.iter().map(leading_sign).collect();
count_sign_changes(&signs)
}
pub fn sign_variations_at_neg_inf(&self) -> usize {
let signs: Vec<i8> = self
.chain
.iter()
.map(|p| {
let ls = leading_sign(p);
if ls == 0 {
return 0;
}
let deg = p.degree().unwrap_or(0);
if deg % 2 == 0 { ls } else { -ls }
})
.collect();
count_sign_changes(&signs)
}
pub fn count_real_roots(&self) -> usize {
let neg = self.sign_variations_at_neg_inf();
let pos = self.sign_variations_at_pos_inf();
neg.saturating_sub(pos)
}
pub fn has_no_real_roots(&self) -> bool {
self.count_real_roots() == 0
}
pub fn count_roots_in(&self, a: &Ratio<BigInt>, b: &Ratio<BigInt>) -> usize {
let sa = self.sign_variations_at(a);
let sb = self.sign_variations_at(b);
sa.saturating_sub(sb)
}
pub fn isolate_roots_in(
&self,
a: &Ratio<BigInt>,
b: &Ratio<BigInt>,
max_depth: u32,
) -> Vec<(Ratio<BigInt>, Ratio<BigInt>)> {
let two = Ratio::from_integer(BigInt::from(2));
let mut result = Vec::new();
let mut stack: Vec<(Ratio<BigInt>, Ratio<BigInt>, u32)> =
vec![(a.clone(), b.clone(), max_depth)];
while let Some((lo, hi, depth)) = stack.pop() {
let n = self.count_roots_in(&lo, &hi);
if n == 0 {
continue;
}
if n == 1 || depth == 0 {
result.push((lo, hi));
continue;
}
let mid = (&lo + &hi) / &two;
stack.push((mid.clone(), hi, depth - 1));
stack.push((lo, mid, depth - 1));
}
result
}
pub fn isolate_all_real_roots(&self) -> Vec<(Ratio<BigInt>, Ratio<BigInt>)> {
let Some(p) = self.chain.first() else {
return vec![];
};
if p.degree().unwrap_or(0) == 0 {
return vec![];
}
let bound = cauchy_bound(p) + Ratio::from_integer(BigInt::from(1));
let neg_bound = -bound.clone();
let raw = self.isolate_roots_in(&neg_bound, &bound, 256);
raw.into_iter()
.map(|(lo, hi)| {
if p.eval(&hi).is_zero() {
(hi.clone(), hi)
} else {
(lo, hi)
}
})
.collect()
}
pub fn refine_interval(
&self,
lo: &Ratio<BigInt>,
hi: &Ratio<BigInt>,
max_width: &Ratio<BigInt>,
) -> (Ratio<BigInt>, Ratio<BigInt>) {
let two = Ratio::from_integer(BigInt::from(2));
let mut lo = lo.clone();
let mut hi = hi.clone();
for _ in 0..512 {
if &hi - &lo <= *max_width || lo == hi {
break;
}
let mid = (&lo + &hi) / &two;
if self.chain.first().is_some_and(|p| p.eval(&mid).is_zero()) {
return (mid.clone(), mid);
}
if self.count_roots_in(&lo, &mid) == 1 {
hi = mid;
} else {
lo = mid;
}
}
(lo, hi)
}
pub fn count_roots_in_closed(&self, a: &Ratio<BigInt>, b: &Ratio<BigInt>) -> usize {
if a > b {
return 0;
}
let open_right = self.count_roots_in(a, b);
let at_a = self.chain.first().is_some_and(|p| p.eval(a).is_zero());
open_right + usize::from(at_a)
}
pub fn leading_sign_of_original(&self) -> i8 {
self.chain.first().map_or(0, leading_sign)
}
}
pub(crate) fn cauchy_bound(p: &Poly) -> Ratio<BigInt> {
let one = Ratio::from_integer(BigInt::from(1));
let Some(n) = p.degree() else {
return one;
};
if n == 0 {
return one;
}
let lc = p.coeff(n);
let mut max = Ratio::from_integer(BigInt::from(0));
for i in 0..n {
let r = (p.coeff(i) / &lc).abs();
if r > max {
max = r;
}
}
max + one
}
fn leading_sign(p: &Poly) -> i8 {
match p.leading_coeff() {
None => 0,
Some(c) if c.is_positive() => 1,
Some(c) if c.is_negative() => -1,
Some(_) => 0, }
}
fn count_sign_changes(signs: &[i8]) -> usize {
let nonzero: Vec<i8> = signs.iter().copied().filter(|&s| s != 0).collect();
nonzero.windows(2).filter(|w| w[0] != w[1]).count()
}
#[cfg(test)]
mod tests {
use super::*;
use num_bigint::BigInt;
use num_rational::Ratio;
fn r(n: i64) -> Ratio<BigInt> {
Ratio::from_integer(BigInt::from(n))
}
#[test]
fn sturm_x2_minus_1() {
let p = Poly::from_coeffs(vec![r(-1), r(0), r(1)]);
let chain = SturmChain::new(&p);
assert_eq!(chain.count_real_roots(), 2);
assert!(!chain.has_no_real_roots());
}
#[test]
fn sturm_x2_plus_1() {
let p = Poly::from_coeffs(vec![r(1), r(0), r(1)]);
let chain = SturmChain::new(&p);
assert_eq!(chain.count_real_roots(), 0);
assert!(chain.has_no_real_roots());
}
#[test]
fn sturm_x3_minus_x() {
let p = Poly::from_coeffs(vec![r(0), r(-1), r(0), r(1)]);
let chain = SturmChain::new(&p);
assert_eq!(chain.count_real_roots(), 3);
}
#[test]
fn sturm_x() {
let p = Poly::x();
let chain = SturmChain::new(&p);
assert_eq!(chain.count_real_roots(), 1);
}
#[test]
fn count_roots_in_subintervals() {
let p = Poly::from_coeffs(vec![r(0), r(-1), r(0), r(1)]);
let chain = SturmChain::new(&p);
assert_eq!(chain.count_roots_in(&r(-2), &r(2)), 3);
let neg_half = Ratio::new(BigInt::from(-1), BigInt::from(2));
assert_eq!(chain.count_roots_in(&r(-2), &neg_half), 1);
let half = Ratio::new(BigInt::from(1), BigInt::from(2));
assert_eq!(chain.count_roots_in(&neg_half, &half), 1);
assert_eq!(chain.count_roots_in(&half, &r(2)), 1);
assert_eq!(chain.count_roots_in(&r(2), &r(10)), 0);
}
#[test]
fn isolate_roots_x3_minus_x() {
let p = Poly::from_coeffs(vec![r(0), r(-1), r(0), r(1)]);
let chain = SturmChain::new(&p);
let intervals = chain.isolate_roots_in(&r(-10), &r(10), 50);
assert_eq!(intervals.len(), 3, "should isolate 3 roots");
for (lo, hi) in &intervals {
assert_eq!(chain.count_roots_in(lo, hi), 1);
}
}
#[test]
fn isolate_roots_no_real() {
let p = Poly::from_coeffs(vec![r(1), r(0), r(1)]);
let chain = SturmChain::new(&p);
let intervals = chain.isolate_roots_in(&r(-100), &r(100), 50);
assert!(intervals.is_empty());
}
#[test]
fn sturm_constant() {
let p = Poly::from_int(5);
let chain = SturmChain::new(&p);
assert_eq!(chain.count_real_roots(), 0);
assert!(chain.has_no_real_roots());
}
#[test]
fn sturm_zero() {
let p = Poly::zero();
let chain = SturmChain::new(&p);
assert_eq!(chain.count_real_roots(), 0);
}
#[test]
fn sturm_x2_minus_2() {
let p = Poly::from_coeffs(vec![r(-2), r(0), r(1)]);
let chain = SturmChain::new(&p);
assert_eq!(chain.count_real_roots(), 2);
}
#[test]
fn sturm_repeated_root() {
let p = Poly::from_coeffs(vec![r(1), r(-2), r(1)]); let chain = SturmChain::new(&p);
assert_eq!(chain.count_real_roots(), 1);
}
#[test]
fn isolate_all_x3_minus_x() {
let p = Poly::from_coeffs(vec![r(0), r(-1), r(0), r(1)]);
let chain = SturmChain::new(&p);
let iv = chain.isolate_all_real_roots();
assert_eq!(iv.len(), 3);
for w in iv.windows(2) {
assert!(w[0].1 <= w[1].0);
}
for (lo, hi) in &iv {
if lo == hi {
assert!(p.eval(lo).is_zero());
} else {
assert_eq!(chain.count_roots_in(lo, hi), 1);
}
}
}
#[test]
fn isolate_all_x2_minus_2() {
let p = Poly::from_coeffs(vec![r(-2), r(0), r(1)]);
let chain = SturmChain::new(&p);
let iv = chain.isolate_all_real_roots();
assert_eq!(iv.len(), 2);
assert!(iv[0].1 <= r(0) && iv[1].0 >= r(0));
let width = Ratio::new(BigInt::from(1), BigInt::from(100));
let (lo, hi) = chain.refine_interval(&iv[1].0, &iv[1].1, &width);
assert!(&hi - &lo <= width);
assert!(lo < Ratio::new(BigInt::from(1415), BigInt::from(1000)));
assert!(hi > Ratio::new(BigInt::from(1414), BigInt::from(1000)));
}
#[test]
fn closed_interval_count() {
let p = Poly::from_coeffs(vec![r(0), r(-1), r(0), r(1)]); let chain = SturmChain::new(&p);
assert_eq!(chain.count_roots_in_closed(&r(-1), &r(1)), 3);
assert_eq!(chain.count_roots_in(&r(-1), &r(1)), 2);
assert_eq!(chain.count_roots_in_closed(&r(0), &r(0)), 1);
assert_eq!(chain.count_roots_in_closed(&r(2), &r(1)), 0);
}
#[test]
fn leading_sign_positive() {
let p = Poly::from_coeffs(vec![r(1), r(0), r(1)]); let chain = SturmChain::new(&p);
assert_eq!(chain.leading_sign_of_original(), 1);
}
#[test]
fn leading_sign_negative() {
let p = Poly::from_coeffs(vec![r(-1), r(0), r(-1)]); let chain = SturmChain::new(&p);
assert_eq!(chain.leading_sign_of_original(), -1);
}
}