多项式全家桶(长期更新)

更新日志:

24.7.25:更新了暴力多项式全家桶部分。
24.7.27:更新了单位根性质、FFT以及NTT部分。
24.7.29:更新了导数部分。
24.8.5:更新了积分,泰勒展开,快速多项式全家桶,常系数齐次线性递推以及Lagrange插值部分。
25.2.11:挖了更多的坑,加入了多项式全家桶的部分封装代码。

Code:

Polynomial

前言

若无特殊说明,默认运算在(modxn)(modp)p为质数)意义下进行,大写字母为多项式(如F(x)),小写字母为系数(如fi)。

暴力多项式全家桶

加法

H(x)=F(x)+G(x),求H(x)

Sol:

hi=fi+giO(n)

乘法

H(x)=F(x)G(x),求H(x)

Sol:

hi=j=0ifjgijO(n2)

取模

F(x)=Q(x)G(x)+R(x),求Q(x),R(x)

Sol:

如高精度除法模拟,O(n2)

求逆

F(x)G(x)1(modxn),求G(x)

Sol:

注意到i=0nfigni=[n=0]

我们将i=0的项抽出来:f0gn=[n=0]i=1nfigni

于是就有gn=[n=0]i=1nfignif0,可以进行O(n2)递推。

求导&积分

G(x)=F(x)

H(x)=F(x)dx

G(x)F(x)

Sol:

gi=(i+1)fi+1

hi=fi1i

O(n)

开根

G2(x)F(x)(modxn)

G(x)

Sol:

fi=j=0igjgij

类似求逆时的处理方法:2g0gi=fij=1i1gjgij

移一下项就有:gi=fij=1i1gjgij2g0

注意g02=f0是边界情况:

  • f00,则G的解数,取决于f0的平方根数量。

  • f0=0,令F(x)=xkH(x),满足h00,那么若k为奇数,则G无解;若k为偶数,记P2(x)=H(x),那么G(x)=xk2P(x)

ln

G(x)lnF(x)(modxn)

保证f0=1

G(x)

Sol:

对原式左右两边求导:G(x)F(x)F1(x)(modxn)

先求出H(x)F1(x)(modxn)

那么有(i+1)gi+1=j=0i(j+1)fj+1hij,外加g0=0即可。

O(n2)

exp

G(x)eF(x)(modxn)

保证f0=0

G(x)

Sol:

狠狠的求导:G(x)=F(x)eF(x)=F(x)G(x)

那么就有(i+1)gi+1=j=0i(j+1)fj+1gij

外加g0=1即可。

O(n2)

三角函数

G(x)sinF(x)

H(x)cosF(x)

G(x),H(x)

Hint:

sinx=eixeix2i

cosx=eix+eix2

(由欧拉公式得到)

Sol:

直接把F(x)带进公式,利用exp算法即可。

ip意义下有对应的数,那么可以直接用这个数来运算。

否则可以扩域,复杂度都是O(n2)

FFT & NTT

前置

多项式插值

n+1个横坐标互不相同的点可以唯一确定一个n次多项式。

证:

利用待定系数法可以得到一个n+1元一次方程组。

观察系数矩阵,可以发现其一定是满秩的。(关于矩阵的秩,见线性代数

也就意味着消元时不可能消掉一个变量的同时消掉另一个变量,那当然就有一个唯一解。

复数

复数a+bi可看成复平面上的一个向量(a,b),可观察出其运算的几何意义:

  • 加法:向量加法。

  • 乘法:模长相乘,辐角相加。

单位根

  • 代数基本定理:任何复系数一元n次多项式方程在复数域上至少有一根。
  • 推论:任何复系数一元n次多项式方程在复数域上恰好有n个根。

其中xn1=0n个根分别记为ωn1,ωn2ωnn1,统称为n次单位根。

根据其几何意义,可知ωnk=cos2kπn+isin2kπn

并且ωn0=1总是成立的。

单位根将单位圆n等分。

拓宽一下ωnii的范围,结合几何意义,可以得到以下性质:

  • ωni+n=ωni

  • ωniωnj=ωni+j

  • (ωni)j=ωnij

  • ωni=ωknki

  • ωni+n2=ωni

FFT

我们现在要求H(x)=F(x)G(x)

根据多项式插值中的原理,我们可以将F(x)G(x)进行多点求值,得到

(x1,F(x1)),(x2,F(x2))(xn,F(xn)),以及

(x1,G(x1)),(x2,G(x2))(xn,G(xn))这两系列点。

(这一步将系数表示法转化为点值表示法。)

H(x)=F(x)G(x),可知iH(xi)=F(xi)G(xi)

所以将上文两系列点相乘即可得到H(x)的点值表示,再将点值表示转化为系数表示即可得到H(x)。(这也叫插值)

Question 1

普通单点求值是O(n)的,求n个点的值就O(n2)了。

那么如何选取x1,x2xn使得多点求值能快速进行?

Sol:

选取xi=ωni

由于单位根的一些性质,我们可以O(nlogn)进行多点求值。

定义操作DFT:{fi}{F(ωni)},即输入F(x)的系数列,得到F(x)在单位根处的点值表示。

考虑如何计算F(ωnk)=i=0nfi(ωnk)i

n=2m,我们按i的奇偶性分组:

F(ωnk)=i=0nfi(ωnk)i=i=0n2f2i(ωnk)2i+i=0n2f2i+1(ωnk)2i+1=i=0n2f2i(ωnk)2i+ωnki=0n2f2i+1(ωnk)2i=i=0n2f2i(ωn2k)i+ωnki=0n2f2i+1(ωn2k)i=DFT({f2i})k+ωnkDFT({f2i+1})k

于是可以递归,每次n减半,合并的过程是O(n)的,所以DFT的复杂度为O(nlogn)

Question 2

如何以优秀的时间复杂度将点值表示转化为系数表示?

Sol:

定义操作IDFT:{H(xi)}{hi},即将点值表示转化为系数表示。

注意到一个重要的式子:

i=0n1(ωni)k=n[nk]

(即单位根反演)

证明:

i=0n1(ωni)k=i=0n1(ωnk)i

发现这是等比数列求和的形式,讨论公比ωnk

  • nk,则ωnk=1,故i=0n1(ωni)k=i=0n1(ωnk)i=n

  • nk,则ωnk1,故i=0n1(ωni)k=i=0n1(ωnk)i=(ωnk)n1wnk1=0

综上,原式得证。

于是又有另一个式子:nhk=i=0n1H(ωni)(ωnk)i

证明:

nhk=i=0n1H(ωni)(ωnk)i=i=0n1(ωnk)ij=0n1hj(ωni)j=i=0n1j=0n1(ωnk)i(ωni)jhj=j=0n1hji=0n1(ωni)jk=j=0n1nhj[njk]=nhk

有了这个式子后,IDFT的其余操作与DFT的操作几乎相同,同样可以做到O(nlogn)

至此,我们成功以O(nlogn)的时间复杂度解决了多项式乘法。

Question 3

如何代码实现?

Sol:

直接按上面的流程做,是递归版本的写法。

(摘自oiwiki

#include <cmath>
#include <complex>

typedef std::complex<double> Comp;  // STL complex

const Comp I(0, 1);  // i
const int MAX_N = 1 << 20;

Comp tmp[MAX_N];

// rev=1,DFT; rev=-1,IDFT
void DFT(Comp* f, int n, int rev) {
  if (n == 1) return;
  for (int i = 0; i < n; ++i) tmp[i] = f[i];
  // 偶数放左边,奇数放右边
  for (int i = 0; i < n; ++i) {
    if (i & 1)
      f[n / 2 + i / 2] = tmp[i];
    else
      f[i / 2] = tmp[i];
  }
  Comp *g = f, *h = f + n / 2;
  // 递归 DFT
  DFT(g, n / 2, rev), DFT(h, n / 2, rev);
  // cur 是当前单位复根,对于 k = 0 而言,它对应的单位复根 omega^0_n = 1。
  // step 是两个单位复根的差,即满足 omega^k_n = step*omega^{k-1}*n,
  // 定义等价于 exp(I*(2*M_PI/n*rev))
  Comp cur(1, 0), step(cos(2 * M_PI / n), sin(2 * M_PI * rev / n));
  for (int k = 0; k < n / 2;++k) {  
    // F(omega^k_n) = G(omega^k*{n/2}) + omega^k*n\*H(omega^k*{n/2})
    tmp[k] = g[k] + cur * h[k];
    // F(omega^{k+n/2}*n) = G(omega^k*{n/2}) - omega^k_n*H(omega^k\_{n/2})
    tmp[k + n / 2] = g[k] - cur * h[k];
    cur *= step;
  }
  for (int i = 0; i < n; ++i) f[i] = tmp[i];
}

当然,我们还可以进一步压缩常数,就是将递归版改成非递归版。

事实上从上往下递归的过程中将f数组的下标对应的二进制数翻转了,例如f(1011)2最后被放到了f(1101)2上。

因此我们可以非递归地完成这一步,然后向上合并即可。

交换位置的操作通过预处理可做到O(n)

代码如下:

#include<bits/stdc++.h>

using namespace std;

const int maxn=1<<22;
const double pi=acos(-1.0);
struct com{
	double x,y;
	
	com(double a=0,double b=0):x(a),y(b){}
	
	com operator+(const com &a) const{
		return com(x+a.x,y+a.y);
	}
	
	com operator-(const com &a) const{
		return com(x-a.x,y-a.y);
	}
	
	com operator*(const com &a) const{
		return com(x*a.x-y*a.y,x*a.y+y*a.x);
	}
}a[maxn],b[maxn];
int n,m,len,rev[maxn];

void init(int len){
	for(int i=0;i<len;++i){
		rev[i]=rev[i>>1]>>1;
		if(i&1) rev[i]|=len>>1;
	}
}

void DFT(com f[],int len,int rv){
	for(int i=0;i<len;++i){
		if(i<rev[i]) swap(f[i],f[rev[i]]);
	}
	for(int h=2;h<=len;h<<=1){
		com wn(cos(2.0*pi/h),sin(2.0*rv*pi/h));
		for(int i=0;i<len;i+=h){
			com w(1.0,0);
			for(int k=i;k<i+h/2;++k){
				com u=f[k];
				com v=w*f[k+h/2];
				f[k]=u+v;
				f[k+h/2]=u-v;
				w=w*wn;
			}
		}
	}
	if(rv==-1){
		for(int i=0;i<len;++i){
			f[i].x/=len;
		}
	}
}

int main(){
	scanf("%d%d",&n,&m);
	for(int i=0;i<=n;++i) cin>>a[i].x;
	for(int i=0;i<=m;++i) cin>>b[i].x;
	len=1;
	while(len<=n+m) len<<=1;
	init(len);
	DFT(a,len,1),DFT(b,len,1);
	for(int i=0;i<len;++i) a[i]=a[i]*b[i];
	DFT(a,len,-1);
	for(int i=0;i<=n+m;++i) printf("%d ",(int)(a[i].x+0.5));
	return 0;
} 

注意非递归版最适合实战。

一些小细节:

  • 从代码中可以看到,向上合并的过程是倍增的。所以多项式的长度要拓展到2k并且大于相乘的两个多项式F(x),G(x)的长度之和。

  • 这样做是不会对答案有影响的,因为多出来的位上系数默认为0。最后输出时去掉多出来的位数即可。

  • DFT之后别忘了IDFT

  • 别混淆要处理的多项式长度len与要保留的多项式长度n

NTT

我们已知复数域中有性质良好的单位根ω来加速DFTIDFT,那么我们在模质数p的有限域下能否找到这样的数?

我们发现,当gp的一个原根时,ωni=giφ(p)n具有同复数域中的单位根一样的性质(两个重要的式子也成立)。

但是这里要求nφ(p),对于n有很大限制,故对于任意模数p,我们不能直接将它套入FFT中。

我们发现对于质数998244353φ(998244353)=998244352=119×223,因此当n=2k223时,程序都能跑,因此998244353成为知名常用模数(尽管有时题目与多项式无关)。

代码如下:

#include<bits/stdc++.h>

using namespace std;
typedef long long ll;

const int maxn=4e6+5,mo=998244353;
ll n,m,a[maxn],b[maxn],rev[maxn];

void init(int len){
	for(int i=1;i<=len;++i){
		rev[i]=rev[i>>1]>>1;
		if(i&1) rev[i]|=len>>1;
	}
}

ll ksm(ll a,ll b){
	ll res=1;
	a%=mo;
	while(b){
		if(b&1) res=res*a%mo;
		a=a*a%mo;
		b>>=1;
	}
	return res;
}

void NTT(ll f[],int len,int rv){
	for(int i=0;i<len;++i){
		if(i<rev[i]) swap(f[i],f[rev[i]]);
	}
	for(int h=2;h<=len;h<<=1){
		int wn=ksm(3,(mo-1)/h);
		for(int i=0;i<len;i+=h){
			ll w=1;
			for(int k=i;k<i+h/2;++k){
				int u=f[k],v=w*f[k+h/2]%mo;
				f[k]=(u+v)%mo;
				f[k+h/2]=(u-v+mo)%mo;
				w=w*wn%mo;
			}
		}
	}
	if(rv==-1){
		reverse(f+1,f+len);
		int inv=ksm(len,mo-2);
		for(int i=0;i<len;++i) f[i]=f[i]*inv%mo;
	}
}

int main(){
	scanf("%lld%lld",&n,&m);
	for(int i=0;i<=n;++i) scanf("%lld",&a[i]);
	for(int i=0;i<=m;++i) scanf("%lld",&b[i]);
	int len=1;
	while(len<=n+m) len<<=1;
	init(len);
	NTT(a,len,1),NTT(b,len,1);
	for(int i=0;i<len;++i) a[i]=a[i]*b[i]%mo;
	NTT(a,len,-1);
	for(int i=0;i<=n+m;++i) printf("%lld ",a[i]%mo);
	
	return 0;
}

快速多项式全家桶

前置

部分高等数学

一些高等数学。

快去看。

牛顿迭代法(NewtonsMethod

快速求零点

对于一条曲线,我们要求它的零点。

先猜测一个零点,再求出该点处的切线,然后取切线的零点重复以上过程,可以快速逼近零点。

快速求函数

我们已知G(F(x))=0,求F(x)

先猜测一个函数F0(x),套用求零点的过程,可以得到F1(x)=F0(x)G(F0(x))G(F0(x)),再不断重复提高精度。

正确性证明:

最开始手动求出[x0]F(x),记作F0(x)

假设现在已知Fk(x)F(x)(modxn)

我们把G(F(x))Fk(x)处泰勒展开得到:

G(F(x))=i0G(i)(Fk(x))i!(F(x)Fk(x))i=0

注意到F(x)Fk(x)的次数至少为n,所以:

i2(F(x)Fk(x))i0(modx2n)

所以i=01G(i)(Fk(x))i!(F(x)Fk(x))i0(modx2n)

即:

G(Fk(x))+G(Fk(x))(F(x)Fk(x))0(modx2n)

整理后得到:

F(x)Fk(x)G(Fk(x))G(Fk(x))(modx2n)

我们令Fk+1(x)Fk(x)G(Fk(x))G(Fk(x))(modx2n),就可以开始倍增求F(x)了。

正式开始

加法

H(x)=F(x)+G(x)

Sol:

直接加,O(n)

乘法

H(x)=F(x)G(x)

Sol:

直接上FFT或者NTTO(nlogn)

求导&积分

同暴力多项式全家桶部分,略。O(n)

求逆

F(x)G(x)1(modxn)

G(x)

Sol:

化式子:1G(x)F(x)=0

首先[x0]G(x)=1[x0]F(x)很好求。

然后带入牛顿迭代的式子得到:

Gk+1(x)Gk(x)1Gk(x)F(x)1Gk2(x)(modx2k+1)

化简一下得到:

Gk+1(x)2Gk(x)Gk2(x)F(x)(modx2k+1)

Gk+1(x)Gk(x)(2Gk(x)F(x))(modx2k+1)

倍增即可。O(nlogn)

取模

F(x)=Q(x)G(x)+R(x)

Q(x),R(x)

Sol:

n=degF,m=degG

那么degQ=nm,degR=m1

rF(x)=xnF(1x),即把F(x)的系数翻转后的函数。

同理有rG(x)=xmG(1x),rQ(x)=xnmQ(1x)

容易得到rF(x)rQ(x)rG(x)(modxnm+1)

证明上式:

F(x)=Q(x)G(x)+R(x)

F(1x)=Q(1x)G(1x)+R(1x)

于是xnF(1x)=xnmQ(1x)xmG(1x)+xnR(1x)

这也就是rF(x)=rQ(x)rG(x)+xnm+1R(x)

注意到xnm+1R(x)的最低次项的次数不低于nm+1,且nm+1>nm=degQ

rF(x)rQ(x)rG(x)(modxnm+1)

于是通过求逆就可以求出rQ(x),Q(x),更进一步将Q(x)代入原式便可算出R(x)

O(nlogn)

开根

G2(x)F(x)(modxn)

G(x)

Sol:

化式子:G2(x)F(x)0(modxn)

带入牛顿迭代的式子:

Gk+1Gk(x)Gk2(x)F(x)2Gk(x)(modx2k+1)

直接求就可以,或者再稍微化简:

Gk+1Gk(x)2+F(x)2Gk(x)(modx2k+1)

关于[x0]G(x)的讨论同暴力多项式全家桶部分,不再赘述。

O(nlogn)

ln

G(x)lnF(x)(modxn)

保证f0=1,求G(x)

Sol:

两边求导:G(x)=F(x)F1(x)

求出G(x),再积回去即可,注意g0=lnf0=0

exp

G(x)eF(x)(modxn)

保证f0=0,求G(x)

Sol:

两边取ln再化简得lnG(x)F(x)0(modxn)

代入牛顿迭代式子有:

Gk+1(x)Gk(x)lnG(x)F(x)G1(x)(modx2k+1)

化简得到:

Gk+1(x)Gk(x)(1lnGk(x)+F(x))(modx2k+1)

O(nlogn)

注意这里要多次求ln,记得清空。

三角函数

同暴力多项式全家桶部分套用公式,并使用多项式exp即可。不再赘述。

分治FFT

多项式多点求值

多项式快速插值

多项式平移

多项式连续点值平移

常系数齐次线性递推

ai=j=1kfjaij

已知{fi},k,n,a0k1,求an

n109,k32000

Sol:

显然矩乘加速过不了。

以斐波那契数列为例找找规律:

a5=a4+a3=2a3+a2=3a2+2a1=5a1+3a0

我们每次考虑将下标最大的一项换成下标更小的项,注意到这个形式与多项式取模类似。

更一般地,对于递推式ai=j=1kfjaij,可以构造多项式G(x)=i0fi+1xifk+1=1,fm=0 (m>k+1))和F(x)=xnF(x)modG(x)的各项系数即将an拆分为初始项后各项的系数。

再看上面的例子,F(x)=x5,G(x)=x2x1

所以F(x)modG(x)=5x+3

a5=5a1+3a0

更严谨的证明:

定义F(fixi)=fiai,那么答案就是F(xn)

由于ai=j=1kfjaij,所以F(xn)=F(i=1kfixni)

所以F(xni=1kfixni)=F(xnk(xki=0k1fkixi))=0

G(x)=xki=0k1fkixi

那么F(A(x)+xmG(x))=F(A(x))+F(xmG(x))=F(A(x))

所以A(x)可以通过多次加减xmG(x)的倍数来降次。

也就是求F(A(x)modG(x))A(x)modG(x)的次数小于k,而a0k1已经给出,就可以用多项式取模算了。

O(klogklogn)

posted @   RandomShuffle  阅读(102)  评论(2编辑  收藏  举报
相关博文:
阅读排行:
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效
点击右上角即可分享
微信分享提示