拉格朗日插值学习笔记

简介

在数值分析中,拉格朗日插值法是以法国18世纪数学家约瑟夫·拉格朗日命名的一种多项式插值方法。如果对实践中的某个物理量进行观测,在若干个不同的地方得到相应的观测值,拉格朗日插值法可以找到一个多项式,其恰好在各个观测的点取到观测到的值。上面这样的多项式就称为拉格朗日(插值)多项式。

拉格朗日插值法

由小学知识可知 n+1 个点 (xi,yi) 可以唯一地确定一个 n 次多项式 y=f(x)

假设我们现在需要知道这个多项式在 k 点的取值,一个最显然的思路就是利用高斯消元直接求出多项式的系数,时间复杂度为 O(n3)。可以用到复杂度更优的拉格朗日插值法,即:

f(k)=i=0nyiijkx[j]x[i]x[j]

如果把 xi 带入公式中,除了第 i 项外的每一项分子中都有 xixi,那么最终只会留下 yi,因此正确性可以保证。单次求值的时间复杂度为 O(n2)

模板题code:

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=2023,mod=998244353;
int n,k,x[N],y[N],ans;
void add(int &x,int y){x+=y;if(x>mod) x-=mod;}
int mul(int a,int b){int res=1;while(b) ((b&1)&&(res=1ll*res*a%mod,0)),a=1ll*a*a%mod,b>>=1;return res;}
int main()
{
	scanf("%d%d",&n,&k);for(int i=1;i<=n;i++) scanf("%d%d",&x[i],&y[i]);
	for(int i=1;i<=n;i++)
	{
		int tmp=y[i];
		for(int j=1;j<=n;j++)
		{
			if(j==i) continue;
			tmp=1ll*tmp*(k-x[j])%mod*mul((x[i]-x[j]),mod-2)%mod;
		}
		add(ans,(tmp+mod)%mod);
	}
	printf("%d\n",ans);
	return 0;
}

横坐标是连续整数的拉格朗日插值

如果已知的 n+1 个点的横坐标是连续的,那么可以将单词求值的复杂度优化到 O(n)

由于横坐标连续,假设这 n+1 个点的横坐标分别为 0,1,2n,原先的插值公式可以改写为:

f(x)=i=0nyijixjij

对于公式中累乘的部分,可以将分母分子分开单独考虑,其中分子为:

ji(xj)=i=0nxjxi

而分母可以拆成两段阶层来算,需要注意正负符号:

(1)ni×i!×(ni)!

于是公式就可以进一步改写为:

f(x)=i=0nyii=0n(xj)(xi)×(1)ni×i!×(ni)!

预处理一下就可以做到 O(n)

教科书般的亵渎

小豆喜欢玩游戏,现在他在玩一个游戏遇到这样的场面,每个怪的血量为 ai,且每个怪物血量均不相同,小豆手里有无限张“亵渎”。亵渎的效果是对所有的怪造成 1 点伤害,如果有怪死亡,则再次施放该法术。我们认为血量为 0 怪物死亡。

小豆使用一张“亵渎”会获得一定的分数,分数计算如下,在使用一张“亵渎”之后,每一个被亵渎造成伤害的怪会产生 xk,其中 x 是造成伤害前怪的血量为 x 和需要杀死所有怪物所需的“亵渎”的张数 k

m50n1013,1ainai 各不相同。

思路:

本题的关键在于求出多项式 i=1nik 的值。

对于函数 f(n)=i=1nik,可以采用归纳法证明其一定为一个 k+1 次多项式。

那么只需要取连续的 k+2 个点,就可以在 O(n) 时间内求出 f(n) 的值。

code:

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=66,mod=1e9+7;
int f[N],pre[N],n,m,k,a[N];
int mul(int a,int b){int res=1;while(b) ((b&1)&&(res=1ll*res*a%mod,0)),a=1ll*a*a%mod,b>>=1;return res;}
void Add(int &a,int b){a+=b;if(a>=mod) a-=mod;}
void Sub(int &a,int b){a-=b;if(a<0) a+=mod;}
int calc(int x)
{
	if(x<=k+2) return f[x];
    int y=0,sum=1,ans=0;
	for(int i=1;i<=k+2;i++) sum=1ll*sum*(x-i)%mod;
	for(int i=1;i<=k+2;i++)
	{
		Add(y,mul(i,k));int val=mul((1ll*(x-i)*pre[i-1]%mod*pre[(k+2)-i]%mod),mod-2);
		if(k+2-i&1) Sub(ans,1ll*y*val%mod*sum%mod);else Add(ans,1ll*val*y%mod*sum%mod);
	}
//	printf("%d\n",ans);
	return ans;
}
void solve()
{
	scanf("%d%d",&n,&m);k=m+1;int ans=0;for(int i=1;i<=m;i++) scanf("%d",&a[i]),Sub(ans,mul(a[i],k));
	for(int i=1;i<=k+2;i++) f[i]=(f[i-1]+mul(i,k))%mod;sort(a+1,a+m+1);Add(ans,calc(n));
    for(int i=1;i<=m;i++) Add(ans,calc(n-a[i]));
    for(int i=1;i<=m;i++) for(int j=i-1;j>=1;j--) Sub(ans,mul(a[i]-a[j],k));
    printf("%d\n",ans);
}
int main()
{
	pre[0]=1;for(int i=1;i<N;i++) pre[i]=1ll*pre[i-1]*i%mod;
	int T;scanf("%d",&T);while(T--) solve();
	return 0;
}

CF995F Cowmpany Cowmpensation

一棵 n 个节点的树,给每个节点分配工资([1,D]),子节点不能超过父亲节点的工资,问有多少种分配方案。

1n3000 , 1D109

思路:

由于 n 的最大值很小,而 D 的最大值很大,可以考虑容斥

除此之外,回到暴力的 dp 转移方程来,令 fi,j 表示以 i 为根的子树用 [1,i] 染色的方案数,可以得到状态转移方程:

fu,i=fu,i1+vsonufv,i

那么最终的答案就是 f1,d。这种做法的复杂度为 O(nd),考虑用拉格朗日插值优化。

u 的子树大小为 su。那么,可以猜测 fu,x 是关于 xsu 次函数。

证明可以用到归纳法:

  • u 为叶子节点时,f(u,x)=1,成立。

  • vsonu 时成立,考虑 u

fu,xfu,x1=vsonufv,x

由于 fv,x 的次数为 sv,则 fu,xfu,x1 的次数为 vsonusv=su1。再还原查分,那么 fu,x 就是关于 xsu1+1=su 次函数。

因此,只需要预处理出 fu,1,fu,2f(u,su+1) 的值,就可以用插值求出 fu,d 的值。

code:

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=3010,mod=1e9+7;
int n,d,h[N],idx,f[N][N],fac[N];
struct edge{int v,nex;}e[N];
void add(int u,int v){e[++idx]=edge{v,h[u]};h[u]=idx;}
void Add(int &a,int b){a+=b;if(a>=mod) a-=mod;}
void Sub(int &a,int b){a-=b;if(a<0) a+=mod;}
void Mul(int &a,int b){a=1ll*a*b%mod;}
int mul(int a,int b){int res=1;while(b) ((b&1)&&(res=1ll*res*a%mod,0)),a=1ll*a*a%mod,b>>=1;return res;}
void dfs(int u)
{
	for(int i=1;i<=n+1;i++) f[u][i]=1;
	for(int i=h[u];i;i=e[i].nex)
	{
		int v=e[i].v;dfs(v);
		for(int j=1;j<=n+1;j++) Mul(f[u][j],f[v][j]);
	}
	for(int i=1;i<=n+1;i++) f[u][i]=(f[u][i-1]+f[u][i])%mod;
}
int main()
{
	scanf("%d%d",&n,&d);for(int v=2,u;v<=n;v++) scanf("%d",&u),add(u,v);dfs(1);
	if(d<=n+1) printf("%d\n",f[1][d]);
	else
	{
		fac[0]=1;for(int i=1;i<=n+1;i++) fac[i]=1ll*fac[i-1]*i%mod;int sum=1,y,ans=0;
		for(int i=1;i<=n+1;i++) Mul(sum,d-i);
		for(int i=1;i<=n+1;i++)
		{
			y=f[1][i];int val=1ll*y*sum%mod*mul(1ll*fac[i-1]*fac[n+1-i]%mod*(d-i)%mod,mod-2)%mod;
			if(n+1-i&1) Sub(ans,val);else Add(ans,val);
		}
		printf("%d\n",ans);
	}
	return 0;
}

填树

给定一棵 n 个点的树,每个的权值 wi0 或为 [li,ri] 之间的整数,选定一棵树上的路径,要求满足 maxwiminui=K,求这样的方案数以及权值之和。

1n2001liri1091K109

思路:

考虑钦定点权的范围为 [w,w+K],那么只需要树形dp就可以得出这一部分的答案。考虑将点权依照 li,ri,w,w+K 分割成 O(n) 个区间。

根据上一题的经验,令 fu,x 为点权在 [x,x+K] 范围内时的方案数。那么 fu,x 就是一个关于 xn+1 次函数、同理,权值和就是一个关于 xn+2 次多项式,那么就可以利用拉格朗日插值在 O(n) 时间内求出这部分区间的答案,总的时间复杂度即为 O(n3)

最后还需要注意,在钦定范围为 [w,w+K] 时,点权的实际取值范围可能在 [w+1,w+K],那么只需要多做一遍插值减去这部分的贡献即可。

code:

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=1010,mod=1e9+7;
int n,K,num[N],l[N],r[N],f[N],g[N],d1[N],d2[N],pre[N],suf[N],Y1[N],Y2[N],sig[2],ans1,ans2,h[N],idx,infac[N];
struct edge{int v,nex;}e[N];
void add(int u,int v){e[++idx]=edge{v,h[u]};h[u]=idx;}
void Add(int &a,int b){a+=b;if(a>=mod) a-=mod;if(a<0) a+=mod;}
void dfs(int u,int fa,int L,int R)
{
	f[u]=g[u]=d1[u]=d2[u]=0;
	int ql=max(L,l[u]),qr=min(R,r[u]);
	int cntu=max(qr-ql+1,0),sumu=(ql<=qr)?1ll*(ql+qr)*(qr-ql+1)/2%mod:0;
	f[u]=d1[u]=cntu,g[u]=d2[u]=sumu;
	for(int i=h[u];i;i=e[i].nex)
	{
		int v=e[i].v;if(v==fa) continue;dfs(v,u,L,R);
		(f[u]+=f[v])%=mod;(g[u]+=g[v])%=mod;
		g[u]=(g[u]+1ll*d2[u]*d1[v]+1ll*d1[u]*d2[v])%mod;
		d2[u]=(d2[u]+1ll*d2[v]*cntu+1ll*d1[v]*sumu)%mod;
		f[u]=(f[u]+1ll*d1[u]*d1[v])%mod;d1[u]=(d1[u]+1LL*d1[v]*cntu)%mod;
	}
}
void solve(int w)
{
	int m=0;num[++m]=w+1,num[++m]=1e9+1;
	for(int i=1;i<=n;i++)
	{
		if(l[i]>w) num[++m]=l[i];
		if(l[i]>w) num[++m]=r[i];
		if(l[i]>K) num[++m]=l[i]-K+w;
		if(r[i]>K) num[++m]=r[i]-K+w;
	}
	sort(num+1,num+m+1);m=unique(num+1,num+m+1)-num-1;
	for(int i=1;i<m;i++)
	{
		int k=min(num[i+1]-num[i],n+3),x=num[i+1]-num[i];pre[0]=suf[k+1]=1;
		for(int j=1;j<=k;j++) dfs(1,0,num[i]+j-1,num[i]+j+K-w-1),Y1[j]=f[1],Y2[j]=g[1];
		for(int j=2;j<=k;j++) Add(Y1[j],Y1[j-1]),Add(Y2[j],Y2[j-1]);
		for(int j=1;j<=k;j++) pre[j]=1ll*pre[j-1]*(x-j+mod)%mod;
		for(int j=k;j>=1;j--) suf[j]=1ll*suf[j+1]*(x-j+mod)%mod;
		for(int j=1;j<=k;j++)
		{
			int res=1ll*pre[j-1]*suf[j+1]%mod*infac[j-1]%mod*infac[k-j]%mod*sig[k-j&1]%mod;
			Add(ans1,1ll*sig[w]*res%mod*Y1[j]%mod);Add(ans2,1ll*sig[w]*res%mod*Y2[j]%mod);
		}
	}
}
int main()
{
//	freopen("tree3.in","r",stdin);
	scanf("%d%d",&n,&K);for(int i=1;i<=n;i++) scanf("%d%d",&l[i],&r[i]);
	for(int u,v,i=1;i<n;i++) scanf("%d%d",&u,&v),add(u,v),add(v,u);infac[0]=infac[1]=sig[0]=1;sig[1]=mod-1;
	for(int i=2;i<N;i++) infac[i]=1ll*(mod-mod/i)*infac[mod%i]%mod;
	for(int i=2;i<N;i++) infac[i]=1ll*infac[i-1]*infac[i]%mod;
	solve(0),solve(1);printf("%d\n%d\n",ans1,ans2);
	return 0;
}
posted @   曙诚  阅读(62)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
点击右上角即可分享
微信分享提示