1. 程式人生 > 其它 >拉格朗日插值學習筆記

拉格朗日插值學習筆記

拉格朗日插值學習筆記

簡介

數值分析中,拉格朗日插值法是以法國十八世紀數學家約瑟夫·拉格朗日命名的一種多項式插值方法。許多實際問題中都用函式來表示某種內在聯絡或規律,而不少函式都只能通過實驗和觀測來了解。如對實踐中的某個物理量進行觀測,在若干個不同的地方得到相應的觀測值,拉格朗日插值法可以找到一個多項式,其恰好在各個觀測的點取到觀測到的值。這樣的多項式稱為拉格朗日(插值)多項式。數學上來說,拉格朗日插值法可以給出一個恰好穿過二維平面上若干個已知點的多項式函式。拉格朗日插值法最早被英國數學家愛德華·華林於1779年發現,不久後(1783年)由萊昂哈德·尤拉再次發現。1795年,拉格朗日在其著作《師範學校數學基礎教程》中發表了這個插值方法,從此他的名字就和這個方法聯絡在一起。

差值

對於學習拉格朗日插值之前,我們首先要弄懂,什麼叫差值?

插值 數學領域數值分析中的通過已知的離散資料求未知資料的過程或方法。

概念可能不好理解,舉個栗子:

我們給出:

\(x_0=1,y_0=3\)

\(x_1=2,y_0=5\)

\(x_2=4,y_0=8\)

\(x_3=5,y_0=4\)

求當 \(x=3\) 時,\(y\) 的值。

首先,我們可以通過列方程的辦法,求出函式,將 \(x\) 帶入求解。

如,我們設 \(y=ax^3+bx^2+cx+d\)

\(\begin{cases} a+b+c+d=3 \\8a+4b+2c+d=5 \\ 64a+16b+4c+d=8 \\125a+25b+5c+d=4 \end{cases}\)

然後聯立求解,這裡就不給出求解過程了,需要用到高斯消元。

這樣做的複雜度是 \(O(n^3)\) 的,有點慢,我們考慮用拉格朗日差值優化這一問題。

拉格朗日插值

我們首先考慮 \(x_0\) ,建立一個函式,使其經過 \((x_0,1),(x_1,0),(x_2,0),(x_3,0)\)

為什麼這麼構造,為什麼不構造一個經過 \((x_0,2),(x_1,1),(x_2,1),(x_3,1)\) 的函式或者其他的呢,這個我們一會再說。

我們先考慮一下,如何才能構造這樣一個函式呢?

\(y=\frac{(X-x_1)\times(X-x_2)\times(X-x_3)}{(x_0-x_1)\times(x_0-x_2)\times(x_0-x_3)}\)

我們不難發現,當 \(X=x_0\) 時,分子變成了 \((x_0-x_1)\times(x_0-x_2)\times(x_0-x_3)\),這與分母相同,於是式子可化為 \(1\)。再看,當 \(X=x_1,X=x_2,X=x_3\) 時,分子會變為 \(0\),於是,我們完成了構造的一大半。

我們發現,當前的函式只是經過了 \((x_0,1)\),我們想要的是它經過 \((x_0,y_0)\),怎麼辦呢。

很簡單,我們直接在原始的基礎上乘上一個 \(y_0\)

\(y=\frac{(X-x_1)\times(X-x_2)\times(X-x_3)}{(x_0-x_1)\times(x_0-x_2)\times(x_0-x_3)}\times y_0\)

此時,由於當 \(X=x_1,X=x_2,X=x_3\) 時,函式值為 \(0\),我們乘上一個 \(y_0\) 不會對其有影響,於是,我們就構造出一個經過 \((x_0,y_0)\) 的函式。

如圖:

此時,我們再回答之前的問題:為什麼要使函式經過 \((x_0,1),(x_1,0),(x_2,0),(x_3,0)\)

按照我自己的理解,這其實是為了之後乘 \(y_0\) 做鋪墊,因為這樣我們的 \(x_0\) 就對應了 \(y_0\) ,而其他值依舊為 \(0\) ,滿足了我們的構造要求:經過 \((x_0,y_0)\) 。而如果是構造一個經過 \((x_0,2),(x_1,1),(x_2,1),(x_3,1)\) 的函式,還是應該有辦法讓它經過 \(y_0\) 的,我沒有嘗試過,但是應該會非常麻煩,所以我覺得這個構造的方式也是拉格朗日插值的一個精妙之處。

返回題目,這樣做還是不夠的,我們的目的是為了構造經過這四個點的函式,這才經過了一個呢。

我們根據上面的方法,依次構造出只經過 \((x_1,y_1)\) 的函式,只經過 \((x_2,y_2)\) 的函式,只經過 \((x_3,y_3)\) 的函式,

\(y0=\frac{(X-x_1)\times(X-x_2)\times(X-x_3)}{(x_0-x_1)\times(x_0-x_2)\times(x_0-x_3)}\times y_0\)

\(y1=\frac{(X-x_0)\times(X-x_2)\times(X-x_3)}{(x_1-x_0)\times(x_1-x_2)\times(x_1-x_3)}\times y_1\)

\(y2=\frac{(X-x_0)\times(X-x_1)\times(X-x_3)}{(x_2-x_0)\times(x_2-x_1)\times(x_2-x_3)}\times y_3\)

\(y3=\frac{(X-x_0)\times(X-x_1)\times(X-x_2)}{(x_3-x_0)\times(x_3-x_1)\times(x_3-x_2)}\times y_3\)

然後,我們把它們加起來。

\(f(x)=y0+y1+y2+y3\)

影象如下:

由於這四個函式都只經過了各不相同的四個點,其餘點的值都為 \(0\),因此,這個解析式就滿足了我們的要求,我們此時只要將待求的 \(x\) 帶入式子,就可以求出對應的值。

如我們的栗子:

我們再回頭看我們的函式

$f(x)=y0+y1+y2+y3 $

\(=\frac{(X-x_1)\times(X-x_2)\times(X-x_3)}{(x_0-x_1)\times(x_0-x_2)\times(x_0-x_3)}\times y_0+\frac{(X-x_0)\times(X-x_2)\times(X-x_3)}{(x_1-x_0)\times(x_1-x_2)\times(x_1-x_3)}\times y_1+\frac{(X-x_0)\times(X-x_1)\times(X-x_3)}{(x_2-x_0)\times(x_2-x_1)\times(x_2-x_3)}\times y_3+\frac{(X-x_0)\times(X-x_1)\times(X-x_2)}{(x_3-x_0)\times(x_3-x_1)\times(x_3-x_2)}\times y_3\)

\(=y_0\times\prod\limits_{j=1}^{3}\frac{x-x_j}{x_0-x_j}+y_1\times \prod\limits_{j=0,j\not=1}^{3}\frac{x-x_j}{x_1-x_j}+y_2\times\prod\limits_{j=0,j\not=2}^{3}\frac{x-x_j}{x_2-x_j}+y_3\times\prod\limits_{j=0,j\not=3}^{3}\frac{x-x_j}{x_3-x_j}\)

\(=\sum\limits_{i=0}^{3}y_i\prod\limits_{i\not=j}\frac{x-x_j}{x_i-x_j}\)

我們把界限設定為 \(n\),就可以得到常見的拉格朗日插值的式子:

\(f(x)=\sum\limits_{i=0}^{n}y_i\prod\limits_{j\not=i}\frac{x-x_j}{x_i-x_j}\)

此時,這道模板題我們就可以直接利用這個公式完成了。

附上程式碼:

/*
work by smyslenny
2021.06.27
P4781 【模板】拉格朗日插值
*/
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cmath>
#include <iomanip>
#include <cstring>
#include <cstdlib>
#include <queue>
#include <vector>

#define int long long
#define INF 0x3f3f3f3f

using namespace std;
const int mod=998244353;
const int M=2e3+5;
int x[M],y[M];
int n,k,Ans;
int read()
{
	register int x=0,y=1;
	register char c=getchar();
	while(c<'0'||c>'9') {if(c=='-') y=0;c=getchar();}
	while(c>='0'&&c<='9') { x=x*10+(c^48),c=getchar();}
	return y?x:-x;
}
int ksm(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 ny(int a)
{
	return ksm(a,mod-2);
}
signed main()
{
	n=read(),k=read();
	for(int i=1;i<=n;i++) x[i]=read(),y[i]=read();
	for(int i=1;i<=n;i++)
	{
		int fz=y[i]%mod,fm=1;
		for(int j=1;j<=n;j++)
		{
			if(i==j) continue;
			fz=fz*(k-x[j])%mod;
			fm=fm*(x[i]-x[j])%mod;
		}
		Ans=((Ans+fz*ny(fm)%mod)+mod)%mod;
	}
	printf("%lld\n",(Ans+mod)%mod);
	return 0;
} 

上面的方式的複雜度是 \(O(n^2)\) 的,對於一些題目中,給出的 \(x\) 的取值是連續的,那麼此時我們可以將複雜度優化到 \(O(n)\)

\(x\)取值連續時的優化

對於我們的式子,我們可以知道左邊的求和 \(O(n)\) 是不能動的,我們考慮使右邊的求積優化成 \(O(1)\) 的。

由於 \(x\) 是連續的,我們可以直接將式子轉化成 \(f(x)=\sum\limits_{i=0}^ n y_i\prod\limits_{j\not=i}\frac{x-j}{i-j}\)

對於分子,我們將其展開 \((x-1)\times(x-2)\times(x-3)\times\dots\times(x-(i-1))\times(x-(i+1))\times\dots\times(x-(j-1))\times(x-j)\)

我們維護一個關於 $ x$ 的字首積和字尾積(類比字首和),\(pre[i]=\prod\limits_{j=0}^i(x-j),suc[i]=\prod\limits_{j=i}^n(x-j)\)

分子就可以寫成 \(pre[i-1]\times suc[i+1]\)

再看分母,拆開

\(i\times(i-1)\times(i-2)\times(i-3)\times\dots\times (i-(i-1))\times(i-(i+1))\times\dots\times(i-n)\)

觀察一下,\((i-(i-1))=1\) ,在這一塊之前的可以看做是 \(1\) ~ \(i\) 的乘積,也就是 \(i!\) ,後面 \((i-(i+1))=-1\) 後面我們可以看做是 \(-1\backsim(i-n)\) 的階乘,為了好計算,我們可以看作是 \(1\backsim(n-i)\) 的階乘。

PS: 這裡我們要注意正負號,當所乘的總數為奇數個時,需要帶上負號,由於公式中不知道個數,下面預設為是整數,請不要認為就是正的。

所以分母可以化成 \(i!\times(n-i)!\)

右邊也就可以化成 \(\frac{pre[i-1]\times suc[i+1]}{i!\times(n-i)!}\) ,分子和分母都可以 \(O(n)\) 預處理,查詢是 \(O(1)\) 的,複雜度就可以優化成 \(O(n)\)

重心拉格朗日插值法

不難發現,當我們每插入一個值的時候,都需要推倒重算,這裡可以用重心拉格朗日插值法來解決。

\(f(x)=\sum\limits_{i=0}^{n}y_i\prod\limits_{j\not=i}\frac{x-x_j}{x_i-x_j}\)

\(=\frac{\prod\limits_{i\not=j}x-x_j}{\prod\limits_{i!=j}x_i-x_j}\)

上下乘 \(x-x_i\)

\(=\sum\limits_{i=1}^{n}y_i\frac{\prod\limits_{i=1}^n x-x_j}{\prod\limits_{i\not=j}(x_i-x_j)\times(x-x_i)}\)

\(=\prod\limits_{i=1}^n (x-x_j)\sum\limits_{i=1}^{n}\frac{y_i}{\prod\limits_{i\not=j}(x_i-x_j)\times(x-x_i)}\)

\(\lambda=\prod\limits_{i=1}^n (x-x_j),\mu(x)=\prod\limits_{i\not=j}(x_i-x_j)\)

\(f(x)=\lambda\sum\limits_{i=1}^n\frac{y_i}{\mu(x)\times(x-x_i)}\)

插入一個值,只用 \(O(n)\) 更新 \(\mu(x)\) ,因為分子我們已經乘上 \((x-x_i)\)

詢問一個值,\(O(n)\) 更新 \(\lambda\) ,再套公式即可。

參考資料:

牛頓插值的幾何解釋是怎麼樣的?

如何直觀地理解拉格朗日插值法?

拉格朗日插值學習小結

拉格朗日插值學習總結