辛普森积分法
辛普森积分法是一种快速求函数定积分的方法。与普通的直线拟合方法相比,辛普森积分法采用的是二次函数来拟合函数,而效果十分显著。
用抛物线拟合
假设我们有三个不重合的点,那么可以唯一确定一个抛物线经过这三个点。如下图所示:
辛普森积分法就是在积分区间
如何计算这个二次函数?我们自然可以解三元三次方程组,但更方便的方法是拉格朗日差值公式:
其中
二次函数的积分
有了二次函数后,计算其积分是十分简单的事,但是这一切还不够完美。下面我们将推导出一个更简单的公式。
设:
那么:
所以:
因为:
所以之前的式子可变为:
调整可得:
于是就是:
这就是辛普森法则。
辛普森积分
下面将用Python简单实现一下辛普森积分,首先我们需要一个计算积分的函数:
1 2 3 4 5 6 7 8 | def sample(f, a, b): """用二次函数近似计算函数f的积分 f: 函数 a: 积分下界 b: 积分上界 """ return (b - a) * (f(a) + 4.0 * f((a + b) * 0.5) + f(b)) * 0.5 |
然后就是对f
分成n
段,每一段使用二次函数近似计算:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | def simpson(f, a, b, n = 1000): """计算定积分 f: 原函数 a: 积分下界 b: 积分上界 n: 划分的区间数 """ delta = (b - a) / n result = 0 for i in range(0, n): left = a + i * delta right = left + delta result += sample(f, left, right) return result |
用它来试着算一下圆周率:
1 2 | In [1]: simpson(lambda x : 2.0 * sqrt(1 - x**2), -1, 1, 100000) Out[1]: 3.1415926390691236 |
可以发现分为sample
换为直线的拟合,你将只能得到
自适应辛普森积分
然而控制区间个数不是一个控制答案精度的好办法,通常我们将使用特定的
如何让算法知道应当继续细分来达到特定精度呢?通常的方法就是将当前区间分为两半,用这两半的拟合值与当前计算的值做差。如果小于
这样我们就将得到一个递归算法,具体实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | def _adaptive_simpson(f, a, b, eps, current): """递归过程 f: 原函数 a: 积分下界 b: 积分上界 eps: 精度 current: [a, b]的定积分近似值,用于减少计算量 """ mid = (a + b) * 0.5 leftans = sample(f, a, mid) rightans = sample(f, mid, b) if abs(leftans + rightans - current) < eps: # 如果达到要求 return current else: # 否则将递归下去计算 return (_adaptive_simpson(f, a, mid, eps * 0.5, leftans) + _adaptive_simpson(f, mid, b, eps * 0.5, rightans)) def adaptive_simpson(f, a, b, eps): """自适应辛普森积分 f: 原函数 a: 积分下界 b: 积分上界 eps: 精度 """ return _adaptive_simpson(f, a, b, eps, sample(f, a, b)) |
我将其与线性拟合的算法对比了一下速度。他们共用一个递归的框架,只是sample
函数有二次函数拟合和直线拟合两种:
直线拟合:
1 2 3 4 | In [1]: %time adaptive_simpson(sin, 0, 1000, 0.00001) CPU times: user 5.58 s, sys: 0 ns, total: 5.58 s Wall time: 5.58 s Out[1]: 0.4376209161316862 |
二次函数拟合:
1 2 3 4 | In [26]: %time adaptive_simpson(sin, 0, 1000, 0.00001) CPU times: user 60 ms, sys: 0 ns, total: 60 ms Wall time: 59.6 ms Out[26]: 0.43762092534838204 |
可见辛普森积分法是相当迅速的。
异常情况
由于是使用二次函数拟合,所以辛普森积分法对光滑的函数效果较好。但下面几种情况需要谨慎考虑:
- 上下震荡十分剧烈的函数(如
$y = \sin(1/x)$ )不适用,事实上这种函数一般都不好积。 - 突变的函数(如
$y = |x|$ ,在$x = 0$ 处无导数)不适用。
对于存在没有导数的点的函数,通常的解决方法就是从这些点分开,然后就可以分段计算了。