首页 > 其他分享 >《拉格朗日插值》小记

《拉格朗日插值》小记

时间:2023-10-01 17:12:53浏览次数:52  
标签:拉格朗 limits 插值 LL ret int MAXN MODD 小记

随便学学,主要是又被卡科技了。

参考文章:

\(Alex\_Wei\) 的拉格朗日插值与多项式乘法

\(Alex\_Wei\) 的多项式 I:拉格朗日插值与快速傅里叶变换

\(yyc\) 的从拉插到快速插值求值

算法介绍

公式口糊

主要用来对于一个给定的 \(n\) 次多项式,用 \(n+1\) 个点值在 \(O(n^2)\) 的时间复杂度内求出多项式的各项系数。这就是插值。

其实这东西和中国剩余定理的思路是很像的,就是对于一个点值而言当 \(k=x_i\) 时,只有 \(y_i\) 是显出来的。其他都是不显的。

所以我们的式子中得有 \(k-x_i\) 的因数。然后我们还要再乘上一个 \(y_i\) 。

那么我们式子就变成了 \(f(k)=\sum\limits_{i=1}^n y_i\prod\limits_{i\not =j} {k-x_j}\)

但是我们发现这样仍然不对,我们还要再除以一个 \(x_i-x_j\) 才对,所以最后公式长这样。

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

拉插的公式还是比较好理解的。

实际上拉插的思路就是构造出一个等价于原函数的函数,以此求值。

如何求解原函数的各项系数呢。

我们可以记 \(D(k)=\prod\limits_{i=1}^n k-x_i,c_i=y_i\prod\limits_{i\not =j}^n \frac 1{x_i-x_j}\) ( \(D(k)\) 是一个多项式而非点值)

那么原式变成 \(\sum\limits_{i=1}^n y_i\frac {D(k)}{k-x_i}\)

我们记 \(A_i(k)\) 为 \(\frac {D(k)}{k-x_i}\) (这是一个多项式,而不是一个点值)

那么我们记答案函数为 \(g\)

\(g=c_1A_i(k)+c_2A_2(k)+...+c_nA_n(k)\)

首先对于 \(D(k)\) 我们可以在 \(O(n^2)\) 的时间复杂度内预处理出来(注意 \(k\) 是什么不影响,因为我们求的是系数,而非真正的值,所以可以看成 \(x\) ,就是说没有具体的值)

而对于每个 \(A_i(k)\) 可以在 \(O(n)\) 的时间内用 \(D(k)\) 除以一次多项式的方法得到,具体实现和反背包类似。

然后把所有系数加起来就得到了我们 \(g\) 的答案。

时间复杂度 \(O(n^2)\)

点击查看代码
#include<bits/stdc++.h>
typedef long long LL;

using namespace std;
const int MAXN=5e3+10,MODD=998244353;
int n;
LL x[MAXN],y[MAXN],d[MAXN],D[MAXN],g[MAXN],f[MAXN];
LL ksm(LL x,LL y) {
	LL ret=1;
	while(y) {
		if(y&1) ret=ret*x%MODD;
		x=x*x%MODD;
		y>>=1;
	}
	return ret;
}
int main () {
	scanf("%d",&n);
	for(int i=1;i<=n;++i) {
		scanf("%d%d",&x[i],&y[i]);
	}
	for(int i=1;i<=n;++i) {
		d[i]=1;
		for(int j=1;j<=n;++j) {
			if(i==j) continue;
			d[i]=(d[i]*(x[i]-x[j]+MODD)%MODD)%MODD;
		}
		d[i]=ksm(d[i],MODD-2)*y[i]%MODD;
	}
	D[0]=1;
	for(int i=1;i<=n;++i) {
		for(int j=n-1;j>=1;--j) {
			D[j]=(D[j-1]+D[j]*(MODD-x[i])%MODD)%MODD;
		}
		D[0]=D[0]*(MODD-x[i])%MODD;
	}
	for(int i=1;i<=n;++i) {
		LL cs=ksm(MODD-x[i],MODD-2);
		g[0]=D[0]*cs%MODD;
		for(int j=1;j<n;++j) {
			g[j]=(D[j]-g[j-1]+MODD)*cs%MODD;
		}
		for(int j=0;j<n;++j) {
			f[j]=(f[j]+d[i]*g[j]%MODD)%MODD;
		}
	}
	for(int i=0;i<n;++i) {
		printf("%lld ",f[i]);
	}
	return 0;
}

对于点值取值连续的处理

令 \(pre_i=\prod\limits_{j=1}^i k-i,suf_i=\prod\limits_{j=i}^n k-i\)

\[\begin{aligned} f(k)&=\sum\limits_{i=1}^n y_i\prod\limits_{i\not =j} \frac {k-x_j}{x_i-x_j}=\sum\limits_{i=1}^n y_i\prod\limits_{j=1}^{i-1} \frac {(k-j)}{i-j} (-1)^{n-i} \prod\limits_{j=i+1}^{n}\frac {(k-j)}{j-i} \\ &=\sum\limits_{i=1}^n y_i\frac {pre_{i-1}}{(i-1)!}\frac {suf_{i+1}}{(n-i)!}(-1)^{n-i} \end{aligned} \]

上面的东西可以快速 \(O(n)\) 预处理出来。

所以对于点值连续的我们可以做到 \(O(n)\) 完成插值。

拉格朗日插值求解 \(dp\)

有时候在做一些 \(dp\) 题的时候,我们可以得到一个 \(dp_{i,j}\) 的转移但是第一维 \(j\) 很大,或第一维 \(i\) 很大,而我们通过证明答案是一个次数与另一维很小的变量有关的一个多项式,那么我们可以带若干个点值进去,然后对于无法求解的答案,通过插值获取。

The Sum of the k-th Powers

首先答案是一个 \(k+1\) 次多项式,虽然我不会证明。

然后我们求出前 \(k+1\) 个的点值,然后由于 \(k\) 比较大,所以考虑点值取值连续,然后 \(O(n)\) 插值即可。

时间复杂度关于 \(k\) 的先行对数,瓶颈在于求 \(k+1\) 个点值,用线性筛只筛质数,可以做到线性复杂度。

点击查看代码
#include<bits/stdc++.h>
typedef long long LL;

using namespace std;
const int MAXN=1e6+10,MODD=1e9+7;
LL n,k;
LL s[MAXN],pre[MAXN],suf[MAXN],pw[MAXN];
LL ksm(LL x,LL y) {
	LL ret=1;
	while(y) {
		if(y&1) ret=ret*x%MODD;
		x=x*x%MODD;
		y>>=1;
	}
	return ret;
}
int main () {
	scanf("%lld%lld",&n,&k);
	for(int i=1;i<=k+2;++i) {
		s[i]=(s[i-1]+ksm(i,k))%MODD;
	}
	pre[0]=1;
	for(int i=1;i<=k+2;++i) {
		pre[i]=pre[i-1]*(n-i)%MODD;
	}
	suf[k+3]=1;
	for(int i=k+2;i>=1;--i) {
		suf[i]=suf[i+1]*(n-i)%MODD;
	}
	pw[0]=1;
	for(int i=1;i<=k+2;++i) pw[i]=pw[i-1]*i%MODD;
	LL ans=0;
	for(int i=1;i<=k+2;++i) {
		ans=(ans+((k+2-i)&1?-1:1)*s[i]*pre[i-1]%MODD*suf[i+1]%MODD*ksm(pw[i-1],MODD-2)%MODD*ksm(pw[k+2-i],MODD-2)%MODD+MODD)%MODD;
	}
	printf("%lld\n",ans);
	return 0; 
}

P4463 [集训队互测 2012] calc

首先直接暴力 \(O(nk)\) 是很简单的,拆贡献,记 \(f_{i,j}\) 前 \(i\) 选了 \(j\) 个数的所有方案各自乘积的和,然后一起乘上 \(i\) 即可。

最后再乘一个 \(n!\) 就可以得到答案。

考虑如何进一步求解。

由于 \(i\) 很大,我们可以猜测 \(f_{i,j}\) 是关于 \(i\) 的 \(kj+b\) 次多项式( \(k,b\) 是常数)

\(f_{i,j}=f_{i-1,j}+f_{i-1,j-1}\times i\)

\(\color{red}\text{一个经典套路,差分证明次数}\)

\(f_{i,j}-f_{i-1}{j}=i\times f_{i-1,j-1}\)

我们记 \(g_{i,j}\) 表示 \(f_{i,j}\) 是关于 \(i\) 的一个 \(g_{i,j}\) 次多项式。

那么根据多项式差分有左边式子的次数是 \(g_{i,j}-1\) 。然后右边式子的次数显然是 \(g_{i-1,j-1}+1\)

所以 \(g_{i,j}-1=g_{i-1,j-1}+1\)

而当 \(j=0\) 时 \(g_{i,0}=0\) ,所以 \(g_{i,j}\) 是一个 \(2j\) 次多项式。

然后带 \(2n+1\) 个点值进去然后插一下就好了。

时间复杂度 \(O(n^2)\)

点击查看代码
#include<bits/stdc++.h>
typedef long long LL;

using namespace std;
const int MAXN=1010;
LL k,n,P;
LL f[MAXN][MAXN];
LL ksm(LL x,LL y) {
	LL ret=1;
	while(y) {
		if(y&1) ret=ret*x%P;
		x=x*x%P;
		y>>=1;
	}
	return ret;
}
int main () {
	scanf("%lld%lld%lld",&k,&n,&P);
	f[0][0]=1;
	for(int i=1;i<=2*n+1;++i) {
		for(int j=0;j<=2*n+1;++j) {
			f[i][j]=f[i-1][j];
			if(j) f[i][j]=(f[i][j]+f[i-1][j-1]*i)%P;
		}
	}
	LL ans=0;
	for(int i=1;i<=2*n+1;++i) {
		LL fz=f[i][n],fm=1;
		for(int j=1;j<=2*n+1;++j) {
			if(i==j) continue;
			fz=fz*(k-j)%P; fz=(fz+P)%P;
			fm=(fm*(i-j)%P+P)%P;
		}
		ans=(ans+fz*ksm(fm,P-2)%P)%P;
	}
	for(int i=1;i<=n;++i) ans=(ans*i)%P;
	printf("%lld\n",ans);
	return 0; 
}

Cowmpany Cowmpensation

证明一下答案是 \(n\) 次多项式即可。

然后简单地树形 \(dp\)

时间复杂度 \(O(n^2)\)

点击查看代码
#include<bits/stdc++.h>
typedef long long LL;

using namespace std;
const int MAXN=3010,P=1e9+7;
int n,D;
vector<int> e[MAXN];
void adline(int f,int t) {
	e[f].push_back(t);
}
LL add(LL x,LL y) {
	return (x+y>=P?x+y-P:x+y);
}
LL f[MAXN][MAXN],g[MAXN][MAXN],sz[MAXN],cz[MAXN];
void dfs(int u) {
	for(int i=1;i<=n+1;++i) {
		f[u][i]=1;
	}
	sz[u]=1;
	for(auto t:e[u]) {
		dfs(t); sz[u]+=sz[t];
		for(int j=1;j<=n+1;++j) {
			f[u][j]=f[u][j]*g[t][j]%P;
		}
	}
	for(int i=1;i<=n+1;++i) {
		g[u][i]=add(g[u][i-1],f[u][i]);
	}
}
LL ksm(LL x,LL y) {
	LL ret=1;
	while(y) {
		if(y&1) ret=ret*x%P;
		x=x*x%P;
		y>>=1;
	}
	return ret;
}
void lglrcz(LL *a,LL n,LL k) {
	LL ans=0;
	for(int i=1;i<=n;++i) {
		LL fz=a[i],fm=1;
		for(int j=1;j<=n;++j) {
			if(i==j) continue;
			fz=(fz*(k-j))%P;
			fm=(fm*(i-j))%P;
		}
		fz=(fz+P)%P; fm=(fm+P)%P;
		ans=(ans+fz*ksm(fm,P-2)%P)%P;
	}
	printf("%lld\n",ans);
}
int main () {
	scanf("%d%d",&n,&D);
	for(int i=2;i<=n;++i) {
		int x;
		scanf("%d",&x);
		adline(x,i);
	}
	dfs(1);
	for(int i=1;i<=n+1;++i) {
		cz[i]=g[1][i];
	}
	lglrcz(cz,n+1,D);
	return 0; 
}

标签:拉格朗,limits,插值,LL,ret,int,MAXN,MODD,小记
From: https://www.cnblogs.com/ddl1no2home/p/17739004.html

相关文章

  • 23.9.29中秋小记
    这是我正式工作以来的第一个中秋。但好像我也没有很想家,没有思念的人。可能在我心中,家这个概念已经不存在了吧。究竟是从什么时候开始的呢?我也不知道虽然父母健在,他们也没有离婚,但是没有家了。......
  • Lagrange插值
    本文主要参考资料:找通项的终极方法!让每个人都能听懂的【拉格朗日插值法】_哔哩哔哩_bilibili回顾,多项式的系数表示法和点值表示法:FFT(快速傅立叶变换)学习-Isakovsky-博客园(cnblogs.com)从系数表示法到点值表示法的运算叫做求值运算,从点值表示法到系数表示法的运算叫做......
  • 《prufer 序列》小记
    今天模拟赛被卡科技了,学一下这个东西,之前也看到很多次,只不过一直都没学。算法简介这是一种可以将带标号的树,转成唯一的整数序列表示的方法。而在“数树”题中也有大用。算法流程大概是将带标号的\(n\)个节点的数用\([1,n]\)中的\(n-2\)个整数来表示一个树。也可以理解成......
  • 莫比乌斯反演小记
    基本内容莫比乌斯函数\(\mu\)定义为\(1\)的逆。一些小性质:\(\mu*1=\epsilon\)\(\mu*\text{id}=\varphi\)反演内容我的理解是:\[[a=1]=\sum\limits_{d|a}\mu(d)\]典型例题例1P2398GCDSUM求\[\sum\limits_{i=1}^n\sum\limits_{j=1}^n\gcd(i,j)\]来推下式......
  • git blame 用法小记
    1、概述git管理的代码仓库,在协作开发中不可避免地会出现代码冲突,或者有新手错误地提交代码。出现问题不可怕,可怕的是找不到问题出在哪里。有时候找到出问题的代码,却不知道是谁提交的。git提供了一个有用的命令gitblame来帮你查看一个文件的每一行是如何被修改的,以及由谁修改......
  • loader编写小记
    此项目在一些大佬的基础上进行了修改,或许能提供一些思路。还在学习中很菜很菜,不足之处还请师傅们多多指点......
  • 动态DP小记
    前言矩阵乘法优化DP,重链剖分。涉及到的知识点是比较复杂的,但是比较重要。这是猫锟在WC2018讲的黑科技,一般用来解决树上的带有点权(边权)修改操作的DP问题,为了普及,甚至CSP2022-ST4考到了此知识点。做法朴素DP设\(dp_{i,0}\)表示不选\(i\),\(i\)的子树的最大权独立集......
  • 拉格朗日插值 学习笔记
    拉格朗日插值学习笔记前言模拟赛考了,我不会,故学之。真的好抽象……背景众所周知,用\(n\)个点可以确定一个\(n-1\)次的多项式,那么应该如何确定呢?我们不妨考虑这样一个题目(其实就是洛谷模板题):给定\(n\)个点\((x,y)\),要求确定\(f(x)\)。当然,直接求出系数可能比较困难,......
  • 插值法
    多项式插值法使用n+1个点,确定一条唯一的多项式:多项式满足:写成矩阵的形式:显然左边是范德蒙德行列式,且有唯一解。根据克拉默法则,解为:其中将解代入原式子得到:二次累加可以交换次序: 其中: 其满足范德蒙德行列式,范德蒙德行列式计算如下:计算公式中的元素得到(不......
  • MSSQL 维护小记(清理进程、重建索引)
    ------------------------------清理进程----------------------------------- declare@deleteSleepSessionnvarchar(100)--申明一个变量declaretablelistcursorlocal--申明一个本地游标forselect'kill'+rtrim(spid)frommaster.dbo.sysprocesses--数据库系统进......