65.9K
CodeProject 正在变化。 阅读更多。
Home

C++ 中的数值方法第 3 部分: 根近似算法

starIconstarIconstarIconstarIconstarIcon

5.00/5 (13投票s)

2019年6月10日

CPOL

8分钟阅读

viewsIcon

16001

实现二分法、牛顿法、Dekker 法和 Brent 法等根近似算法

欢迎回到 thoughts-on-cpp.com 的新文章。这次,我想更深入地探讨我经常用于解决众多数值问题的根近似方法。我们将从简单的二分法开始,然后研究牛顿法和割线法,最后以 Dekker 和 Brent 的黑盒方法进行总结。

前导码

在接下来的章节中,我们将详细介绍几种用于函数根近似的算法。我们将所有算法都与非线性函数 f(x)=x^3-x^2-x-1 进行比较,该函数在定义的区间 [a,b] 中**只有一个根**(这个条件非常重要,否则可能难以确定算法得到的解是哪个)。附加条件是 f(a) \cdot f(b) < 0 。请记住,我们不检查迭代次数。这可能是必要的,因为我们通常不希望陷入无限循环。一如既往,您可以在 GitHub 上找到所有源代码。

二分法

让我们从一种主要用于搜索任意大小数组中的值的方法开始,即 二分法。但它也可用于根近似。二分法的优点在于其实现简单且保证收敛(如果存在解,二分法总能找到它)。算法相当简单:

  1. 计算给定区间 [a,b] 的中点 m=\frac{a+b}{2}
  2. 计算中点 m 的函数结果 f(m)
  3. 如果新的 m-a|f(m)| 满足收敛条件,则我们找到了解 m
  4. 比较 f(m) 的符号,并替换 f(a)f(b),使得结果区间包含所寻找的根

Bisection algorithm

二分法实现

正如我们从源代码中看到的,二分法的实际实现非常简单,大部分代码只是为了输出。重要部分发生在 while 循环内,该循环一直执行,直到 f(x) 的结果不满足条件 f(x) < \epsilon 。新位置 xcalculateX() 方法中计算,该方法检查 f(x) 的符号,并根据结果将 x 赋值给 ab,以定义包含根点的新范围。重要的是要注意,二分法始终需要 f(a)f(b) 具有不同符号的结果,这是通过 checkAndFixAlgorithmCriteria() 方法确保的。该算法收敛速度较慢,需要 36 次迭代才能达到 \epsilon = 1\cdot10^{-10} 的收敛条件,但只要有足够的时间,它终究会收敛。

Results of Bisection Algorithm

牛顿法

只要我们还知道函数的导数并且函数是光滑的,我们就可以使用速度更快的 牛顿法,其中根的下一个更接近的点 x_{n+1} 可以通过以下公式计算:

x_{n+1}=x_n-\frac{f(x_n)}{f'(x_n)}

Newton algorithm

牛顿法实现

同样,该算法会一直迭代,直到 f(x) 的结果不满足条件 f(x) < \epsilon calculateX() 方法不仅应用牛顿法的公式,还会检查 f(x) 的导数是否接近于零。由于浮点数不能直接比较,我们必须将 f'(x) 与最接近零的最小浮点数进行比较,这可以通过调用 std::numeric_limits<double>::min() 来获得。在这种 f'(x) 为零的情况下,算法将无法收敛(达到驻点),如下图所示。

Newton algorithm with stationary condition

牛顿法的优势在于其极高的速度。在我们的例子中,它只需要 6 次迭代就能达到 \epsilon = 1\cdot10^{-10} 的收敛条件。但该算法也有几个缺点,如上所述,例如:

  • 仅适用于我们知道函数导数的情况
  • 仅适用于光滑且连续的函数
  • 如果起始点 x_0 选择不当,或者计算出的点 x_{n+1} 位于局部最大值或最小值处或附近,则 f'(x) 的导数为 0,我们就遇到了驻点条件。
  • 如果函数的导数行为不佳,牛顿法倾向于“过冲”。此时 x_{n+1} 可能离根太远,对算法没有帮助。

Results of Newton Algorithm

割线法

在许多情况下,我们没有函数的导数,或者计算它可能过于复杂。在这种情况下,我们可以使用 割线法。该方法基本与 Newton 方法相同,但计算必需的斜率不是通过函数导数 f'(x),而是通过使用 f(x) 计算的两个 xy 值的商来完成。

x_{n+1}=x_n-f(x_n)\frac{x_n-x_{n-1}}{f(x_n)-f(x_{n-1})}

Secant method

割线法实现

该算法需要一个区间 [a,b],该区间可能(不绝对必要但有帮助)包含我们正在寻找的根。它使用 calculateX() 方法,该方法不仅计算 x_{n+1} ,还会检查分母 f(x_n)-f(x_{n-1}) 是否为 0。此外,该算法也非常快,在一个选择良好的区间内只需要 7 次迭代,在一个较宽的区间内需要 10 次迭代。这两种计算都满足 \epsilon = 1\cdot10^{-10} 的相同收敛条件。与牛顿法一样,该算法也有一些缺点需要注意:

  • 仅适用于光滑且连续的函数
  • 如果计算出的某个点 x_{n+1} 位于局部最大值或最小值处或附近,则 f'(x) 的导数为 0,我们就遇到了驻点条件。
  • 如果计算出的函数斜率商非常平坦,割线法也可能“过冲”。此时 x_{n+1} 可能离根太远,对算法没有帮助。
  • 如果区间 [a,b] 选择不当,例如包含局部最小值和最大值,算法会趋向于振荡。总的来说,局部或全局的最小值和最大值在某些情况下对割线法来说是个问题。

Results of Secant Algorithm

Dekker 法

Dekker 法的思路是将牛顿法/割线法的速度与二分法的收敛保证相结合。算法定义如下:

s=\left\{ \begin{matrix} b_k-f(b_k)\frac{b_k-b_{k-1}}{f(b_k)-f(b_{k-1})}, & if \: f(b_k) \neq f(b_{k-1}) \\ m & otherwise \end{matrix} \quad \right.

m=\frac{a+b}{2}

  1. b_k 是当前迭代的根的猜测值,而 a_k b_k 的当前对应点,使得 f(a_k) f(b_k) 符号相反。
  2. 算法必须满足条件 f(b_k) \leq f(a_k) ,以便 b_k 是最接近根的解。
  3. f(b_{k-1}) 是上一次迭代的值,算法开始时 b_{k-1}=a_k
  4. 如果 s 在 m 和 b_k 之间,则 b_{k+1}=s ,否则 b_{k+1}=m
  5. 如果 f(a_k)f(b_{k+1}) > 0 \wedge f(b_k)f(b_{k+1}) < 0 ,则 a_{k+1} = b_k ,否则 a_{k+1} = a_k

Dekker 法实现

正如我们在 Dekker 法的实现中所见,我们始终使用 calculateSecant()calculateBisection() 方法计算 m 和 s 的值,并根据 useSecantMethod() 方法的确认结果来分配这些值,该方法会确认 s 是否在 m 和 b_k 之间(如规则 4 所定义)。在第 32-33 行,我们确认函数 f(x) 的结果值是否满足规则 5。由于存在二分法,我们在每次迭代后都必须检查并纠正 $latex f(a_k)f(b_k) < 0 &s=1 &$ 的条件是否仍然满足,这通过 checkAndFixAlgorithmCriteria() 方法完成。

Results of Dekker Algorithm

Dekker 算法的速度与牛顿法/割线法一样快,同时也保证了收敛。在选择良好的区间情况下需要 7 次迭代,在较宽的区间情况下需要 9 次迭代。正如我们所见,Dekker 算法非常快,但也有一些例子表明 |b_k-b_{k+1}| 非常小但仍然使用了割线法。在这种情况下,算法将比纯二分法花费更多迭代。

Brent 法

由于 Dekker 法收敛速度较慢,该方法被 Brent 扩展,现在称为 Brent 法或 Brent-Dekker 法。该算法通过使用四个点(abb_{k-1} b_{k-2} )而不是三个点,以及使用 逆二次插值而不是线性插值和二分法,以及额外的条件来防止收敛缓慢,从而扩展了 Dekker 算法。该算法根据以下条件决定使用哪种方法:二分法、割线法或逆二次插值。

  • 二分法(B = 上次迭代使用了二分法)
    • B \wedge |s-b_k| \geq (b_k-b_{k-1})/2  !B \wedge |s-b_k| \geq (b_{k-1}-b_{k-2})/2  B \wedge |b_k-b_{k-1}|< |\delta|  !B \wedge |b_{k-1}-b_{k-2}|< |\delta|
  • 逆二次插值
    • f(a_k) \neq f(b_{k-1}) \wedge f(b_k) \neq f(b_{k-1})
  • 在所有其他情况下,使用割线法。

Brent 法实现

通过这些修改,Brent 算法至少和二分法一样快,但在最佳情况下比纯割线法略快。Brent 算法在选择良好的区间情况下需要 6 次迭代,在较宽的区间情况下需要 9 次迭代。

Results of Brent Algorithm

那么,选择哪种算法?

我们已经看到了五种可以用来近似函数根的算法:二分法、牛顿法、割线法、Dekker 法和 Brent 法。它们都有不同的可能性和缺点。总的来说,我们可以对选择哪种算法进行如下论述:

  • 如果函数光滑且行为良好,并且您也知道函数的导数,请使用牛顿法。
  • 如果函数光滑且行为良好,但您不知道函数的导数,请使用割线法。
  • 如果您不确定或知道您的函数有跳跃或其他问题,请使用 Brent 法。
算法 开始/区间 迭代次数
二分法 [0, 2] 36
Newton x0 = 1.5 6
割线法(好) [1.5, 2] 7
割线法(差) [0, 2] 10
Dekker 法(好) [1.5, 2] 7
Dekker 法(差) [0, 2] 9
Brent 法(好) [1.5, 2] 6
Brent 法(差) [0, 2] 9

您喜欢这篇文章吗?
您有什么想法?您喜欢这篇文章吗?
欢迎评论和分享本文。

© . All rights reserved.