由方程求解引发的关于牛顿迭代的想法

题目

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}\)
这就是我们最后的递推式。
由于本人退役多年,所以代码先不放了

posted @ 2021-06-22 21:00  24Kmagic  阅读(176)  评论(2)    收藏  举报