由方程求解引发的关于牛顿迭代的想法
题目
https://www.luogu.com.cn/problem/P1024
解析
正常方法
这道题一般人都能想到二分
代码https://www.luogu.com.cn/record/48426667
牛顿迭代
background
多数方程不存在求根公式,因此求精确根非常困难,甚至不可解,从而寻找方程的近似根就显得特别重要。方法使用函数f(x)的泰勒级数的前面几项来寻找方程f(x) = 0的根。牛顿迭代法是求方程根的重要方法之一,其最大优点是在方程f(x) = 0的单根附近具有平方收敛,而且该法还可以用来求方程的重根、复根,此时线性收敛,但是可通过一些方法变成超线性收敛。另外该方法广泛用于计算机编程中。
以上内容来自百度百科,但是对于我们来说,我们不需要了解这么多,我们只需要知道牛顿迭代找根的速度非常快(甚至比二分还快)
牛顿迭代
方法
牛顿迭代听起来非常的高深莫测,其实说起来就是一句话:用切线逼近零点。
定义函数\(f(x)\),我们找到一个区间,使区间中存在一个根(实根分布),设r 是\(f(x) = 0\)的根,选\(x_0\)作为r的初始近似值,然后过\((x_0,f(x_0))\) 做 函数图像的切线,那么切线与x轴焦点的横坐标\(x_1\)称为r的一次近似值,然后过点\((x_1,f(x_1))\)再次作函数图像的切线(我刚刚说过啥来着,用切线逼近),再次求出切线与x轴的交点\(x_2\),如是重复下去,然后找到一个非常小的值,定义为eps(这个值根据我们需要的精度来改,比如说我在刚开始放的那道题精度是10-2,那么eps定义成10-3即可),当\(f(x_n) < eps\)时,我们就可以认为\(x_n\)是我们要的一个根。
求坐标
那么我们该如何求出切线和x轴交点的横坐标呢?推导过程如下:
函数的斜率 \(k=\frac{\Delta y}{\Delta x}\)
\(\Delta x = x_1 - x_0\)
带入x轴上点的坐标并移项得
\(0 - f(x_0) = f'(x_0) \times (x_1 - x_0)\)
进一步化简得
\(0 - f(x_0) = f'(x_0) \times x_1 - f'(x_0) \times x_0\)
\(f'(x_0) \times x_1 = f'(x_0) \times x_0 - f(x_0)\)
从而得到
\(x_1 = x_0 - \frac {f(x_0)} {f'(x_0)}\)
以此类推,最终得到
\(x_{n+1} = x_n - \frac {f(x_n)} {f'(x_n)}\)
应用
那么学会了牛顿迭代,刚才那道题应该就很好切了吧
上代码:
#include <bits/stdc++.h>
#define Enter puts("")
#define Space putchar(' ')
using namespace std;
typedef long long ll;
typedef unsigned long long Ull;
typedef double Db;
inline ll Read() {
ll Ans = 0;
char Ch = ' ' , Las;
while(!isdigit(Ch)) {
Las = Ch;
Ch = getchar();
}
while(isdigit(Ch)) {
Ans = (Ans << 3) + (Ans << 1) + Ch - '0';
Ch = getchar();
}
if(Las == '-')
Ans = -Ans;
return Ans;
}
inline void Write(ll x) {
if(x < 0) {
x = -x;
putchar('-');
}
if(x >= 10)
Write(x / 10);
putchar(x % 10 + '0');
}
inline ll Quick_Power(ll a , ll b) {
ll Ans = 1 , Base = a;
while(b != 0) {
if(b & 1 != 0)
Ans *= Base;
Base *= Base;
b >>= 1;
}
return Ans;
}
Db x1 , x2 , x3 , a , b , c , d;
inline Db f(Db x) {
return a * x * x * x + b * x * x + c * x + d;
}
inline Db df(Db x) {
return 3 * a * x * x + 2 * b * x + c;
}
inline Db slove(Db l,Db r) {
Db x , x0 = (l + r) / 2;
while(abs(x0 - x) > 0.0001)
x = x0 - f(x0) / df(x0) , swap(x0 , x);
return x;
}
int main() {
cin >> a >> b >> c >> d;
Db p = (-b - sqrt(b * b - 3 * a * c)) / (3 * a);
Db q = (-b + sqrt(b * b - 3 * a * c)) / (3 * a);
x1 = slove(-100 , p);
x2 = slove(p , q);
x3 = slove(q , 100);
printf("%.2lf %.2lf %.2lf" , x1 , x2 , x3);
return 0;
}
后记
关于牛顿迭代为什么比二分快,因为二分是线性收敛,牛顿迭代是二阶收敛,关于证明为什么是二阶收敛我实在是心有余而力不足了,等以后学的多一点了再回来证明
现在先把p阶收敛的相关内容弄上来:
设迭代过程\(x_{k + 1} = f(x_k)\)收敛于\(f(x) = 0\)的根\(x_0\),记迭代绝对误差\(e_k = |x_0 - x_k|\),若存在常数p(p≥1)和c(c>0),使\(\lim_{k\rightarrow +\infty }\frac{e_{k+1}}{e_{k}^{p}}=C\)则称序列{xn}是 p 阶收敛的,c称渐近误差常数。特别地,p=1时称为线性收敛,p=2时称为平方收敛或二阶收敛。1 < p < 2时称为超线性收敛。
后记之后
前几天在oiwiki上看到了一个关于牛顿迭代的东西,发现可以把牛顿迭代的应用具像化。怎么具像化呢,其实就是求平方根。
我们不难想象,求一个数n的平方根,其实就是求方程 \(f(x) = x^2^ - n\) 的解。
所以根据上文得到的规律,我们可以得到\(x_{n+1} = x_n - \frac {f(x_n)} {f'(x_n)}\)
继续化简得 \(x_{n+1} = x_n - \frac {x_n - n/x_n} {2}\)
这就是我们最后的递推式。
由于本人退役多年,所以代码先不放了

浙公网安备 33010602011771号