找到(立方)多项式的实根的简单方法是什么?

时间:2011-02-05 11:20:47

标签: c math numerical-methods polynomial-math

对我来说,这似乎是一个显而易见的问题,但我无法在任何地方找到它。 我有一个三次多项式,我需要找到函数的真正根源。什么是 这样做的方法?

我找到了几个立方函数根的封闭形式公式,但它们都使用复数或大量的测角函数,我不喜欢它们(也不知道选择哪一个)。

我需要一些简单的东西;越快越好;而且我知道我最终需要求解更高阶的多项式,因此使用数值求解器也许会有所帮助。 我知道我可以用一些图书馆来为我做艰苦的工作,但是我想说我想做这个练习。

我在C编码,所以请不要import magic_poly_solver

奖金问题:如何在给定的时间间隔内仅查找根?

5 个答案:

答案 0 :(得分:8)

对于三次多项式,有closed form solutions,但它们并不特别适合数值计算。

我会对立方体情况执行以下操作:任何三次多项式都至少有一个实根,您可以使用牛顿方法轻松找到它。然后,使用deflation得到剩余的二次多项式求解,请参阅我的答案there,了解如何正确执行后一步骤。

提醒一句:如果判别式接近于零,那么将存在数字上的多个实根,并且牛顿方法将失败。此外,由于到根的附近,多项式就像(x - x0)^ 2,你将失去有效数字的一半(因为P(x)将是< epsilon x - x0< sqrt (ε))。因此,您可能需要对此进行排除,并在此特定情况下使用闭合形式解,或求解导数多项式。

如果您想在给定的时间间隔内找到根,请检查Sturm's theorem

通用多项式求解的更一般(复杂)算法是Jenkins-Traub algorithm。这在这里显然有些过分,但在立方体上效果很好。通常,您使用第三方实现。

由于你做C,使用GSL肯定是你最好的选择。

另一种通用方法是用例如companion matrix找到特征值。平衡的QR分解,或减少到Householder形式。这是GSL采取的方法。

答案 1 :(得分:2)

如果您不想使用封闭式解决方案(或期望更大次序的多项式),最明显的方法是使用Newton's method计算近似根。

不幸的是,虽然取决于起始值,但不可能决定迭代时会得到哪些根。

另见here

答案 2 :(得分:2)

Solving quartics and cubics for graphics,发表于 Graphics Gems V

答案 3 :(得分:1)

对于用简单的C代码求解三次方程组,我发现著名数字专家William Kahan教授的QBC求解器足够健壮,相当快且相当准确:

威廉·卡汉(William Kahan),“求解一个实立方方程。” PAM-352,加州大学伯克利分校纯粹与应用数学中心。 1986年11月10日。(onlineonline

这使用基于导数的迭代方法来找到实根,然后将其简化为二次方程,最后使用数值健壮的二次方程求解器来找到剩余的两个根。通常,迭代求解器需要大约五到十次迭代才能收敛到结果。通过明智地使用fma()标准数学函数在ISO C99中使用的fused multiply-add(FMA)操作,可以提高两个求解器的准确性和性能。

对二次求解器的准确性至关重要的是判别式的计算。为此,我基于最近的研究使用以下代码:

/* Compute B*B - A*C, accurately 

   Claude-Pierre Jeannerod, Nicolas Louvet, and Jean-Michel Muller, 
   "Further Analysis of Kahan's Algorithm for the Accurate Computation 
   of 2x2 Determinants". Mathematics of Computation, Vol. 82, No. 284, 
   Oct. 2013, pp. 2245-2264

   https://www.ams.org/journals/mcom/2013-82-284/S0025-5718-2013-02679-8/S0025-5718-2013-02679-8.pdf
*/
double DISC (double A, double B, double C)
{
    double w = C * A;
    double e = fma (-C, A, w);
    double f = fma (B, B, -w);
    double r = f + e;
    return r;
}

使用双精度算法,Kahan的求解器无法始终产生精确到双精度的结果。 Kahan论文中提供的测试用例之一说明了这种情况的原因:

658x³-190125x²+ 18311811x-587898164

使用任意精度的数学库,我们发现该三次方程的根如下:

96.229639346592182182_18 ...
96.357064825184152_07 ...±i * 0.069749752043689625_43 ...

QBC使用双精度算术将根计算为

96.2296393 50445893
96.35706482 3257289 ±i * 0.0697497 48521837268

这样做的原因是,围绕实根的函数求值会遭受计算出的函数值中高达60%的误差,从而阻止了迭代求解器更接近于根。通过将函数和导数评估更改为将双精度计算用于中间计算(以较高的计算成本),我们可以解决该问题。

/* Data type for double-double computation */
typedef struct {
    double l;  // low / tail
    double h;  // high / head
} dbldbl;

dbldbl make_dbldbl (double head, double tail);
double get_dbldbl_head (dbldbl a);
double get_dbldbl_tail (dbldbl a);
dbldbl add_dbldbl (dbldbl a, dbldbl b);
dbldbl mul_dbldbl (dbldbl a, dbldbl b);

void EVAL (double X, double A, double B, double C, double D, 
           double * restrict Q, double * restrict Qprime, 
           double * restrict B1, double * restrict C2)
{
#if USE_DBLDBL_EVAL
    dbldbl AA, BB, CC, DD, XX, AX, TT, UU;
    AA = make_dbldbl (A, 0);
    BB = make_dbldbl (B, 0);
    CC = make_dbldbl (C, 0);
    DD = make_dbldbl (D, 0);
    XX = make_dbldbl (X, 0);
    AX = mul_dbldbl (AA, XX);
    TT = add_dbldbl (AX, BB);
    *B1 = get_dbldbl_head (TT) + get_dbldbl_tail(TT);
    UU = add_dbldbl (mul_dbldbl (TT, XX), CC);
    *C2 = get_dbldbl_head (UU) + get_dbldbl_tail(UU);
    TT = add_dbldbl (mul_dbldbl (add_dbldbl (AX, TT), XX), UU);
    *Qprime = get_dbldbl_head (TT) + get_dbldbl_tail(TT);
    UU = add_dbldbl (mul_dbldbl (UU, XX), DD);
    *Q = get_dbldbl_head (UU) + get_dbldbl_tail(UU);
#else // USE_DBLDBL_EVAL
    *B1 = fma (A, X, B); 
    *C2 = fma (*B1, X, C);
    *Qprime = fma (fma (A, X, *B1), X, *C2);
    *Q = fma (*C2, X, D);
#endif // USE_DBLDBL_EVAL
}

/* Construct new dbldbl number. |tail| must be <= 0.5 ulp of |head| */
dbldbl make_dbldbl (double head, double tail)
{
    dbldbl z;
    z.l = tail;
    z.h = head;
    return z;
}

/* Return the head of a double-double number */
double get_dbldbl_head (dbldbl a)
{
    return a.h;
}

/* Return the tail of a double-double number */
double get_dbldbl_tail (dbldbl a)
{
    return a.l;
}

/* Add two dbldbl numbers */
dbldbl add_dbldbl (dbldbl a, dbldbl b)
{
    dbldbl z;
    double e, q, r, s, t, u;

    /* Andrew Thall, "Extended-Precision Floating-Point Numbers for GPU 
       Computation." 2006. http://andrewthall.org/papers/df64_qf128.pdf
    */
    q = a.h + b.h;
    r = q - a.h;
    t = (a.h + (r - q)) + (b.h - r);
    s = a.l + b.l;
    r = s - a.l;
    u = (a.l + (r - s)) + (b.l - r);
    t = t + s;
    s = q + t;
    t = (q - s) + t;
    t = t + u;
    z.h = e = s + t;
    z.l = (s - e) + t;

    /* For result of zero or infinity, ensure that tail equals head */
    if (isinf (s)) {
        z.h = s;
        z.l = s;
    }
    if (z.h == 0) {
        z.l = z.h;
    }

    return z;
}

/* Multiply two dbldbl numbers */
dbldbl mul_dbldbl (dbldbl a, dbldbl b)
{
    dbldbl z;
    double e, s, t;

    s = a.h * b.h;    
    t = fma (a.h, b.h, -s);
    t = fma (a.l, b.l, t);
    t = fma (a.h, b.l, t);
    t = fma (a.l, b.h, t);
    z.h = e = s + t;
    z.l = (s - e) + t;

    /* For result of zero or infinity, ensure that tail equals head */
    if (isinf (s)) {
        z.h = s;
        z.l = s;
    }
    if (z.h == 0) {
        z.l = z.h;
    }

    return z;
}

使用更精确的函数和导数评估计算出的根为:

96.22963934659218 0
96.35706482518415 3 ±i * 0.06974975204 5672006

虽然实际零件现在的精度在双精度范围内,但虚部仍然不可用。原因是在这种情况下,二次方程对系数的微小差异敏感。它们中任何一个的一个ulp错误都可能导致假想部分之间的差异约为10 -11 。可以通过将系数表示为双精度以上并在二次求解器中使用高精度计算来解决此问题。

答案 4 :(得分:0)

/*******************************************************************************
 * FindCubicRoots solves:
 *      coeff[3] * x^3 + coeff[2] * x^2 + coeff[1] * x + coeff[0] = 0
 *  returns:
 *      3 - 3 real roots
 *      1 - 1 real root (2 complex conjugate)
 *******************************************************************************/

int FindCubicRoots(const FLOAT coeff[4], FLOAT x[3]);

http://www.realitypixels.com/turk/opensource/index.html#CubicRoots