拉格朗日插值法

· · 算法·理论

-2. 引言

相信我们小时候都做过找规律题目,比如 1,3,5,7\dots。

相信我们肯定脱口而出下一个肯定是 9!

但并不一定,下一个可以是任何实数,比如 33。

-1. 我会瞪眼!

比如一个数列:1,3,5,7,33。

发现可以构造为 P_x=(x-1)(x-2)(x-3)(x-4)+2x-1。

此时 P_1=1,P_2=3,P_3=5,P_4=7,P_5=33。

0. 寻找规律

我们发现 (x-1) 在 x=1 时为 0,这一项 (x-1)(x-2)(x-3)(x-4) 就全是 0 了。

所以 1\le x\le 4 时,满足这个条件。

我们发现可以专门构造一些 (x-x_i) 来使某些项 =0。

1. 正题:拉格朗日插值法

给定 n 个点:(x_1,y_1),(x_2,y_2),(x_3,y_3),\dots,(x_n,y_n)。

其中 x_i 互不相同,我们尝试构造:

首先构造基函数 L_i(x),L_i(x)=\prod_{j\ne i}\frac{x-x_j}{x_i-x_j}。

可能有些同学看不懂这个式子,展开来就是(这里写 n=4 的情况):

L_1(x)=\frac{(x-x_2)(x-x_3)(x-x_4)}{(x_1-x_2)(x_1-x_3)(x_1-x_4)} L_2(x)=\frac{(x-x_1)(x-x_3)(x-x_4)}{(x_2-x_1)(x_2-x_3)(x_2-x_4)} L_3(x)=\frac{(x-x_1)(x-x_2)(x-x_4)}{(x_3-x_1)(x_3-x_2)(x_3-x_4)} L_4(x)=\frac{(x-x_1)(x-x_2)(x-x_3)}{(x_4-x_1)(x_4-x_2)(x_4-x_3)}

那这个函数有什么用呢,他有如下性质:

当 i=j 时,L_i(x_j)=1。

当 i\ne j 时,L_i(x_j)=0。

然后答案就是:

P(x)=\sum^{n}_{i=1}y_iL_i(x)

因为当 x=x_j 时,只有 L_j(x_j)=1,其他都是 0,所以 P(x_j)=y_j。

2. 如何用 C++ 实现呢

给出一道 例题

先看代码:

::::success[代码]

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int mod=998244353;
int n,k,ans,x[2005],y[2005];
int pw(int a,int b){ // 快速幂
    int res=1;
    while(b){
        if(b&1) res=res*a%mod;
        a=a*a%mod,b>>=1;
    }return res;
}int inv(int a){return pw(a,mod-2);} // 逆元
signed main(){
    cin>>n>>k;
    for(int i=1;i<=n;i++) cin>>x[i]>>y[i];
    for(int i=1;i<=n;i++){
        int z=y[i]%mod,m=1;
        for(int j=1;j<=n;j++){
            if(i==j) continue;
            z=z*(k-x[j])%mod; // 分子
            m=m*(x[i]-x[j])%mod; // 分母
        }ans=((ans+z*inv(m)%mod)+mod)%mod; // 分子/分母的和(即为答案)
    }cout<<(ans+mod)%mod;
}

::::

显然,这个代码就是模拟一遍刚刚的流程,时间复杂度是 \mathcal{O}(n^2)。

3. 拓展

连续点值优化

当插值节点的横坐标是连续整数时(例如 x_i=1,2,\dots,n),拉格朗日插值可以大幅加速。

看看这个式子,也就是 P(x):

P(x)=\sum^{n}_{i=1}y_i\prod_{j\ne i}\frac{x-x_j}{x_i-x_j}

我们发现了什么?分母可以预处理,分子也可以用前缀积和后缀积在 \mathcal{O}(1) 时间内得到!

我们先令 x_i=i(方便之后推式子,反正如果不是就把 i 移个几位就好了),则:

f(k)=\sum_{i=1}^{n} y_{i} \frac{\text { pre}_{i-1} \text{ suf}_{i+1}}{(i-1)!(-1)^{n-i}(n-i)!}

我们来仔细看一下这个式子:

重心拉格朗日插值

重心拉格朗日插值将插值多项式写为一种更稳定的形式,适合动态加点的场景。

也就是换一种写法:定义重心权 \omega_i 为 \frac{1}{\prod_{j\ne i}(x_i-x_j)}。

即 \frac{1}{\omega_i} 就是 \prod_{j\ne i}(x_i-x_j),它出现在 P(x) 的分母中:

P(x)=\sum^{n}_{i=1}y_i \textcolor{red}{\prod_{j\ne i}} \frac{x-x_j}{\textcolor{red}{(x_i-x_j)}}

令 \ell(x)=\prod_{i=1}^n(x-x_i),这是所有节点构成的多项式。

然后我们试试重写基函数:

L_i(x)=\prod_{j\ne i}\frac{x-x_j}{x_i-x_j}=\frac{\prod_{j\ne i}(x-x_j)}{\prod_{j\ne i}(x_i-x_j)}=\omega_i\prod_{j\ne i}(x-x_j)=\omega_i \frac{\ell(x)}{x-x_i}

把基函数带入最后的多项式:

P(x)=\sum^{n}_{i=1}y_i\prod_{j\ne i}\frac{x-x_j}{x_i-x_j}\Rightarrow P(x)=\ell(x)\sum_{i=1}^n \frac{\omega_i y_i}{x-x_i}

就大功告成了!

第二种形式(再优化!)

考虑用同样的节点插值常数函数 1。因为插值多项式唯一,所以:

1=\sum^{n}_{i=1}L_i(x)=\ell(x)\sum_{i=1}^n \frac{\omega_i}{x-x_i}

于是 \ell(x)=\frac{1}{\sum^n_{i=1}\frac{\omega_i}{x-x_i}}。

然后再带回原来的式子,得到:

f(x)=\frac{\sum^n_{i=1} \frac{\omega_i y_i}{x-x_i}}{\sum^n_{i=1} \frac{\omega_i}{x-x_i}}

我们发现他不用算 \ell(x) 了!

求值时只需要计算两个和,每个和都是 O(n) 的时间复杂度。

使用完重心拉插后发现单次求值变成 O(n) 了,只有预处理是 O(n^2),还可以动态添加节点。

动态添加节点

假设我们现在已经有了 n 个节点 (x_1,y_1),(x_2,y_2),(x_3,y_3),\dots,(x_n,y_n),往里面添加新点 (x_{n+1},y_{n+1})。

然后更新重心权,旧点:

\omega_i^′=\frac{1}{\prod_{j\ne i,j\le n+1}(x_i-x_j)}=\frac{\omega_i}{x_i-x_{n+1}}

显然,更新一个旧点的时间复杂度是 O(1)。

新点(n+1):

\omega_{n+1}=\frac{1}{\prod^n_{j=1}(x_{n+1}-x_j)}

因为只有一个点,所以哪怕处理是 O(n) 的也没事。

之后求值仍然是 O(n)。这比重新计算所有 \omega_i 的 O(n^2) 快得多。

总结一下:

重心拉插的公式(第二种形式):

f(x)=\frac{\sum^n_{i=1} \frac{\omega_i y_i}{x-x_i}}{\sum^n_{i=1} \frac{\omega_i}{x-x_i}}

其中 \omega_i=\frac{1}{\prod_{j\ne i}(x_i-x_j)}。

End.