Sturm's theorem #
For a squarefree real polynomial p and a < b with p a ≠ 0, p b ≠ 0, the number of distinct
real roots of p in (a, b] is V(a) - V(b), where V(x) is the number of sign variations of the
Sturm sequence p, p', -(p % p'), … evaluated at x.
The local analysis at a root of a member of the chain is organised through the relation
Sturm.FlankReduce on sign lists, and the global statement follows by induction on the number of
critical points in (a, b]. The file also provides a sign-variation toolkit on lists of signs
(Sturm.signChanges, Sturm.wallCount) that BudanFourier reuses.
Main definitions #
Sturm.sturmAux p q: the signed remainder chainp, q, -(p % q), ….Sturm.sturmSeq p:sturmAux p (derivative p).Sturm.signVarAt L x: the number of sign variations of the values atxof the polynomials inL.Sturm.signChanges s: the number of sign variations of a list of signs, zeros ignored.
Main results #
Sturm.sturm: Sturm's theorem.Sturm.signVarAt_drop_at_critical_point: the local drop ofVat a single critical point.Sturm.signChanges_eq_wallCount,Sturm.wallCount_even_iff_endpoints: parity of the number of sign variations from the end signs.Sturm.sturmAux_eq_sturmSeq,Sturm.signVarAt_eq_signVariations: the chain is Mathlib'sPolynomial.sturmSeqandsignVarAtis Mathlib'sList.signVariationsof the values;Sturm.signVariations_eq_signChangesrelatesPolynomial.signVariationstosignChanges.
Implementation notes #
Mathlib has the Sturm sequence (Polynomial.sturmSeq) and sign variations of lists, but not
Sturm's theorem. This file keeps its own chain sturmAux, defined by recursion on the degree of the
divisor, and proves that it agrees with Polynomial.sturmSeq for p ≠ 0.
References #
- S. Basu, R. Pollack, M.-F. Roy, Algorithms in Real Algebraic Geometry, 2nd ed., Springer, 2006, §2.2.
- W. Li, The Sturm–Tarski Theorem, Archive of Formal Proofs, 2014 (Isabelle/HOL).
The signed remainder chain starting from (p, q): p :: q :: -(p % q) :: …, stopping when a
remainder hits 0. Terminates because (p % q).degree < q.degree.
Equations
- Sturm.sturmAux p q = if _hq : q = 0 then [p] else p :: Sturm.sturmAux q (-(p % q))
Instances For
Sturm sequence of p: the signed remainder sequence of p and its derivative.
Equations
Instances For
Sign variations of a list of polynomials evaluated at x: count sign changes in
L.map (eval x), ignoring zeros. Eval-analogue of Polynomial.signVariations.
Equations
- One or more equations did not get rendered due to their size.
Instances For
P1 — chain algebra #
The signed-remainder step next = -(a % b) obeys a + next = b * (a / b). Evaluated at a root of
the middle member b, this forces a(x) = -next(x): the predecessor and successor are antipodal
there. With consecutive members coprime (squarefree case), neither vanishes, so they have strictly
opposite signs — the engine that makes interior sign-variation changes cancel.
Signed-remainder step. With next = -(a % b), we have a + next = b * (a / b).
Antipodal at a root of the middle member. If x is a root of b, the predecessor a and
the successor next = -(a % b) take opposite values at x: a(x) = -next(x).
Coprimality is preserved by the signed-remainder step. If a, b are coprime then so are
b and the successor next = -(a % b). (Same ideal: (a, b) = (b, a % b).)
Squarefree ⟹ coprime to its derivative (over ℝ, a perfect field). The whole Sturm chain
inherits its gcd from gcd p p', which is a unit precisely when p is squarefree.
Coprime polynomials share no real root (Bézout: u·a + v·b = 1 evaluated at a common root
gives 0 = 1).
P2 — local constancy #
On an interval where a polynomial has no root its evaluation keeps a constant sign (IVT: an opposite
sign at the endpoints would force a root in between). Hence on an interval free of roots of EVERY
chain member, the whole sign pattern — and therefore signVarAt — is constant.
Constant sign on a root-free interval. If q has no root in [a, b], then q takes the
same sign at a and b.
signVarAt is constant on a root-free interval. If no member of L has a root in [a, b],
the sign variations of L agree at a and b.
P3 — behaviour at a root of p (analytic core) #
At a simple root α of p (so p'(α) ≠ 0), write p = (X - α)·g; then g(α) = p'(α) ≠ 0, and
near α, sign(p(x)) = sign(x - α)·sign(p'(α)): p flips sign, with orientation fixed by p'.
This is the analytic engine behind "the leading pair (p, p') loses exactly one sign variation as
x crosses α". The remaining combinatorial accounting (full V-drop over the list) is P3'/P5.
At a root α of p, the cofactor p /ₘ (X - α) evaluates to p'(α).
Sign flip at a simple root. Near a simple root α of p,
sign(p(x)) = sign(x - α) · sign(p'(α)).
P3' — sign-variation combinatorics (toward the V-drop) #
Factor signVarAt through a pure signChanges : List SignType → ℕ and prove the bookkeeping
facts the V-drop needs: zeros are invisible, and a three-term window with opposite nonzero ends
always shows exactly one variation regardless of its middle (the interior-root cancellation from
P1's antipodal property, in combinatorial form).
Sign changes in a raw list of signs: drop the zeros, then count adjacent differences.
Equations
- Sturm.signChanges s = (List.destutter (fun (x1 x2 : SignType) => x1 ≠ x2) (List.filter (fun (x : SignType) => decide (x ≠ 0)) s)).length - 1
Instances For
signVarAt is signChanges of the evaluated sign pattern.
A leading zero is invisible to signChanges.
Head recursion. Prepending a nonzero sign c adds exactly one variation when c differs
from the first surviving (nonzero) sign d of s, and none when it agrees. This is the leading-pair
flip in combinatorial form.
Leading-pair drop. If the head c flips relative to the first surviving sign c' of the
tail (c ≠ c', both nonzero), the count is one more than with head c'. With c = sign p(α⁻) and
c' = sign p'(α) = sign p(α⁺), this is the exact -1 of the (p, p') pair across a root.
Interior cancellation. Deleting a middle entry that sits between two nonzero opposite signs
leaves the count unchanged — whatever the deleted sign is. This is why an interior chain member,
whose sign may change across α, does not move V (its neighbours are fixed and opposite).
P5 — structural facts about the Sturm sequence (toward assembly) #
The chain stops at a zero divisor: sturmAux p 0 = [p].
Every chain sturmAux p q starts with p.
The Sturm sequence starts with p.
Every member of a Sturm chain is nonzero, provided the dividend is.
The product of all Sturm-sequence members is nonzero (each member is nonzero in the domain
ℝ[X]). Its roots are exactly the critical points; this makes the critical set finite.
Consecutive members of a signed-remainder chain are coprime (with a coprime seed). Each step
preserves coprimality (isCoprime_step), so every adjacent pair [x, m] appearing in the chain is
coprime — the source of hiso (no two adjacent members vanish at a common point).
The successor of a chain triple is the signed remainder. For any three consecutive members
[x, m, y] of a signed-remainder chain, y = -(x % m) — the chain's defining relation, surfaced as
a fact about adjacent triples. Together with eval_eq_neg_next_of_root this gives the antipodal
flank behaviour at an interior root (the source of hflank).
The last member of a coprime-seeded chain is a unit. The chain ends at the last nonzero
remainder — the gcd — which for a coprime seed is a unit (IsCoprime p 0 ↔ IsUnit p at the base).
Hence the final Sturm member is a nonzero constant, never vanishing (the source of hlast).
Root-crossing bridge. If, between two points x and y, only the head polynomial p
changes sign (the rest of the list keeps its signs), p is nonzero at both, and the head sign at
y equals the first surviving sign of the tail, then V is exactly one larger at x. This lifts
the combinatorial signChanges_head_drop to the evaluated sign lists — the -1 of V across a
simple root of p, in generic position.
Tail signs are unchanged on a root-free interval (the htail provider for the keystone).
The first surviving tail sign is the sign of the first (nonzero-valued) member (the hfirst
provider for the keystone).
Interior cancellation at arbitrary position (monolith engine) #
The first surviving sign of pre ++ a :: X does not depend on X (the inserted/changed entry
sits after the nonzero a, never becoming the first nonzero).
Interior cancellation, any position. Deleting a middle entry between two nonzero opposite signs leaves the count unchanged, anywhere in the list (induction on the prefix).
Interior-member crossing (piece ii): neighbours are nonzero and opposite #
At a root of an interior chain member, its neighbours are nonzero and opposite. If a and
m are coprime (consecutive Sturm members) and m(c) = 0, then a(c) and the successor
(-(a % m))(c) are both nonzero and carry opposite signs — exactly the configuration that makes
signChanges_remove_middle fire, so the interior root contributes 0 to the variation change.
Surgery atom. At a point z where the two flanking polynomials pa, pb take nonzero
opposite signs, the middle polynomial pm can be deleted from the list without changing signVarAt
— anywhere in the list. This is signChanges_remove_middle_append lifted to evaluated polynomial
lists; iterating it deletes every interior member that vanishes at a critical point.
Middle irrelevance. Between two nonzero opposite signs, the middle entry's value does not
affect the count: changing it from m to m' is invisible (both reduce to the same list with the
middle deleted). This is the two-point form of interior cancellation — across a critical point an
interior member changes sign, but its fixed opposite neighbours absorb the change.
Middle irrelevance at the polynomial level (one interior member, at one point).
The key step: flank-reduction (the decoupled two-point comparison engine) #
From the Socratic analysis of a critical point: deleting interior chain members that vanish there
(each flanked by nonzero opposite neighbours) preserves the sign-variation count. Encode "delete
flanked middles" as an inductive relation; it preserves signChanges. The Sturm chain then only has
to exhibit the reductions — the combinatorics is decoupled.
xs reduces to ys by deleting middle entries flanked by nonzero opposite signs.
- refl (l : List SignType) : FlankReduce l l
- del (pre : List SignType) (a m b : SignType) (rest : List SignType) (ha : a ≠ 0) (hb : b ≠ 0) (hab : a ≠ b) {ys : List SignType} : FlankReduce (pre ++ a :: b :: rest) ys → FlankReduce (pre ++ a :: m :: b :: rest) ys
Instances For
Flank-reduction preserves the count. Each deletion is signChanges_remove_middle_append.
Lift to evaluated polynomial lists. If the sign patterns of two polynomial lists at z are
flank-related, their sign variations at z agree. This is where the decoupling pays off: the Sturm
chain only has to exhibit a FlankReduce between the c⁻ and c⁺ sign patterns.
Chain-level reduction step. If at z the flanking polynomials pa, pb carry nonzero
opposite signs, a FlankReduce of the list with the middle pm deleted extends to one of the full
list. Iterating this (one per interior vanisher) exhibits the reduction of the chain's sign pattern.
The flank hypotheses come from sign_neighbours_opposite_at_interior_root (chain structure).
FlankReduce is closed under consing. Prepending the same head to both sides preserves the
relation (every deletion's prefix just grows by one). Lets the chain-walk keep a fixed head while
reducing the tail.
Chain-walk reduction (the decoupled wall). Walking a polynomial chain a :: tail, every
interior member that vanishes at c — flanked (hflank) by neighbours of nonzero opposite sign
at the evaluation point z — is deleted: the sign pattern at z FlankReduces to that of the chain
with those interior vanishers removed (the head is always kept). hiso records that no two
consecutive members both vanish at c (Sturm chains satisfy this by consecutive coprimality);
hlast records that the final member does not vanish at c (Sturm's last member is a nonzero
constant). With these, the chain's c⁻/c⁺ sign patterns both reduce to the same kept chain.
Domain-wall view: signChanges as a local additive sum (BPR sign-variation theory in Lean) #
Following classical real-algebraic-geometry sign-variation theory (Basu–Pollack–Roy). On a
zero-free list,
signChanges equals the local count of adjacent disagreements (wallCount), an additive sum that
makes the two-point comparison decouple.
The local count of adjacent disagreements (±1 domain walls; no zeros dropped).
Equations
- Sturm.wallCount [] = 0
- Sturm.wallCount [head] = 0
- Sturm.wallCount (a :: b :: t) = (if a = b then 0 else 1) + Sturm.wallCount (b :: t)
Instances For
On a zero-free list, signChanges is the local additive wallCount (head recursion).
The additive bridge. signChanges s = wallCount (nonzero filtrate of s).
The ℤ/2 layer of sign variation. For a zero-free list, (-1)^(wallCount) telescopes to the
product of the endpoint signs: modulo 2, sign variation forgets everything but whether the ends
disagree. This is the engine behind Descartes' even-difference refinement and the transversality
(sign p') at a simple root — the single-bit shadow of the count.
Parity, clean form. A zero-free list has an even number of sign changes iff its endpoints
agree. The ±1 shadow: parity of sign variation = "do the ends match?".
Continuity sub-thread: signs are locally constant near a nonzero point #
Local sign constancy. Near a point where q is nonzero, q keeps its sign (continuity).
Flank-opposite persists near an interior root. At a root c of an interior member m
(coprime to its predecessor a), the predecessor and successor -(a % m) keep nonzero opposite
signs at every nearby z — exactly the flank hypotheses flankReduce_eval_step needs at c⁻ / c⁺
(not just at c).
Local root-crossing (closing P3 for one simple root, generic position) #
Just to the right of a simple root, p carries the sign of p'(α).
Just to the left of a simple root, p carries the opposite sign of p'(α).
Local root-crossing. Across a simple root α of p (generic position: α the only root
of p in [a,α] and [α,b], and no Sturm-tail member vanishes in [a,b]), the sign variations of
the Sturm sequence drop by exactly one.
Single critical point: the decoupled crossing (chain-walk + head-drop) #
Single-critical-point crossing. If c ∈ [a, b] is the only point of [a, b] where any
member of the Sturm sequence of a squarefree p vanishes (with p(a), p(b) ≠ 0), then V drops by
1 if p(c) = 0 and is unchanged otherwise. Both endpoints' sign patterns FlankReduce (via
flankReduce_chain_walk) to the same chain with the interior c-vanishers removed; on that reduced
chain only the head p can cross, so the change collapses to a single head-drop. This is the per-point
quantum of Sturm's theorem — and it also covers the case c = a/c = b (a tail member vanishing at an
endpoint, which then necessarily has p(c) ≠ 0) and the degenerate no-critical case.
Playground — what else falls out of the machinery #
Bolzano, sign form. A sign change of p between a and b forces a real root in [a,b]
(contrapositive of local constancy).
Bridge to Descartes' rule of signs. Mathlib's signVariations (sign changes of the
coefficient list) is exactly our signChanges of the mapped coefficient signs — so the whole
signChanges toolkit (zeros invisible, interior cancellation, head recursion) transfers to it.
A simple corollary of the bridge: the sign-variation count of the coefficients is invariant
under prepending a zero sign (the signChanges lemma, now visible for Descartes).
Sturm's theorem. For squarefree p and a < b with neither endpoint a root of p, the
drop in sign variations of the Sturm sequence equals the number of distinct real roots in (a, b].
Comparison with Mathlib's Polynomial.sturmSeq and List.signVariations #
For p ≠ 0, the chain sturmAux p q is Mathlib's Sturm sequence Polynomial.sturmSeq p q.
The Sturm sequence of p ≠ 0 is Mathlib's Polynomial.sturmSeq p (derivative p).
signVarAt L x is Mathlib's List.signVariations of the values of L at x.