// Copyright (C) 2026 Kiyotsugu Arai // SPDX-License-Identifier: LGPL-3.0-or-later // polynomial_roots.hpp // Numerical root-finding algorithms for polynomials // // Low degree (1 to 4): closed-form solutions robust against cancellation // High degree (5 and above): Jenkins-Traub, Laguerre, DKA iterative methods // // All functions return a vector of Complex (real roots have im=0) #ifndef SANGI_POLYNOMIAL_ROOTS_HPP #define SANGI_POLYNOMIAL_ROOTS_HPP #include #include #include #include #include #include #include namespace sangi { // ================================================================ // Internal helpers // ================================================================ namespace detail { // Sign function template T sgn(T val) { if (val > T(0)) return T(1); if (val < T(0)) return T(-1); return T(0); } // Cube root (real) template T cubeRoot(T x) { using std::cbrt; return cbrt(x); } } // namespace detail // ================================================================ // A-1. solveLinear (linear equation) // ================================================================ /// Returns the solution of the linear equation a*x + b = 0 /// p.coefficients(): [b, a] (ascending powers) template std::vector> solveLinear(const Polynomial& p) { assert(p.degree() == 1); T a = p[1]; // coefficient of x T b = p[0]; // constant term T root = -b / a; return { Complex(root) }; } // ================================================================ // A-2. solveQuadratic (quadratic equation) // ================================================================ /// Returns the solution of the quadratic equation a*x^2 + b*x + c = 0 /// Cancellation mitigation: compute the larger-magnitude root first, then the smaller root via Vieta's formulas /// [Source] Okumura, "Encyclopedia of Algorithms", p205 template std::vector> solveQuadratic(const Polynomial& p) { assert(p.degree() == 2); T a = p[2]; // coefficient of x^2 T b = p[1]; // coefficient of x T c = p[0]; // constant term // Make monic: x^2 + px + q = 0 T pp = b / a; T q = c / a; using std::sqrt; using std::abs; if (q == T(0)) { // x^2 + px = x(x+p) = 0 return { Complex(T(0)), Complex(-pp) }; } T halfP = pp / T(2); // p/2 T disc = halfP * halfP - q; // discriminant D/4 if (disc > T(0)) { // 2 real roots — cancellation mitigation // Compute the larger-magnitude root first T x1; if (halfP > T(0)) x1 = -halfP - sqrt(disc); else x1 = -halfP + sqrt(disc); T x2 = q / x1; // Vieta: x1*x2 = q return { Complex(x1), Complex(x2) }; } else if (disc < T(0)) { // Complex conjugate roots T re = -halfP; T im = sqrt(-disc); return { Complex(re, im), Complex(re, -im) }; } else { // Double root T x = -halfP; return { Complex(x), Complex(x) }; } } // ================================================================ // A-3. solveCubic (cubic equation) // ================================================================ /// Returns the solution of the cubic equation a*x^3 + b*x^2 + c*x + d = 0 /// Cardano's formula + trigonometric method (casus irreducibilis) template std::vector> solveCubic(const Polynomial& p) { assert(p.degree() == 3); using std::abs; using std::sqrt; using std::cbrt; using std::cos; using std::acos; using std::pow; const T pi = std::acos(T(-1)); T a = p[3]; T b = p[2]; T c = p[1]; T d = p[0]; // Make monic b /= a; c /= a; d /= a; // Tschirnhaus transformation: t = x + b/3 → t^3 + pt + q = 0 T b3 = b / T(3); T pp = c - b * b3; // p = c - b^2/3 T qq = d - b3 * c + T(2) * b3 * b3 * b3; // q = d - bc/3 + 2b^3/27 // Discriminant Δ = -(4p^3 + 27q^2) T disc = -(T(4) * pp * pp * pp + T(27) * qq * qq); std::vector> roots; if (disc > T(0)) { // Casus irreducibilis: 3 real roots → trigonometric method T r = sqrt(-pp / T(3)); // r = √(-p/3) T cosTheta = -qq / (T(2) * r * r * r); // cos(θ) = -q/(2r^3) // Clamp when numerical error pushes it beyond ±1 if (cosTheta > T(1)) cosTheta = T(1); if (cosTheta < T(-1)) cosTheta = T(-1); T theta = acos(cosTheta); T t0 = T(2) * r * cos(theta / T(3)); T t1 = T(2) * r * cos((theta + T(2) * pi) / T(3)); T t2 = T(2) * r * cos((theta + T(4) * pi) / T(3)); roots.push_back(Complex(t0 - b3)); roots.push_back(Complex(t1 - b3)); roots.push_back(Complex(t2 - b3)); } else if (disc < T(0)) { // 1 real root + complex conjugate roots T sqrtQ = qq * qq / T(4) + pp * pp * pp / T(27); T sqrtVal = sqrt(sqrtQ); T A = -qq / T(2) + sqrtVal; T B = -qq / T(2) - sqrtVal; A = detail::cubeRoot(A); B = detail::cubeRoot(B); T realRoot = A + B - b3; roots.push_back(Complex(realRoot)); // Remaining 2 roots: divide by (x - realRoot) and solve the quadratic // From t^3 + pt + q = (t - (A+B))(t^2 + (A+B)t + ...), // obtain the quadratic coefficients via synthetic division T sumAB = A + B; T re = -sumAB / T(2) - b3; T im = abs(A - B) * sqrt(T(3)) / T(2); roots.push_back(Complex(re, im)); roots.push_back(Complex(re, -im)); } else { // Δ = 0: repeated root(s) if (pp == T(0) && qq == T(0)) { // Triple root: t^3 = 0 T x = -b3; roots.push_back(Complex(x)); roots.push_back(Complex(x)); roots.push_back(Complex(x)); } else { // Double root + single root // When t³+pt+q=0 is (t-α)²(t+2α)=0, p=-3α², q=2α³ // → α = -3q/(2p), β = -2α = 3q/p T doubleRoot = -T(3) * qq / (T(2) * pp); T singleRoot = T(3) * qq / pp; roots.push_back(Complex(singleRoot - b3)); roots.push_back(Complex(doubleRoot - b3)); roots.push_back(Complex(doubleRoot - b3)); } } return roots; } // ================================================================ // A-4. solveQuartic (quartic equation) // ================================================================ /// Returns the solution of the quartic equation a*x^4 + b*x^3 + c*x^2 + d*x + e = 0 /// Ferrari's method: factor into two quadratics using a real root of the resolvent cubic template std::vector> solveQuartic(const Polynomial& p) { assert(p.degree() == 4); using std::abs; using std::sqrt; T a4 = p[4]; T a3 = p[3]; T a2 = p[2]; T a1 = p[1]; T a0 = p[0]; // Make monic a3 /= a4; a2 /= a4; a1 /= a4; a0 /= a4; // Depressed quartic: t = x - a3/4 // t^4 + pt^2 + qt + r = 0 T shift = a3 / T(4); T pp = a2 - T(6) * shift * shift; T qq = a1 - T(2) * a2 * shift + T(8) * shift * shift * shift; T rr = a0 - a1 * shift + a2 * shift * shift - T(3) * shift * shift * shift * shift; std::vector> roots; if (abs(qq) < std::numeric_limits::epsilon() * T(100)) { // qq ≈ 0 → biquadratic: t^4 + pt^2 + r = 0 // Letting u = t^2, u^2 + pu + r = 0 Polynomial biQuad(std::vector{rr, pp, T(1)}); auto uRoots = solveQuadratic(biQuad); for (const auto& u : uRoots) { auto sqrtU = sangi::sqrt(u); roots.push_back(Complex(sqrtU.re - shift, sqrtU.im)); roots.push_back(Complex(-sqrtU.re - shift, -sqrtU.im)); } return roots; } // Resolvent cubic: 8y^3 - 4py^2 - 8ry + (4pr - q^2) = 0 Polynomial resolvent(std::vector{ T(4) * pp * rr - qq * qq, T(-8) * rr, T(-4) * pp, T(8) }); auto cubicRoots = solveCubic(resolvent); // Select the largest among the real roots (numerical stability) T y0 = cubicRoots[0].re; for (size_t i = 1; i < cubicRoots.size(); ++i) { if (abs(cubicRoots[i].im) < std::numeric_limits::epsilon() * T(100)) { if (cubicRoots[i].re > y0) y0 = cubicRoots[i].re; } } // Factor into two quadratics // t^4 + pt^2 + qt + r = (t^2 + αt + β)(t^2 - αt + γ) // α^2 = 2y0 - p, β = y0 - q/(2α), γ = y0 + q/(2α) T alpha2 = T(2) * y0 - pp; T alpha; if (alpha2 > T(0)) { alpha = sqrt(alpha2); } else if (alpha2 > -std::numeric_limits::epsilon() * T(100)) { alpha = T(0); } else { // Fallback: tiny negative value due to numerical error alpha = sqrt(abs(alpha2)); } T beta, gamma; if (abs(alpha) > std::numeric_limits::epsilon() * T(100)) { beta = y0 - qq / (T(2) * alpha); gamma = y0 + qq / (T(2) * alpha); } else { // Case α ≈ 0 T sqrtY0sq_r = sqrt(abs(y0 * y0 - rr)); beta = y0 - sqrtY0sq_r; gamma = y0 + sqrtY0sq_r; } // Solve the two quadratic equations Polynomial quad1(std::vector{beta, alpha, T(1)}); Polynomial quad2(std::vector{gamma, -alpha, T(1)}); auto roots1 = solveQuadratic(quad1); auto roots2 = solveQuadratic(quad2); // Undo the shift for (auto& r : roots1) roots.push_back(Complex(r.re - shift, r.im)); for (auto& r : roots2) roots.push_back(Complex(r.re - shift, r.im)); return roots; } // ================================================================ // B-1. Jenkins-Traub method // ================================================================ /// Polynomial root-finding by the Jenkins-Traub method (based on TOMS493) /// The most robust polynomial root-finding method. A three-stage algorithm. /// Based on C. Bond's implementation template std::vector> jenkinsTraub(const Polynomial& poly) { using std::abs; using std::sqrt; using std::log; using std::exp; using std::cos; using std::sin; int degree = poly.degree(); if (degree < 1) return {}; // Build the coefficient array in descending order (TOMS493 convention) // poly is in ascending powers: coeffs_[0]=constant, coeffs_[n]=leading coefficient // op[0]=leading coefficient, op[n]=constant term std::vector op(degree + 1); for (int i = 0; i <= degree; ++i) op[i] = poly[degree - i]; // Invalid if the leading coefficient is zero if (op[0] == T(0)) return {}; // Storage for results std::vector zeror(degree), zeroi(degree); int foundCount = 0; // Working arrays int n = degree; std::vector p(n + 1), qp(n + 1), k(n + 1), qk(n + 1), svk(n + 1); std::vector temp(n + 1), pt(n + 1); // Machine constants const T base = T(2); const T eta = std::numeric_limits::epsilon(); const T infin = std::numeric_limits::max() / T(10); const T smalno = std::numeric_limits::min() * T(10); const T are = eta; const T mre = eta; const T lo = smalno / eta; // Rotation angle (94 degrees) T rot = T(94) * T(0.017453293); T cosr = cos(rot); T sinr = sin(rot); T xx = sqrt(T(0.5)); T yy = -xx; // Remove zero roots first while (n > 0 && op[n] == T(0)) { int j = degree - n; zeror[j] = T(0); zeroi[j] = T(0); foundCount++; n--; } if (n < 1) goto done; // Copy the coefficients into p for (int i = 0; i <= n; ++i) p[i] = op[i]; // Main root-finding loop { // Local variables T sr, si, u, v, a, b, c, d; T a1, a3, a7, ee, f, g, h; T szr, szi, lzr, lzi; // --- Define the internal functions as lambdas --- // quadsd: synthetic division of p by the quadratic (1, u, v) auto quadsd = [&](int nn, T uu, T vv, const std::vector& pp, std::vector& qq, T& aa, T& bb) { bb = pp[0]; qq[0] = bb; aa = pp[1] - bb * uu; qq[1] = aa; for (int i = 2; i <= nn; ++i) { T cc = pp[i] - aa * uu - bb * vv; qq[i] = cc; bb = aa; aa = cc; } }; // quad: solution of the quadratic equation a*z^2 + b1*z + c = 0 auto quad = [&](T qa, T b1, T qc, T& sr_out, T& si_out, T& lr_out, T& li_out) { if (qa == T(0)) { sr_out = (b1 != T(0)) ? -qc / b1 : T(0); lr_out = T(0); si_out = T(0); li_out = T(0); return; } if (qc == T(0)) { sr_out = T(0); lr_out = -b1 / qa; si_out = T(0); li_out = T(0); return; } // Avoid overflow in the discriminant T bb = b1 / T(2); T dd, eee; if (abs(bb) < abs(qc)) { eee = (qc < T(0)) ? -qa : qa; eee = bb * (bb / abs(qc)) - eee; dd = sqrt(abs(eee)) * sqrt(abs(qc)); } else { eee = T(1) - (qa / bb) * (qc / bb); dd = sqrt(abs(eee)) * abs(bb); } if (eee < T(0)) { // Complex conjugate roots sr_out = -bb / qa; lr_out = sr_out; si_out = abs(dd / qa); li_out = -si_out; } else { // Real roots if (bb >= T(0)) dd = -dd; lr_out = (-bb + dd) / qa; sr_out = (lr_out != T(0)) ? (qc / lr_out) / qa : T(0); si_out = T(0); li_out = T(0); } }; // calcsc: compute the scalar quantities int calcType; auto calcsc = [&]() { quadsd(n - 1, u, v, k, qk, c, d); if (abs(c) > abs(k[n - 1]) * T(100) * eta || abs(d) > abs(k[n - 2]) * T(100) * eta) { // type 1 or 2 if (abs(d) < abs(c)) { calcType = 1; ee = a / c; f = d / c; g = u * ee; h = v * b; a3 = a * ee + (h / c + g) * b; a1 = b - a * (d / c); a7 = a + g * d + h * f; } else { calcType = 2; ee = a / d; f = c / d; g = u * b; h = v * b; a3 = (a + g) * ee + h * (b / d); a1 = b * f - a; a7 = (f + u) * a + h; } } else { calcType = 3; } }; // nextk: compute the next K polynomial auto nextk = [&]() { if (calcType == 3) { k[0] = T(0); k[1] = T(0); for (int i = 2; i < n; ++i) k[i] = qk[i - 2]; return; } T tmp = (calcType == 1) ? b : a; if (abs(a1) <= abs(tmp) * eta * T(10)) { k[0] = T(0); k[1] = -a7 * qp[0]; for (int i = 2; i < n; ++i) k[i] = a3 * qk[i - 2] - a7 * qp[i - 1]; return; } a7 /= a1; a3 /= a1; k[0] = qp[0]; k[1] = qp[1] - a7 * qp[0]; for (int i = 2; i < n; ++i) k[i] = a3 * qk[i - 2] - a7 * qp[i - 1] + qp[i]; }; // newest: estimate the new u, v auto newest = [&](T& uu, T& vv) { if (calcType == 3) { uu = T(0); vv = T(0); return; } T a4_, a5_; if (calcType == 2) { a4_ = (a + g) * f + h; a5_ = (f + u) * c + v * d; } else { a4_ = a + u * b + h * f; a5_ = c + (u + v * f) * d; } T b1_ = -k[n - 1] / p[n]; T b2_ = -(k[n - 2] + b1_ * p[n - 1]) / p[n]; T c1_ = v * b2_ * a1; T c2_ = b1_ * a7; T c3_ = b1_ * b1_ * a3; T c4_ = c1_ - c2_ - c3_; T tmp_ = a5_ + b1_ * a4_ - c4_; if (tmp_ == T(0)) { uu = T(0); vv = T(0); return; } uu = u - (u * (c3_ + c2_) + v * (b1_ * a1 + b2_ * a7)) / tmp_; vv = v * (T(1) + c4_ / tmp_); }; // realit: iteration for a real zero auto realit = [&](T sss, int& nz, int& iflag) { nz = 0; T s = sss; iflag = 0; int j = 0; T omp = T(0), t = T(0); while (true) { T pv = p[0]; qp[0] = pv; for (int i = 1; i <= n; ++i) { pv = pv * s + p[i]; qp[i] = pv; } T mp = abs(pv); T ms = abs(s); T eee = (mre / (are + mre)) * abs(qp[0]); for (int i = 1; i <= n; ++i) eee = eee * ms + abs(qp[i]); if (mp <= T(20) * ((are + mre) * eee - mre * mp)) { nz = 1; szr = s; szi = T(0); return; } j++; if (j > 10) return; if (j >= 2) { if (abs(t) <= T(0.001) * abs(s - t) && mp >= omp) { iflag = 1; sss = s; return; } } omp = mp; // Update the K polynomial T kv = k[0]; qk[0] = kv; for (int i = 1; i < n; ++i) { kv = kv * s + k[i]; qk[i] = kv; } if (abs(kv) <= abs(k[n - 1]) * T(10) * eta) { k[0] = T(0); for (int i = 1; i < n; ++i) k[i] = qk[i - 1]; } else { t = -pv / kv; k[0] = qp[0]; for (int i = 1; i < n; ++i) k[i] = t * qk[i - 1] + qp[i]; } kv = k[0]; for (int i = 1; i < n; ++i) kv = kv * s + k[i]; t = T(0); if (abs(kv) > abs(k[n - 1]) * T(10) * eta) t = -pv / kv; s += t; } }; // quadit: iteration for a quadratic factor auto quadit = [&](T uu, T vv, int& nz) { nz = 0; int tried = 0; u = uu; v = vv; int j = 0; T omp = T(0), relstp = T(0); while (true) { quad(T(1), u, v, szr, szi, lzr, lzi); if (abs(abs(szr) - abs(lzr)) > T(0.01) * abs(lzr)) return; quadsd(n, u, v, p, qp, a, b); T mp = abs(a - szr * b) + abs(szi * b); // Upper bound on the round-off error T zm = sqrt(abs(v)); T eee = T(2) * abs(qp[0]); T t = -szr * b; for (int i = 1; i < n; ++i) eee = eee * zm + abs(qp[i]); eee = eee * zm + abs(a + t); eee *= (T(5) * mre + T(4) * are); eee -= (T(5) * mre + T(2) * are) * (abs(a) + t) + abs(b) * zm; // HVE fix eee += T(2) * are * abs(t); if (mp <= T(20) * eee) { nz = 2; return; } j++; if (j > 20) return; if (j >= 2 && !(relstp > T(0.01) || mp < omp || tried)) { if (relstp < eta) relstp = eta; relstp = sqrt(relstp); u = u - u * relstp; v = v + v * relstp; quadsd(n, u, v, p, qp, a, b); for (int i = 0; i < 5; ++i) { calcsc(); nextk(); } tried = 1; j = 0; } omp = mp; calcsc(); nextk(); calcsc(); T ui, vi; newest(ui, vi); if (vi == T(0)) return; relstp = abs((vi - v) / vi); u = ui; v = vi; } }; // fxshfr: fixed-shift iteration auto fxshfr = [&](int l2, int& nz) { nz = 0; T betav = T(0.25), betas = T(0.25); T oss = sr, ovv = v; T otv = T(1), ots = T(1); quadsd(n, u, v, p, qp, a, b); calcsc(); for (int j = 0; j < l2; ++j) { nextk(); calcsc(); T ui, vi; newest(ui, vi); T vv = vi; T ss = T(0); if (k[n - 1] != T(0)) ss = -p[n] / k[n - 1]; T tv = T(1), ts = T(1); if (j > 0 && calcType != 3) { if (vv != T(0)) tv = abs((vv - ovv) / vv); if (ss != T(0)) ts = abs((ss - oss) / ss); T tvv = (tv < otv) ? tv * otv : T(1); T tss = (ts < ots) ? ts * ots : T(1); int vpass = (tvv < betav) ? 1 : 0; int spass = (tss < betas) ? 1 : 0; if (spass || vpass) { T svu = u, svv_ = v; for (int i = 0; i < n; ++i) svk[i] = k[i]; T s = ss; int vtry = 0, stry = 0; // Try whichever converges faster bool tryQuad = !(spass && (!vpass || tss < tvv)); if (tryQuad) goto try_quad; goto try_real; try_real: { int iflag = 0; realit(s, nz, iflag); if (nz > 0) return; stry = 1; betas *= T(0.25); if (iflag != 0) { ui = -(s + s); vi = s * s; goto try_quad; } goto restore; } try_quad: quadit(ui, vi, nz); if (nz > 0) return; vtry = 1; betav *= T(0.25); if (!stry && spass) { for (int i = 0; i < n; ++i) k[i] = svk[i]; goto try_real; } restore: u = svu; v = svv_; for (int i = 0; i < n; ++i) k[i] = svk[i]; if (vpass && !vtry) goto try_quad; quadsd(n, u, v, p, qp, a, b); calcsc(); } } ovv = vv; oss = ss; otv = tv; ots = ts; } }; // --- Main loop --- auto findRoots = [&]() { while (n > 2) { // Coefficient scaling T maxCoef = T(0), minCoef = infin; for (int i = 0; i <= n; ++i) { T x = abs(p[i]); if (x > maxCoef) maxCoef = x; if (x != T(0) && x < minCoef) minCoef = x; } T sc = lo / minCoef; bool doScale = true; if (sc > T(1) && infin / sc < maxCoef) doScale = false; if (sc <= T(1)) { if (maxCoef < T(10)) doScale = false; if (sc == T(0)) sc = smalno; } if (doScale) { int l = static_cast(log(sc) / log(base) + T(0.5)); T factor = (l >= 0) ? std::pow(base, static_cast(l)) : std::pow(T(1) / base, static_cast(-l)); if (factor != T(1)) { for (int i = 0; i <= n; ++i) p[i] *= factor; } } // Compute a lower bound on the roots for (int i = 0; i <= n; ++i) pt[i] = abs(p[i]); pt[n] = -pt[n]; T x = exp((log(-pt[n]) - log(pt[0])) / static_cast(n)); if (pt[n - 1] != T(0)) { T xm = -pt[n] / pt[n - 1]; if (xm < x) x = xm; } // Shrink the interval while (true) { T xm = x * T(0.1); T ff = pt[0]; for (int i = 1; i <= n; ++i) ff = ff * xm + pt[i]; if (ff <= T(0)) break; x = xm; } T dx = x; // Refine the lower bound by Newton iteration while (abs(dx / x) > T(0.005)) { T ff = pt[0], df = ff; for (int i = 1; i < n; ++i) { ff = ff * x + pt[i]; df = df * x + ff; } ff = ff * x + pt[n]; dx = ff / df; x -= dx; } T bnd = x; // Stage 1: no-shift K polynomial (5 steps) int nm1 = n - 1; for (int i = 1; i < n; ++i) k[i] = static_cast(n - i) * p[i] / static_cast(n); k[0] = p[0]; T aa = p[n], bb = p[n - 1]; bool zeroK = (k[n - 1] == T(0)); for (int jj = 0; jj < 5; ++jj) { T cc = k[n - 1]; if (!zeroK) { T t = -aa / cc; for (int i = 0; i < nm1; ++i) { int j = n - i - 1; k[j] = t * k[j - 1] + p[j]; } k[0] = p[0]; zeroK = (abs(k[n - 1]) <= abs(bb) * eta * T(10)); } else { for (int i = 0; i < nm1; ++i) { int j = n - i - 1; k[j] = k[j - 1]; } k[0] = T(0); zeroK = (k[n - 1] == T(0)); } } // Save the K polynomial for (int i = 0; i < n; ++i) temp[i] = k[i]; // Stage 2-3: shifted iteration (up to 20 shifts) bool found = false; for (int cnt = 0; cnt < 20; ++cnt) { T xxx = cosr * xx - sinr * yy; yy = sinr * xx + cosr * yy; xx = xxx; sr = bnd * xx; si = bnd * yy; u = T(-2) * sr; v = bnd; int nz = 0; fxshfr(20 * (cnt + 1), nz); if (nz != 0) { int j = degree - n; zeror[j] = szr; zeroi[j] = szi; foundCount += nz; n -= nz; for (int i = 0; i <= n; ++i) p[i] = qp[i]; if (nz == 2) { zeror[j + 1] = lzr; zeroi[j + 1] = lzi; } found = true; break; } // Restore for (int i = 0; i < n; ++i) k[i] = temp[i]; } if (!found) break; // did not converge within 20 shifts } // Remaining linear or quadratic if (n == 1) { zeror[degree - 1] = -p[1] / p[0]; zeroi[degree - 1] = T(0); foundCount++; } else if (n == 2) { T sr_, si_, lr_, li_; quad(p[0], p[1], p[2], sr_, si_, lr_, li_); zeror[degree - 2] = sr_; zeroi[degree - 2] = si_; zeror[degree - 1] = lr_; zeroi[degree - 1] = li_; foundCount += 2; } }; findRoots(); } done: // Convert the result into a vector of Complex std::vector> result; result.reserve(foundCount); for (int i = 0; i < foundCount; ++i) result.push_back(Complex(zeror[i], zeroi[i])); return result; } // ================================================================ // B-2. Laguerre method // ================================================================ /// Polynomial root-finding by the Laguerre method /// Finds one root at a time with cubic convergence, lowering the degree by deflation (factor removal) template std::vector> laguerre( const Polynomial& poly, T eps = std::numeric_limits::epsilon() * T(100), size_t maxIter = 1000) { using std::abs; using std::sqrt; int n = poly.degree(); if (n < 1) return {}; // Convert to a complex-coefficient polynomial std::vector> coeffs(n + 1); for (int i = 0; i <= n; ++i) coeffs[i] = Complex(poly[i]); std::vector> roots; // Evaluate the complex polynomial by Horner's method auto evalPoly = [](const std::vector>& c, const Complex& z) { int deg = static_cast(c.size()) - 1; Complex val = c[deg]; for (int i = deg - 1; i >= 0; --i) val = val * z + c[i]; return val; }; // Evaluate the derivative auto evalDeriv = [](const std::vector>& c, const Complex& z) { int deg = static_cast(c.size()) - 1; Complex val = c[deg] * T(deg); for (int i = deg - 1; i >= 1; --i) val = val * z + c[i] * T(i); return val; }; // Evaluate the second derivative auto evalDeriv2 = [](const std::vector>& c, const Complex& z) { int deg = static_cast(c.size()) - 1; Complex val = c[deg] * T(deg) * T(deg - 1); for (int i = deg - 1; i >= 2; --i) val = val * z + c[i] * T(i) * T(i - 1); return val; }; while (n > 2) { T tn = static_cast(n); T tn1 = static_cast(n - 1); // Initial value: a nontrivial point using the Cauchy root bound r = 1 + max|a_i/a_n| T maxRatio = T(0); for (int i = 0; i < n; ++i) { T ratio = sangi::abs(coeffs[i]) / sangi::abs(coeffs[n]); if (ratio > maxRatio) maxRatio = ratio; } T r0 = T(1) + maxRatio; // Initial value off the real axis (using a complex number also lets complex roots be found) Complex z(r0 * T(0.4), r0 * T(0.9)); for (size_t iter = 0; iter < maxIter; ++iter) { Complex zo = z; Complex pz = evalPoly(coeffs, z); Complex dpz = evalDeriv(coeffs, z); Complex ddpz = evalDeriv2(coeffs, z); // Laguerre correction formula (division-avoiding form) // H_tilde = (n-1)^2 * P'(z)^2 - n(n-1) * P(z) * P''(z) Complex H = tn1 * (tn1 * (dpz * dpz) - tn * pz * ddpz); Complex sqrtH = sangi::sqrt(H); Complex d1 = dpz + sqrtH; Complex d2 = dpz - sqrtH; // Choose the larger-magnitude one (numerical stability) Complex denom = (normSq(d1) >= normSq(d2)) ? d1 : d2; if (normSq(denom) == T(0)) { // Denominator is zero → add a perturbation and retry z += Complex(r0 * T(0.1), r0 * T(0.1)); continue; } Complex delta = Complex(tn) * pz / denom; z -= delta; if (sangi::abs(z - zo) < eps) break; } roots.push_back(z); n--; // Deflation: divide P by (x - z) std::vector> newCoeffs(n + 1); newCoeffs[n] = coeffs[n + 1]; for (int i = n - 1; i >= 0; --i) newCoeffs[i] = coeffs[i + 1] + newCoeffs[i + 1] * z; coeffs = std::move(newCoeffs); } // Remaining linear or quadratic if (n == 1) { roots.push_back(-coeffs[0] / coeffs[1]); } else if (n == 2) { Complex a = coeffs[2]; Complex b = coeffs[1]; Complex c = coeffs[0]; Complex disc = sangi::sqrt(b * b - Complex(T(4)) * a * c); roots.push_back((-b + disc) / (Complex(T(2)) * a)); roots.push_back((-b - disc) / (Complex(T(2)) * a)); } return roots; } // ================================================================ // B-3. Durand-Kerner-Aberth (DKA) method // ================================================================ /// Polynomial root-finding by the Aberth-Ehrlich method (all roots simultaneously, cubic convergence) /// /// Update formula: w[i] = P(z[i]) / P'(z[i]) /// z[i] -= w[i] / (1 - w[i] * Σ_{j≠i} 1/(z[i]-z[j])) /// Unlike the plain DK method (quadratic convergence), it uses P'/P information, /// so convergence stays stable even near repeated roots. /// /// After convergence, Newton polish: refine each root with the original polynomial. template std::vector> durandKernerAberth( const Polynomial& poly, T eps = std::numeric_limits::epsilon() * T(100), size_t maxIter = 1000) { using std::abs; using std::sqrt; using std::cos; using std::sin; const T pi = std::acos(T(-1)); int N = poly.degree(); if (N < 1) return {}; if (N == 1) return solveLinear(poly); if (N == 2) return solveQuadratic(poly); // Make monic T lc = poly.leadingCoefficient(); std::vector coeffs(N + 1); for (int i = 0; i <= N; ++i) coeffs[i] = poly[i] / lc; // Centroid shift of the roots: β = -a_{N-1}/N T beta = -coeffs[N - 1] / static_cast(N); // Build the shifted polynomial: P(x + β) // Taylor shift by Horner's method std::vector shifted(N + 1); for (int i = 0; i <= N; ++i) shifted[i] = coeffs[i]; for (int j = 0; j < N; ++j) { for (int i = N - 1; i >= j; --i) { shifted[i] += beta * shifted[i + 1]; } } // Remove zero roots int zeroRoots = 0; while (zeroRoots < N && abs(shifted[zeroRoots]) < eps * T(100)) { zeroRoots++; } int M = N - zeroRoots; // effective degree if (M == 0) { std::vector> result(N, Complex(beta)); return result; } std::vector workCoeffs(M + 1); for (int i = 0; i <= M; ++i) workCoeffs[i] = shifted[i + zeroRoots]; // Evaluate P(z) and P'(z) simultaneously by Horner's method auto evalPP = [&](const Complex& z) -> std::pair, Complex> { Complex p(workCoeffs[M]); Complex dp(T(0)); for (int i = M - 1; i >= 0; --i) { dp = dp * z + p; p = p * z + Complex(workCoeffs[i]); } return { p, dp }; }; // Initial values: placed non-uniformly on the circle of the Fujiwara upper-bound radius // Fujiwara bound: 2 * max_i( |a_i/a_n|^(1/(n-i)) ) // workCoeffs is monic (a_n = 1) T r = T(0); for (int i = 0; i < M; ++i) { T val = std::pow(abs(workCoeffs[i]), T(1) / static_cast(M - i)); if (val > r) r = val; } r = std::max(T(1), T(2) * r); std::vector> X(M); for (int i = 0; i < M; ++i) { // Avoid accidental symmetry in the initial values via an irrational angle offset T theta = T(2) * pi * static_cast(i) / static_cast(M) + T(0.4) / static_cast(M); X[i] = Complex(r * cos(theta), r * sin(theta)); } // Aberth-Ehrlich iteration // High-degree polynomials may require many iterations to converge size_t maxAberthIter = std::max(maxIter, static_cast(M) * 50); T epsSq = eps * eps; for (size_t iter = 0; iter < maxAberthIter; ++iter) { bool converged = true; for (int i = 0; i < M; ++i) { auto [pz, dpz] = evalPP(X[i]); // Newton correction: w = P(z) / P'(z) T dpNorm = normSq(dpz); if (dpNorm < epsSq * epsSq) continue; Complex w = pz / dpz; // Aberth correction: Σ 1/(z[i] - z[j]) Complex aberth_sum(T(0)); for (int j = 0; j < M; ++j) { if (j == i) continue; Complex dz = X[i] - X[j]; T dzNorm = normSq(dz); if (dzNorm > epsSq * epsSq) aberth_sum += Complex(T(1)) / dz; } Complex denom = Complex(T(1)) - w * aberth_sum; Complex offset; if (normSq(denom) < epsSq * epsSq) { // On degeneracy, fall back to the Newton step offset = w; } else { offset = w / denom; } // Relative convergence test: |offset| > eps * (|z| + 1) T zi_scale = abs(X[i]) + T(1); if (abs(offset) > eps * zi_scale) converged = false; X[i] -= offset; } if (converged) break; } // Undo the shift + add the zero roots std::vector> result; result.reserve(N); for (int i = 0; i < M; ++i) result.push_back(X[i] + Complex(beta)); for (int i = 0; i < zeroRoots; ++i) result.push_back(Complex(beta)); // Newton polish: refine each root with the original polynomial Polynomial dp = poly.derivative(); for (auto& z : result) { for (int k = 0; k < 5; ++k) { Complex pz = poly(z); Complex dpz = dp(z); if (normSq(dpz) < epsSq * epsSq) break; Complex corr = pz / dpz; z -= corr; if (normSq(corr) <= epsSq * normSq(z)) break; } // Drop the imaginary part of a real root if (abs(z.im) < eps * (T(1) + abs(z.re))) z.im = T(0); } return result; } // ================================================================ // B-3b. Durand-Kerner (Weierstrass) method // ================================================================ /// Polynomial root-finding by the Durand-Kerner (Weierstrass) method (all roots simultaneously, classical) /// /// Updates each root simultaneously by z_i ← z_i − P(z_i) / (lc·Π_{j≠i}(z_i − z_j)). /// A classical simultaneous iteration without the Aberth correction, more naive than durandKernerAberth /// (quadratic convergence). Starting from the complex initial values (0.4+0.9i)^i, it converges to all roots when there are no repeated roots. /// /// References: Durand (1960), Kerner (1966), Weierstrass (1891) template std::vector> weierstrass( const Polynomial& poly, T eps = std::numeric_limits::epsilon() * T(100), size_t maxIter = 1000) { using std::abs; int N = poly.degree(); if (N < 1) return {}; if (N == 1) return solveLinear(poly); if (N == 2) return solveQuadratic(poly); const T lc = poly.leadingCoefficient(); // Initial values: (0.4 + 0.9i)^i (a classical seed: non-real, off the unit circle) std::vector> z(N); Complex seed(T(0.4), T(0.9)); Complex cur(T(1)); for (int i = 0; i < N; ++i) { cur = cur * seed; z[i] = cur; } for (size_t iter = 0; iter < maxIter; ++iter) { T maxCorr = T(0); for (int i = 0; i < N; ++i) { // Gauss-Seidel (apply updates immediately) Complex denom(T(1)); for (int j = 0; j < N; ++j) if (j != i) denom = denom * (z[i] - z[j]); if (abs(denom) < eps * eps) continue; // roots too close together Complex w = poly(z[i]) / (Complex(lc) * denom); // includes making monic z[i] = z[i] - w; T c = abs(w); if (c > maxCorr) maxCorr = c; } if (maxCorr < eps) break; } // Drop the imaginary part of real roots for (auto& zi : z) if (abs(zi.im) < eps * (T(1) + abs(zi.re))) zi.im = T(0); return z; } // ================================================================ // B-3c. Graeffe root-squaring method // ================================================================ /// Polynomial root-finding by Graeffe's root-squaring method (for real roots with well-separated moduli) /// /// Repeatedly forms p(x)·p(−x) = s(x²), squaring the roots r_i → r_i^{2^m}. /// For roots with separated moduli, the monic coefficient ratio after squaring collapses to the dominant term, so /// the modulus can be read off as |r_i| ≈ |a_{n−i}/a_{n−i+1}|^{1/2^m}. The sign of a real root is /// recovered from P(±|r_i|), and finally polished with a few Newton steps. /// /// Note (scope): equal-modulus roots, complex conjugate pairs, and repeated roots cannot be separated by /// basic Graeffe (this implementation targets real roots with separated moduli). Because coefficients grow /// doubly exponentially, each squaring is monic-normalized, and the number of squarings is limited by maxSquarings. A historical method. /// /// @param maxSquarings number of squarings (default 6 = exponent 2^6=64) template std::vector> graeffe( const Polynomial& poly, int maxSquarings = 6, T eps = std::numeric_limits::epsilon() * T(100)) { using std::abs; int n = poly.degree(); if (n < 1) return {}; if (n == 1) return solveLinear(poly); if (n == 2) return solveQuadratic(poly); const T tiny = std::numeric_limits::epsilon() * T(10); // Monic coefficients a[0..n], a[n]=1 std::vector a(n + 1); const T lc = poly.leadingCoefficient(); for (int i = 0; i <= n; ++i) a[i] = poly[i] / lc; int sq = 0; for (sq = 0; sq < maxSquarings; ++sq) { // r(x) = a(x)·a(−x) = Σ_{i,j} a_i a_j (−1)^j x^{i+j} std::vector r(2 * n + 1, T(0)); for (int i = 0; i <= n; ++i) { T ai = a[i]; if (ai == T(0)) continue; for (int j = 0; j <= n; ++j) { T sign = (j % 2 == 0) ? T(1) : T(-1); r[i + j] += ai * a[j] * sign; } } // s(y) = even-degree extraction of r (y = x²) std::vector b(n + 1); for (int k = 0; k <= n; ++k) b[k] = r[2 * k]; T bn = b[n]; if (std::abs(bn) < tiny) break; // degenerate for (int k = 0; k <= n; ++k) b[k] /= bn; // make monic (b[n]=±1) // Overflow guard: coefficients grow doubly exponentially, so stop before exceeding the double range. // Making monic normalizes only the leading coefficient while the others grow, so squaring further would yield inf. T mx = T(0); for (int k = 0; k <= n; ++k) mx = std::max(mx, std::abs(b[k])); if (!std::isfinite(mx) || mx > T(1e150)) break; // keep a and sq at the previous stage a = b; } // |r_i| = |a_{n-i}/a_{n-i+1}|^{1/2^sq} const T E = std::pow(T(2), T(sq)); std::vector> roots; roots.reserve(n); for (int i = 1; i <= n; ++i) { T ratio = std::abs(a[n - i]) / (std::abs(a[n - i + 1]) + tiny); T mag = std::pow(ratio, T(1) / E); // Sign recovery: whichever of P(+mag) and P(-mag) is smaller T r0 = (std::abs(poly(mag)) <= std::abs(poly(-mag))) ? mag : -mag; // Polish with a few Newton steps (on the original polynomial) Polynomial dp = poly.derivative(); for (int k = 0; k < 8; ++k) { T pv = poly(r0), dpv = dp(r0); if (std::abs(dpv) < tiny) break; T step = pv / dpv; r0 -= step; if (std::abs(step) < eps * (T(1) + std::abs(r0))) break; } roots.push_back(Complex(r0)); } return roots; } // ================================================================ // B-3d. Lehmer-Schur method // ================================================================ namespace detail_lehmer { // Whether there is a root inside the unit disk |w|<1 (Lehmer's Schur-Cohn test, boolean). // q: complex coefficients (ascending powers). Iterate the Schur transform Tq = conj(a0)·q − an·q* // until the degree drops; if |a0|²−|an|²<0 occurs along the way, there is a root inside. Degeneracy (|a0|≈|an|) is // conservatively treated as true (so as not to miss roots). template bool hasRootInUnitDisk(std::vector> q, T tol) { using std::abs; while (q.size() >= 2) { while (q.size() >= 2 && normSq(q.back()) <= tol * tol) q.pop_back(); if (q.size() < 2) break; const std::size_t n = q.size() - 1; T scale = T(0); for (auto& c : q) scale = std::max(scale, normSq(c)); if (scale <= tol * tol) break; T d = normSq(q[0]) - normSq(q[n]); // |a0|² − |an|² if (d < -tol * scale) return true; // root inside if (d <= tol * scale) return true; // degenerate → conservatively true // Schur transform T q = conj(a0)·q − an·q* (degree n−1) std::vector> tq(n); const Complex a0 = q[0], an = q[n]; for (std::size_t k = 0; k < n; ++k) tq[k] = conj(a0) * q[k] - an * conj(q[n - k]); q.swap(tq); } return false; } // Returns the complex coefficients (ascending powers) of P(c + r·w) via Taylor shift + scale. template std::vector> shiftScale(const std::vector>& a, Complex c, T r) { const std::size_t n = a.size() - 1; std::vector> b = a; for (std::size_t i = 0; i < n; ++i) // Taylor shift P(c+t) for (std::size_t j = n - 1; ; --j) { b[j] = b[j] + c * b[j + 1]; if (j == i) break; } Complex rk(T(1)); // scale by t = r·w for (std::size_t k = 0; k <= n; ++k) { b[k] = b[k] * rk; rk = rk * Complex(r); } return b; } // Evaluate a complex-coefficient polynomial by Horner. template Complex evalComplex(const std::vector>& a, Complex z) { Complex p = a.back(); for (std::size_t i = a.size() - 1; i-- > 0; ) p = p * z + a[i]; return p; } // Newton polish on a complex-coefficient polynomial. template Complex newtonPolish(const std::vector>& a, Complex z, int iters, T tol) { const std::size_t n = a.size() - 1; for (int it = 0; it < iters; ++it) { Complex p = a[n], dp(T(0)); for (std::size_t i = n; i-- > 0; ) { dp = dp * z + p; p = p * z + a[i]; } if (normSq(dp) <= tol * tol) break; Complex step = p / dp; z = z - step; if (normSq(step) <= tol * tol * (T(1) + normSq(z))) break; } return z; } // Synthetic division of a complex-coefficient polynomial by (w − root) (deflation). template std::vector> deflate(const std::vector>& a, Complex root) { const std::size_t n = a.size() - 1; std::vector> b(n); b[n - 1] = a[n]; for (std::size_t k = n - 1; k-- > 0; ) b[k] = a[k + 1] + root * b[k + 1]; return b; } } // namespace detail_lehmer /// Polynomial root-finding by the Lehmer-Schur method (disk subdivision in the complex plane) /// /// Starting from a disk containing all roots (Cauchy bound), cover the disk with 9 sub-disks of half the radius /// (center + 8 surrounding), apply the Schur-Cohn test (Lehmer's unit-disk /// test) to each sub-disk, and descend into the sub-disk that contains a root. Once the radius is small enough, use the center /// as a seed for Newton polish → deflation, repeating this degree-many times to obtain all roots. /// /// The test is based on the exact Schur-Cohn transform. Since seed accuracy comes from the subdivision and root accuracy from Newton polish, /// even if the cover has some gaps, deflation can still extract all roots. /// /// References: Lehmer (1961), Schur-Cohn test template std::vector> lehmerSchur( const Polynomial& poly, T tol = std::numeric_limits::epsilon() * T(100), size_t maxDescend = 200) { using std::abs; using namespace detail_lehmer; int N = poly.degree(); if (N < 1) return {}; if (N == 1) return solveLinear(poly); if (N == 2) return solveQuadratic(poly); std::vector> orig(N + 1), work; for (int i = 0; i <= N; ++i) orig[i] = Complex(poly[i]); work = orig; // Cauchy bound: all roots lie in |z| < R T an = abs(orig[N]); if (!(an > T(0))) return {}; T m = T(0); for (int i = 0; i < N; ++i) m = std::max(m, abs(orig[i])); const T R = T(1) + m / an; const T seedTol = std::max(tol, T(1e-8) * R); std::vector> roots; roots.reserve(N); for (int found = 0; found < N; ++found) { const std::size_t deg = work.size() - 1; if (deg == 1) { // linear: solve directly roots.push_back(Complex(T(0)) - work[0] / work[1]); break; } // Descend by disk subdivision into the sub-disk containing a root Complex c(T(0)); T r = R; for (size_t step = 0; step < maxDescend && r > seedTol; ++step) { // Cover: center (c, r/2) + 8 surrounding (c + 0.75r·e^{iπk/4}, r/2) Complex centers[9]; centers[0] = c; for (int k = 0; k < 8; ++k) { T ang = T(2) * std::acos(T(-1)) * T(k) / T(8); centers[k + 1] = c + Complex(T(0.75) * r * std::cos(ang), T(0.75) * r * std::sin(ang)); } const T rr = r / T(2); bool moved = false; for (int k = 0; k < 9; ++k) { if (hasRootInUnitDisk(shiftScale(work, centers[k], rr), tol)) { c = centers[k]; r = rr; moved = true; break; } } if (!moved) { // Fallback: move to the center with the smallest |P| and only shrink the radius int best = 0; T bestV = normSq(evalComplex(work, centers[0])); for (int k = 1; k < 9; ++k) { T v = normSq(evalComplex(work, centers[k])); if (v < bestV) { bestV = v; best = k; } } c = centers[best]; r = rr; } } Complex root = newtonPolish(orig, c, 80, tol); // polish on the original polynomial roots.push_back(root); work = deflate(work, root); } // Clean up the imaginary part of real roots for (auto& z : roots) if (abs(z.im) < tol * (T(1) + abs(z.re))) z.im = T(0); return roots; } // ================================================================ // B-4. Bairstow method // ================================================================ /// Polynomial root-finding by the Bairstow method (real-coefficient quadratic factorization) /// /// Iteratively performs synthetic division of a real-coefficient polynomial by (x² + r·x + s), /// correcting r, s by Newton's method to extract a quadratic factor. /// Since the computation stays in real coefficients, complex roots are obtained naturally as conjugate pairs. /// /// References: W.H. Press et al., Numerical Recipes (§9.5) template std::vector> bairstow( const Polynomial& poly, T eps = std::numeric_limits::epsilon() * T(100), size_t maxIter = 1000) { using std::abs; using std::sqrt; int deg = poly.degree(); if (deg < 1) return {}; if (deg == 1) return solveLinear(poly); if (deg == 2) return solveQuadratic(poly); // Descending-power coefficient array a[0]=highest degree, ..., a[n]=constant int n = deg; std::vector a(n + 1); for (int i = 0; i <= n; ++i) a[i] = poly[n - i]; std::vector> roots; roots.reserve(deg); while (n > 2) { // Initial estimate: based on the ratio of the low-order coefficients T r, s; if (abs(a[0]) > eps) { r = a[1] / a[0]; s = a[2] / a[0]; } else { r = T(1); s = T(1); } std::vector b(n + 1), c(n + 1); for (size_t iter = 0; iter < maxIter; ++iter) { // Synthetic division: divide P(x) by (x² + r·x + s) // b[j] = a[j] - r·b[j-1] - s·b[j-2] b[0] = a[0]; b[1] = a[1] - r * b[0]; for (int i = 2; i <= n; ++i) b[i] = a[i] - r * b[i - 1] - s * b[i - 2]; // For partial derivatives: apply the same recurrence to b // c[j] = b[j] - r·c[j-1] - s·c[j-2] // Result: ∂b[j]/∂r = -c[j-1], ∂b[j]/∂s = -c[j-2] c[0] = b[0]; c[1] = b[1] - r * c[0]; for (int i = 2; i <= n - 1; ++i) c[i] = b[i] - r * c[i - 1] - s * c[i - 2]; // Newton system: // [c[n-2] c[n-3]] [Δr] [b[n-1]] // [c[n-1] c[n-2]] [Δs] = [b[n] ] T cn2 = c[n - 2]; T cn3 = (n >= 3) ? ((n - 3 >= 0) ? c[n - 3] : T(0)) : T(0); T cn1 = c[n - 1]; T det = cn2 * cn2 - cn3 * cn1; if (abs(det) < eps * eps) { // Singular → add a perturbation and retry r += T(0.5); s -= T(0.5); continue; } T dr = (b[n - 1] * cn2 - b[n] * cn3) / det; T ds = (b[n] * cn2 - b[n - 1] * cn1) / det; r += dr; s += ds; if (abs(dr) < eps * (T(1) + abs(r)) && abs(ds) < eps * (T(1) + abs(s))) { break; } } // Find the roots of the quadratic factor x² + r·x + s T disc = r * r - T(4) * s; if (disc >= T(0)) { T sq = sqrt(disc); roots.push_back(Complex((-r + sq) / T(2))); roots.push_back(Complex((-r - sq) / T(2))); } else { T sq = sqrt(-disc); roots.push_back(Complex(-r / T(2), sq / T(2))); roots.push_back(Complex(-r / T(2), -sq / T(2))); } // Deflation: make the quotient polynomial b[0..n-2] the next a n -= 2; std::vector newA(n + 1); for (int i = 0; i <= n; ++i) newA[i] = b[i]; a = std::move(newA); } // Remaining linear or quadratic if (n == 1) { roots.push_back(Complex(-a[1] / a[0])); } else if (n == 2) { T disc = a[1] * a[1] - T(4) * a[0] * a[2]; if (disc >= T(0)) { T sq = sqrt(disc); roots.push_back(Complex((-a[1] + sq) / (T(2) * a[0]))); roots.push_back(Complex((-a[1] - sq) / (T(2) * a[0]))); } else { T sq = sqrt(-disc); roots.push_back(Complex(-a[1] / (T(2) * a[0]), sq / (T(2) * a[0]))); roots.push_back(Complex(-a[1] / (T(2) * a[0]), -sq / (T(2) * a[0]))); } } return roots; } // ================================================================ // C. Unified dispatcher // ================================================================ /// Find all roots of a polynomial (automatically selects the best algorithm by degree) /// /// Algorithm selection: /// Degree 1-4: closed-form solution (with cancellation mitigation) /// Degree 5-19: Jenkins-Traub (one root at a time + deflation, high precision) /// Degree 20 and above: Aberth-Ehrlich + Newton polish (all roots simultaneously, /// avoids accumulation of deflation error) template std::vector> solvePolynomial( const Polynomial& p, T eps = std::numeric_limits::epsilon() * T(100), size_t maxIter = 1000) { // Remove leading zero coefficients and find the effective degree int deg = p.degree(); if (deg < 0) return {}; // zero polynomial // Obtain the effective polynomial (already normalized, so degree() is correct) switch (deg) { case 0: return {}; // constant polynomial → no roots case 1: return solveLinear(p); case 2: return solveQuadratic(p); case 3: return solveCubic(p); case 4: return solveQuartic(p); default: // Degree 20 and above: DKA (all roots simultaneously + Newton polish) // Avoids accumulation of deflation error if (deg >= 20) return durandKernerAberth(p, eps, maxIter); // Degree 5-19: Jenkins-Traub (one root at a time, high precision) return jenkinsTraub(p); } } // ================================================================ // C'. Root-finding with convergence flags ─ *post-verifies* the result to report "did it find all roots / is the precision sufficient" // ================================================================ // // Motivation (TODO A.1): solvePolynomial / jenkinsTraub / durandKernerAberth / laguerre / bairstow // only return a vector of Complex, and do not tell the caller (a) whether *all* degree-many roots were // found (an iteration cutoff may silently return fewer = a counting hole), nor (b) how precise each root is. // Without changing the existing signatures at all (non-destructive), add a thin wrapper that *post-verifies* the result. // ★ Rather than "trusting the solver's internal converged flag", it actually substitutes the returned roots into the polynomial // and measures the residual |p(root)|. Hence even if the solver is buggy and returns too few / sloppy roots, converged will not // wrongly become true (the double-side counterpart of numeric_bridge's "don't trust generation, verify it" spirit). // If an exact (proven) guarantee is required, also use numeric_bridge::certifyBracketedRoot. template struct PolyRootsResult { std::vector> roots; // roots returned by the solver (real roots have im≈0) bool converged = false; // all roots found (found==expected) ∧ all residuals ≤ tol bool complete = false; // whether degree-many roots were returned (found==expected) std::size_t found = 0; // number of returned roots std::size_t expected = 0; // expected number of roots (= effective degree) T maxResidual = T(0); // max_i |p(root_i)| (over the found roots) }; /// Find all roots, post-verify the result, and return it with convergence/completeness flags (non-destructive, calls the existing API internally). /// residualTol: upper bound on |p(root)| (absolute residual). For large-scale polynomials, the caller may /// judge by its own criterion based on maxResidual (the raw value is also returned). template PolyRootsResult solvePolynomialChecked( const Polynomial& p, T residualTol = std::numeric_limits::epsilon() * T(1e4), T eps = std::numeric_limits::epsilon() * T(100), std::size_t maxIter = 1000) { PolyRootsResult r; int deg = p.degree(); if (deg < 0) return r; // zero polynomial → converged=false (ill-formed) r.expected = static_cast(deg); r.roots = solvePolynomial(p, eps, maxIter); r.found = r.roots.size(); r.complete = (r.found == r.expected); T maxres = T(0); for (const auto& z : r.roots) { T res = abs(p(z)); // |p(root)| (modulus of Complex, ADL) if (res > maxres) maxres = res; } r.maxResidual = maxres; r.converged = r.complete && (deg == 0 || maxres <= residualTol); return r; } // ================================================================ // D. Real root isolation by Vincent's method (Vincent Real Root Isolation) // ================================================================ /** * @brief Real-root isolation interval */ template struct RealRootInterval { T lower; ///< lower bound of the interval T upper; ///< upper bound of the interval bool exact; ///< if true, lower == upper (an exact root) }; namespace detail { /// Number of sign variations in a coefficient sequence (Descartes' rule of signs) template std::size_t signVariations(const std::vector& coeffs) { std::size_t count = 0; int last_sign = 0; // -1, 0, +1 for (const auto& c : coeffs) { int s = (c > T(0)) ? 1 : (c < T(0)) ? -1 : 0; if (s == 0) continue; if (last_sign != 0 && s != last_sign) ++count; last_sign = s; } return count; } /// Number of sign variations of a polynomial template std::size_t signVariations(const Polynomial& p) { return signVariations(p.coefficients()); } /// Taylor shift: compute p(x + c) (Horner scheme, O(n²)) template Polynomial taylorShift(const Polynomial& p, const T& c) { int n = p.degree(); if (n < 0) return p; // Transform coeffs[i] into the coefficients of p(x+c) std::vector a(p.coefficients()); for (int i = 0; i < n; ++i) { for (int j = n - 1; j >= i; --j) { a[j] += c * a[j + 1]; } } return Polynomial(std::move(a)); } /// Reciprocal polynomial: x^n * p(1/x) (reverse the coefficients) template Polynomial reciprocalPoly(const Polynomial& p) { auto c = p.coefficients(); std::reverse(c.begin(), c.end()); return Polynomial(std::move(c)); } /// Cauchy upper bound: upper bound on the positive real roots max(1, Σ|a_i/a_n|) template T cauchyBound(const Polynomial& p) { int n = p.degree(); if (n <= 0) return T(1); T lc = std::abs(p[n]); T bound = T(0); for (int i = 0; i < n; ++i) bound = std::max(bound, std::abs(p[i]) / lc); return T(1) + bound; } /// Square-free part: p / gcd(p, p') template Polynomial squareFree(const Polynomial& p) { auto dp = p.derivative(); if (dp.isZero()) return p; auto g = gcd(p, dp); if (g.degree() <= 0) return p; return p / g; } /// Refine a root by bisection template T bisectRoot(const Polynomial& p, T lo, T hi, T tol, std::size_t max_iter = 100) { T flo = p(lo), fhi = p(hi); if (std::abs(flo) <= tol) return lo; if (std::abs(fhi) <= tol) return hi; // If the signs are the same, return the midpoint if ((flo > T(0)) == (fhi > T(0))) return (lo + hi) / T(2); for (std::size_t i = 0; i < max_iter; ++i) { T mid = (lo + hi) / T(2); if (hi - lo < tol) return mid; T fm = p(mid); if (std::abs(fm) <= tol) return mid; if ((fm > T(0)) == (flo > T(0))) { lo = mid; flo = fm; } else { hi = mid; fhi = fm; } } return (lo + hi) / T(2); } /// Isolation of positive real roots (VCA bisection) /// Tracks the Möbius transform x = (a*t + b)/(c*t + d), t ∈ (0, ∞) template void vincentPositiveRoots( const Polynomial& p_orig, T upper_bound, std::vector>& result, T epsilon) { struct WorkItem { Polynomial q; // transformed polynomial T a, b, c, d; // Möbius parameters }; std::vector stack; stack.push_back({p_orig, T(1), T(0), T(0), T(1)}); std::size_t max_work = static_cast(p_orig.degree()) * 500; std::size_t work = 0; while (!stack.empty() && work < max_work) { ++work; auto [q, a, b, c, d] = std::move(stack.back()); stack.pop_back(); int deg = q.degree(); if (deg < 1) continue; // Normalize the leading coefficient to be positive if (q[deg] < T(0)) q = q * T(-1); std::size_t v = signVariations(q); if (v == 0) continue; if (v == 1) { // Find the positive real root of q and map it back to the original coordinate via the inverse Möbius transform T t_root; if (deg == 1) { // Linear: solve directly t_root = -q[0] / q[1]; } else { // Find the positive root of q by bisection T ub = cauchyBound(q); t_root = bisectRoot(q, T(0), ub, std::numeric_limits::epsilon() * T(100)); } if (t_root >= T(0)) { T x_root = (a * t_root + b) / (c * t_root + d); T margin = std::max(std::abs(x_root) * epsilon, epsilon); result.push_back({x_root - margin, x_root + margin, false}); } continue; } // Check for a root at t = 1 (= x = (a+b)/(c+d)) T val_at_1 = q(T(1)); if (std::abs(val_at_1) <= epsilon * (std::abs(q[deg]) + T(1))) { T exact_root = (a + b) / (c + d); result.push_back({exact_root, exact_root, true}); // Deflate by q / (x - 1) auto [quot, rem] = q.divmod(Polynomial({T(-1), T(1)})); q = std::move(quot); if (q.degree() < 1) continue; if (q[q.degree()] < T(0)) q = q * T(-1); v = signVariations(q); if (v == 0) continue; } // (1, ∞) part: t → t + 1, q₁(t) = q(t + 1) // Möbius: (a(t+1)+b)/(c(t+1)+d) = (at+(a+b))/(ct+(c+d)) auto q1 = taylorShift(q, T(1)); stack.push_back({std::move(q1), a, a + b, c, c + d}); // (0, 1) part: t → 1/(t+1) // q₂(t) = (t+1)^n * q(1/(t+1)) = taylorShift(reciprocal(q), 1) // Möbius: (bt+(a+b))/(dt+(c+d)) auto q2 = taylorShift(reciprocalPoly(q), T(1)); stack.push_back({std::move(q2), b, a + b, d, c + d}); } } } // namespace detail /** * @brief Real root isolation by Vincent's method * * Isolates all real roots of a polynomial into mutually disjoint intervals. * Uses the bisection version of the Vincent-Collins-Akritas (VCA) algorithm. * * Each interval contains exactly one real root. * When exact == true, it is the exact root value (lower == upper). * * @param p input polynomial * @param epsilon lower bound tolerance on the interval width (default: 100ε) * @return vector of isolation intervals (lower ≤ upper, sorted) */ template [[nodiscard]] std::vector> vincentRealRootIsolation( const Polynomial& p, T epsilon = std::numeric_limits::epsilon() * T(100)) { int deg = p.degree(); if (deg <= 0) return {}; std::vector> result; // Linear: solve directly if (deg == 1) { T root = -p[0] / p[1]; result.push_back({root, root, true}); return result; } // Extract the root at x = 0 Polynomial q = p; int zero_roots = 0; while (q.degree() >= 1 && std::abs(q[0]) <= epsilon * std::abs(q[q.degree()])) { // q / x std::vector c(q.coefficients().begin() + 1, q.coefficients().end()); q = Polynomial(std::move(c)); ++zero_roots; } if (zero_roots > 0) { result.push_back({T(0), T(0), true}); } if (q.degree() < 1) { return result; } // Square-free part auto sf = detail::squareFree(q); T bound = detail::cauchyBound(sf); // Isolate the positive roots detail::vincentPositiveRoots(sf, bound, result, epsilon); // Negative roots: isolate the positive roots of p(-x) and flip the sign // p(-x): flip the sign of the odd-degree coefficients auto coeffs = sf.coefficients(); for (std::size_t i = 0; i < coeffs.size(); ++i) { if (i % 2 == 1) coeffs[i] = -coeffs[i]; } Polynomial neg_poly(std::move(coeffs)); T neg_bound = detail::cauchyBound(neg_poly); std::vector> neg_intervals; detail::vincentPositiveRoots(neg_poly, neg_bound, neg_intervals, epsilon); for (auto& iv : neg_intervals) { result.push_back({-iv.upper, -iv.lower, iv.exact}); } // Sort std::sort(result.begin(), result.end(), [](const RealRootInterval& a, const RealRootInterval& b) { return a.lower < b.lower; }); return result; } // ================================================================ // Bernoulli method (extraction of the dominant root by the power method) // ================================================================ /** * @brief Find the largest-magnitude root (dominant root) of a polynomial by the Bernoulli method (power method) * * Make the polynomial p(x) = a_n x^n + ... + a_1 x + a_0 monic, and exploit the property that the ratio * y_k / y_{k-1} of the impulse response of the characteristic-equation-based linear recurrence * y_k = -c_{n-1} y_{k-1} - ... - c_0 y_{k-n} converges to the dominant root. * * Reference: Hayato Togawa, "Numerical Computation", p74 * * @param poly polynomial (degree ≥ 1) * @param tolerance converges when the difference between successive approximate roots is at most this * @param max_iterations maximum number of iterations * @return the dominant root and the convergence status * * @note Does not converge when the largest-magnitude root is a repeated root or an equal-magnitude complex conjugate pair. */ template std::pair bernoulli_method( const Polynomial& poly, T tolerance = std::numeric_limits::epsilon() * T(1000), size_t max_iterations = 1000) { int n = poly.degree(); if (n <= 0) return {T(0), false}; // Make monic: divide by the leading coefficient T lead = poly[n]; std::vector c(n); for (int i = 0; i < n; ++i) { c[i] = poly[i] / lead; } // State vector y[0..n-1]: the latest n values of the impulse response // Initial values: y[0]=1 (impulse), y[1..n-1]=0 std::vector y(n, T(0)); y[0] = T(1); // Compute the impulse response via the linear recurrence // y_new = -(c[n-1]*y[0] + c[n-2]*y[1] + ... + c[0]*y[n-1]) T r_prev = T(0); bool converged = false; T r = T(0); // Skip the first few steps, where y[previous] = 0 can occur size_t warmup = static_cast(n) + 2; for (size_t iter = 0; iter < max_iterations + warmup; ++iter) { T y_new = T(0); for (int j = 0; j < n; ++j) { y_new -= c[n - 1 - j] * y[j]; } // Compute the ratio (after warmup) if (iter >= warmup && std::abs(y[0]) > std::numeric_limits::epsilon()) { r = y_new / y[0]; if (iter > warmup && std::abs(r - r_prev) <= tolerance) { converged = true; break; } r_prev = r; } // Shift: discard y[n-1] and put y_new at the front for (int j = n - 1; j > 0; --j) { y[j] = y[j - 1]; } y[0] = y_new; // Overflow prevention: scale if the values grow too large T max_val = T(0); for (int j = 0; j < n; ++j) { T av = std::abs(y[j]); if (av > max_val) max_val = av; } if (max_val > T(1e100)) { for (int j = 0; j < n; ++j) { y[j] /= max_val; } } } return {r, converged}; } } // namespace sangi #endif // SANGI_POLYNOMIAL_ROOTS_HPP