🧮 第7章 其他算法
📄 一句话概括

牛顿法用切线代替曲线:在当前点画一条切线,交横轴的地方就是下一个猜测。 它每轮把正确答案的位数翻倍——六轮就把误差压到 2.22e-16。 高斯消元两步:先把左下角消成 0,再从最后一行往回代。

📝 本章和前面几章的关系

前两章都是「在一堆数据里找一个」,这一章换个题:求一个满足条件的数。 解方程没有「试完所有可能」的路子,只能一点点逼近——这叫数值方法。 这一篇的两个算法各代表一种思路:牛顿法是迭代逼近,高斯消元是化繁为简。

一、牛顿法:用切线一点点逼近

要求「x 的平方等于 2」,也就是解 x*x - 2 = 0。这个方程没有漂亮的整数解。 牛顿法的做法:随便猜一个数,然后不断用切线修正它。 具体说:在当前这一点给曲线画一条切线,看切线和横轴交在哪儿,那个交点就是下一轮的猜测值。

为什么用切线? 因为切线是直线,直线好算。曲线难解,但它的切线在附近和它很像——用近似值去换「能算」。 演示里从 1 起步,六轮收敛到 1.4142135624。真值 sqrt(2) 也是这个数,六轮之后误差 2.22e-16。 这个精度意味着什么? 双精度浮点数一共才 16 位左右有效数字,所以这已经是这台机器能给的最准答案了。

二、误差每轮砍一大截:二次收敛

演示里那张表的前两列最有说服力:x 是 1、1.5、1.4167、1.41422,代入值(也就是误差的量级)是 -1、 0.25、0.0069、0.0000060。 每一轮的误差差不多是上一轮误差的平方。0.25 的平方是 0.06,实际到 0.0069,量级对得上; 再平方就是 5e-5,实际 6e-6。 这种速度叫二次收敛。它比「每轮砍一半」快得多:砍一半要三十轮才到 1e-9,位数翻倍五轮就到了。

所以牛顿法在数值计算里非常受宠:只要它收敛,就是几轮的事。 注意「只要它收敛」这五个字。下一段说的是它什么时候不收敛。

三、牛顿法的条件:能求导,起点别太远

演示末尾那两行提醒是这一节的关键:牛顿法要求函数能求导,还得从「离答案不太远」的地方起步。 为什么要求导?因为切线的斜率就是导数。不会求导就画不出切线,方法没法启动。 为什么起点要近?因为切线只在「附近」和曲线像。 离得远的时候,切线和曲线根本不是一个方向,交点可能跑到更远的地方去。

演示里那句原话很实在:起点选在拐弯处,可能越算越远——它快,但不保证一定收敛。 这是数值方法共同的毛病: 给你一个「通常很好用」的办法,而不是一个「永远保证对」的办法。 工程上靠的是「跑完检查一下结果像不像」。

四、高斯消元:解三个未知数

换一个题:三个方程三个未知数。手算的做法是「消元」——用一个方程把另一个方程里的 x 消掉。 高斯消元把这个动作系统化:一列一列地消,把左下角全部变成 0。 演示里原始方程组是三行四列(三个系数加一个右边的值)。 消元之后左下角全是 0,形状变成一个「三角形」。

为什么变成三角形就好解?因为最后一行只剩一个未知数——直接算出来。 然后往上代:知道 z 了,倒数第二行只剩 y 和 z,y 也就出来了。这叫回代。 演示里解出 x = 2、y = 3、z = -1。 手算三元就是这一套,电脑算三百元也是同一套,只是循环多跑几万次。

五、选主元:先换行,再消元

演示里有一句话容易漏过去:消元后的第一行不是原来那行。 原来的第一行第一个系数是 2,但程序把绝对值最大的 -3 那一行换到了最上面。这个动作叫选主元。 为什么必须换?两个理由。第一,如果主元是 0,下一步要除以它,直接除零崩掉。

第二,就算主元不是 0 但很小,除以它会放大误差。 除以 0.0001 等于乘一万,原来那点舍入误差也被放大一万倍。 所以「把绝对值最大的换到当前行」既是防崩,也是防误差。 这一行代码不写,很多方程组算出来的答案会莫名其妙地不准。

六、两个算法,两种思路

牛顿法是迭代:从一个猜测出发,每轮改一点,越来越接近答案。 高斯消元是变换:不改答案,只改方程组的写法,把它换成一眼能解的形状。 这两种思路覆盖了数值计算的大半:要么一点点逼近,要么换个形式让它变简单。

它们还有个共同点:都是「有限步内做完」,但结果可能是近似值。这和第 1 章说的「算法要有穷、 要能验证」对得上——验证的办法就是代回去看等号成不成立。 这一条很实用:牛顿法算出的 1.4142135624 代回 x*x 得 2.0000000000000004, 差一点点,但这点差在浮点数里已经是最好了。

七、动手 Demo:牛顿法求根号 2,高斯消元解三个未知数

先用牛顿法解 x*x - 2 = 0,把每一轮的 x、代入值和误差都打出来。 再解一个三元一次方程组:先选主元消元,把左下角消成 0,再回代。

def f(x):
    return x * x - 2

def df(x):
    return 2 * x

x = 1.0
for i in range(6):
    nxt = x - f(x) / df(x)
    print(i, '%.10f' % x, '%.10f' % f(x), '%.10f' % nxt)
    x = nxt
import math
print('误差', abs(x - math.sqrt(2)))

M = [[2.0, 1.0, -1.0, 8.0],
     [-3.0, -1.0, 2.0, -11.0],
     [-2.0, 1.0, 2.0, -3.0]]
for col in range(3):
    piv = max(range(col, 3), key=lambda r: abs(M[r][col]))
    M[col], M[piv] = M[piv], M[col]
    for r in range(col + 1, 3):
        k = M[r][col] / M[col][col]
        for c in range(col, 4):
            M[r][c] -= k * M[col][c]

for row in M:
    print('  '.join('%8.3f' % v for v in row))

s = [0.0] * 3
for i in (2, 1, 0):
    s[i] = (M[i][3] - sum(M[i][j] * s[j] for j in range(i + 1, 3))) / M[i][i]
print('解', s)

实跑输出:

--------------------------------------------------------------------
牛顿法与高斯消元
--------------------------------------------------------------------


  ① 牛顿法:用切线一点点逼近
  --------------------------------------------------------------------
  求 x 的平方等于 2,也就是 x*x - 2 = 0

| 第几轮 | 当前 x | 代入的值 | 下一轮的 x |
|---:|---:|---:|---:|
| 0 | 1.0000000000 | -1.0000000000 | 1.5000000000 |
| 1 | 1.5000000000 | 0.2500000000 | 1.4166666667 |
| 2 | 1.4166666667 | 0.0069444444 | 1.4142156863 |
| 3 | 1.4142156863 | 0.0000060073 | 1.4142135624 |
| 4 | 1.4142135624 | 0.0000000000 | 1.4142135624 |
| 5 | 1.4142135624 | 0.0000000000 | 1.4142135624 |

  真值 sqrt(2) = 1.4142135624
  六轮之后误差:2.22e-16
  每一轮都在当前这一点画一条切线,切线与横轴的交点就是下一个 x。切线比曲线好算,所以用它代替。
  注意收敛得多快:误差从 1 掉到 1e-2,再掉到 1e-4、1e-8——每轮大约把位数翻倍。
  这叫二次收敛,是牛顿法值钱的地方。
  ! 牛顿法要求函数能求导,还得从「离答案不太远」的地方起步。
  起点选在拐弯处,可能越算越远——它快,但不保证一定收敛。

  ② 高斯消元:解三个未知数
  --------------------------------------------------------------------
  原方程组(每行四个数:x、y、z 的系数和右边的值):
       2.000     1.000    -1.000     8.000
      -3.000    -1.000     2.000   -11.000
      -2.000     1.000     2.000    -3.000
  消元之后(左下角全是 0):
      -3.000    -1.000     2.000   -11.000
       0.000     1.667     0.667     4.333
       0.000     0.000     0.200    -0.200
  第一行不是原来那行——选主元时把绝对值最大的 -3 那行换到了最上面,免得一开始就除以一个很小的数。
  从最后一行往回代:x = 2,y = 3,z = -1
  两步:先一列一列把左下角消成 0(消元),再从最后一行往上一行行解出来(回代)。
  手算三元就是这一套,电脑算三百元也是同一套。
  上面那个「把绝对值最大的行换到当前行」叫选主元。不做这一步,遇到主元是 0 就会除零;
  做了这一步,数值误差也小得多。

①牛顿法那一行 x - f(x)/df(x) 就是「画切线、求交点」的公式。 它没有魔法,只是把几何动作写成了算术。 ②第二轮起 x 就接近 1.41 了,第三轮已经 1.4142。位数一轮一轮翻倍,这就是二次收敛。 ③消元那段 piv = max(..., key=lambda r: abs(...)) 就是选主元:在剩下的行里挑绝对值最大的。 ④消元后打印的三行左下角全是 0.000,形状是三角形。回代从 i = 2 开始往上走。 ⑤' '.join('%8.3f' % v ...) 是为了对齐输出。 直接打印 Python 的列表会带引号和方括号,看不出三角形的形状。

30 秒实验

  1. 把牛顿法的起点从 1.0 改成 100.0,看要几轮收敛。
  2. 把起点改成 0.0,会怎样?(df(0) 是 0,会除零)
  3. 把消元那段里的选主元整行删掉,看解出来的答案还对不对。

常见误区

  1. 「牛顿法一定能算出答案。」它要求能求导、起点别太远。起点糟糕时会越算越远。
  2. 「误差 2.22e-16 说明算错了。」那已经是双精度浮点的极限精度,再准不了。
  3. 「选主元只是为了防除零。」也为了防误差放大——除以一个很小的数会放大舍入误差。
自测三问(点开对答案) 1. 牛顿法每一轮在做什么? —— 在当前点画切线,求切线与横轴的交点,把它当作下一个猜测值。 2. 为什么叫二次收敛? —— 每轮误差大约变成上一轮的平方,位数翻倍。比「砍一半」快得多。 3. 高斯消元的两步是什么? —— 先选主元、一列一列把左下角消成 0;再从最后一行往上回代。

解方程要「逼近」。那算面积呢?下一节把面积切成条,一个一个加起来——顺带讲一个特别聪明的筛法。

相关笔记