FFT入门——学习笔记

FFT入门

给一个非常好的入门视频:

快速傅里叶变换

复数与单位根

定义:i2=1为虚数单位,我们称形如a+bi(a,bR)的数为复数。

我们可以用复数在复平面上表示点(0,0)>(a,b)的向量,我们称x的正轴与该向量的夹角为幅角a2+b2模长
下文中,我们默认n,m为2的幂
我们以原点为起点,单位圆的n等分点为终点,设幅角为正且最小的向量所对应的复数为wn0,表示n次单位根,设其余n1个单位根为wn1,wn2
这里显然有:wnn=wn0=1

引出欧拉公式

wnk=cos2kπn+isin2kπn

那么由向量的运算法则(模长相乘,幅角相加),显然有wnk=(wn1)k

image

那么几个小性质:

  1. wn0=wnn=1
  2. wdndk=wnk
  3. wnk=(wn1)k
  4. wnk+n2=wnk

关于性质4的证明,可以将k+n2看作旋转180度的结果,自然为复。

给张图

image
image

点值表示法

对于多项式A(x)=a0+a1x+a2x2++an1xn1,(a0,a1,a2)为其系数表示法。

而带入n个不同的x,得到的n个二元组(x0,y0),(x1,y1),(x2,y2)即为多项式的点值表示法。

对于两个多项式A(x),B(x),计算他们的乘积C(x)=A(x)B(x)的点值表示就只需要将A(x),B(x)对应的点值表示的y值相乘即可,也即(x0,y0,A×y0,B),但我们需要n+m个点。

可见点值表示法求多项式的积是非常方便的,这引出了一个极为伟大的思想:求出A,B的点值表示->求出A(x)×B(x)的点值表示->将点值表示化为系数表示

其中第二步可以O(n+m)做到。其中表示A,B分别为n,m阶多项式。

因为Cn+m阶多项式,所以可以将A,B不足的位补0。下面我们来尝试优化第一个和第三个步骤,这就是FFT所做的事情。

FFT流程

我们现在讨论如何求出A的点值表达。将单位根wnk带入,得到:

A(wnk)=k=0n1akwnkk
因为n2的幂。考虑奇偶分组:

A1(x)=a0+a2x2+a4x4,A2(x)=a1x+a3x3+a5x5

得到:A(x)=A1(x2)+xA2(x2)

wnk(k<n2)带入得到:

A(wnk)=A1(wn2k)+wnkA2(wn2k)

wnk+n2带入得到:

A(wnk+n2)=A1(wn2k+n)+wnk+n2A2(wn2k+n)=A1(wnnwn2k)wnkA2(wn2k)

k,k+n2正好取遍[0,n1]
这个式子告诉我们,只需要我们处理左半区间的A1,A2的点值表达式,就可以快速得到A的整个区间的点值表达式。故可以分治处理,复杂度O(nlogn)

可以写出代码:

//node 是复数类
db pi=acos(-1.0);
void solve(int limit,node a[],int tag){
    if(limit==1)return ;//单个常数没有用 
    node a1[(limit>>1)+5],a2[(limit>>1)+5];
    for(int i=0;i<=limit;i+=2){
    	a1[i>>1]=a[i];
    	a2[i>>1]=a[i+1];
	}//处理出系数 
	solve(limit>>1,a1,tag);
	solve(limit>>1,a2,tag);
	node wn={cos(2.0*pi/limit),tag*sin(2.0*pi/limit)},w={1.0,0};//pi是圆周率
	for(int i=0;i<(limit>>1);i++,w=w*wn){
		a[i]=a1[i]+w*a2[i];
		a[i+(limit>>1)]=a1[i]-w*a2[i];
	}
}
//tag=1

a数组就给出了x=wn0,wn1,wn2A的点值表示。
至于我们为什么要搞一个tag,会告诉你答案的。

点值化系数

直觉告诉我们,因为上文的solve在对其进行系数化点值的时候,我们搞了个变量tag=1,不难想到它的逆过程就是tag=-1,就可以让点值化系数了!

但是,这个系数不是我们想要的。

设上述FFT把A(x)的点值表示求出为(y0,y1,y2),逆过程求出来的系数是(c0,c1,c2),则ck=j=0n1yjwnjk

展开:

ck=j=0n1(i=0n1aiwnij)wnjk=i=0n1aij=0n1wnj(ik)

wnik看作整体,运用等比数列求和公式,得到:ck=i=0n1aiwn(ik)n1wnik1

因为wn(ik)n=(wnn)ik=1,故分子为0,分母显然不为0,但当i=k时,得到其为:nai显然可以给出:ck=nak,故可以给出:

ak=ckn

这就是FFT求逆。点值化系数。

这有什么用呢?我们在求出来A,B的点值表示并且相乘后,再求逆得出(c0,c1),再对每一个c除以n+m就可以得出C的系数了!

所以整个过程是这样的。盗用一张图:

image

代码实现:

code

迭代换递归

不难看出,递归的解法虽然简洁明了,但却有极大的空间和常数开销,我们考虑将其从递归转化为迭代:

image

观察这一张图。

我们将底层的数的下标写作二进制数:

原序列 000 001 010 011 100 101 110 111
重排后 000 100 010 110 001 101 011 111

乍一看,我们好像发现:这个二进制数好像上下对应是反过来了。考虑证明这个性质,实际也不难,每一次划分就对一个二进制位进行了交换,自然会反过来。

将一个l位的二进制数x倒置,可以这样做:设r[x]x倒置后的结果,则有r[x]=(r[x>>1]>>1)|((x&1)<<(l-1)),其中limit=2l

所以我们可以预处理出这个倒置的数组r,即可处理出合并的顺序,然后将长度为二的幂的区间自大到小合并即可,就省去了自顶向下带来的巨大常数和空间开销。

code

NTT

首先,类比FFT,我们利用了单位根的以下性质:

  1. wn0=wnn=1
  2. wdndk=wnk
  3. wnk=(wn1)k
  4. wnk+n2=wnk

而NTT是解决在模意义下的多项式乘法。

为什么我们需要NTT?因为FFT它炸精了!

而NTT的重要思想就是在整数域,模意义下的同样具有以上性质的整数,这让我们发现了——原根

前置知识

定义:
a,p互素,那么满足an1(modp)的最小正整数n即为ap的阶,记作δp(a)

例如δ7(2)=3

原根

pN+,aZ,若满足δp(a)=φ(p),则称a为模p的一个原根。

注意,对于模数p,如果它有原根,那么它的原根数量是φ(φ(p))

对于m来说,存在模m的原根的重要条件是:m=2,4,pa,2pa(pPrime,aN+)

性质

对于原根,存在一个非常重要的定理

p是素数,g是模p的一个原根,那么gimodp(1<g<p,0i<p1)互不相同。

用原根代替单位根

这里因为n是2的幂,所以我们对p有一定要求,p=a2x+1,常见的有:

998244353=119×223+1,1004535809=479×221+13是他们的原根之一。

gnn1(modp)gn1,gn1gnn1在模p下互不相同,则gngp1n(modp)等价于wn1

证明:

  1. gnn1(modp)

根据定义显然

  1. gn1,gn1gnn1在模p下互不相同

根据定义显然

  1. wnk+n2=wnk,wn2=wn21

由于gn1=gp1n,设m=p1n,则nm=p1,当n=n2时,m=2m。所以gn2=g2m=gn21,这样我们就证明了后面一条定理
然后对于gnk+n2=gnk·gnn2gnn2=gp12,根据费马小定理,可以得到gp12=11。然后因为g01(modp),根据性质2可以得到gp12=1。所以带入即可得证。

  1. j=0n1gnj(ik)当且仅当在ik=0时为n,否则为0

同理当ik=0时显然为n
ik时,根据等比数列求和公式可以得到gn(ik)n1gnik1,根据原根的定义和费马小定理:gnn=gp11(modp),所以也可以得到分子为0。

综上,gn1=gp1n为一个可替代w的选择。

在上面的FFT代码中,我们仅仅需要写个快速幂,再更改几行:

code

不过为什么我的NTT跑不过FFT啊,还慢得一批。
附上一张常用表:
//(gmod(r2k+1)的原根)

素数 r k g
3 1 1 2
5 1 2 2
17 1 4 3
97 3 5 5
193 3 6 5
257 1 8 3
7681 15 9 17
12289 3 12 11
40961 5 13 3
65537 1 16 3
786433 3 18 10
5767169 11 19 3
7340033 7 20 3
23068673 11 21 3
104857601 25 22 3
167772161 5 25 3
469762049 7 26 3
998244353 119 23 3
1004535809 479 21 3
2013265921 15 27 31
2281701377 17 27 3
3221225473 3 30 5
75161927681 35 31 3
77309411329 9 33 7
206158430209 3 36 22
2061584302081 15 37 7
2748779069441 5 39 3
6597069766657 3 41 5
39582418599937 9 42 5
79164837199873 9 43 5
263882790666241 15 44 7
1231453023109121 35 45 3
1337006139375617 19 46 3
3799912185593857 27 47 5
4222124650659841 15 48 19
7881299347898369 7 50 6
31525197391593473 7 52 3
180143985094819841 5 55 6
1945555039024054273 27 56 5
4179340454199820289 29 57 3
上面这一坨都不用管,只需要记下以下三个质数即可
998244353,1004535809,469762049,他们都存在原根3且都在int范围内。

任意模数NTT

它大概是这样的,由于算法竞赛中常见的模数是109次方级别的,最常用的是998244353,1e9+7这两个,于是对于多项式乘法A(x)B(x),设两个多项式分别是n,m(n<m)阶的,那么乘法所产生的最大值是109×109m,由于m不会大于106级别,所以答案的值不会超过1024。可以这样考虑,我们选择3个1e9级别的模数,比如上文所说的998244353,1004535809,469762049,这三者的乘积显然是大于1e24的,我们分别对于这些数跑NTT,这很方便,但需要9次NTT。然后我们便得到了如下这个方程组:

{xa1(modp1)xa2(modp2)xa3(modp3)

因为直接合并三个质数会爆long long,于是我们需要科技。

根据CRT的方法,可以先合并前两个式子,这样p1p2不会爆long long,得到:

{xA(modP)xa3(modp3)

其中P=p1p2,A=a1p2p21+a2p1p11,p11,p21分别是在模p2,p1时的逆元。

然后就有:x=kP+Aa3(modp3),立即可推得:k(a3A)×P1(modp3)

然后就给出了答案:x=kP+A(modp)

这里在做P1的时候long long会挂掉。而计算逆元用费马小定理,故我们需要龟速乘!奇淫技巧!
当然偷懒可以直接__int128。奇淫技巧类似于:

ll mul(ll a,ll b,ll P){
	a=(a%P+P)%P,b=(b%P+P)%P;
	return ((a*b-(ll)((long double)a/P*b+1e-6)*P)%P+P)%P;
}

建议背下。
于是稍加更改便可以写出三模数NTT代码:
细节蛮多的,我会尽量在代码中标注。

code

事实上:

简直慢的要死

MTT

同理解决任意模数的问题。

不妨将 A(x),B(x) 分别的系数拆掉,也即设 A1(x)×215+A2(x)=A(x),B(x) 同理。

然后我们来考虑加速,朴素的想法是:

A(x)×B(x)=A1(x)B1(x)×230+215×(A2(x)×B1(x)+A1(x)×B2(x))+A2(x)B2(x)

那么不妨利用 FFT 的特性,令 fx=a1x+i·a2x,gx=b1x+i·b2x

因此 f×g=A1(x)×B1(x)A2(x)×B2(x)+i·(A1(x)×B2(x)+A2(x)×B1(x))

然后再有取 f=a1x+i·(a2x),做 f×g

得到 A1(x)×B1(x)+A2(x)×B2(x)+i·(A1(x)×B2(x)A2(x)×B1(x))

分列方程,即可解出所需四个乘法。

也即 f+f 得到了 a1b1,a1b2ff 得到了 a2b2,a2b1

一共五次FFT。非常优秀。

注意复数除以实数: a+i·bc=ac+i·bc

code

分治FFT

我们一般的卷积式子是这样的:

ai=j=0ibjcij

这时候就是裸的FFT/NTT

但有时候会遇到如下毒瘤的式子:

ai=j=1iaijbi

我们发现,TA和正常的卷积并无什么差别,但问题是a出现在了式子的两边???

这时候,正常的FFT/NTT不能满足我们的要求,于是考虑改变这个算法,食用分治法:

假设我们已经知道了[l,mid]中的a值,那么左半区间[l,mid]对右半区间[mid+1,r]的贡献F可以计算为:

Fi=j=lmidajbij

在每轮分治求这个卷积的值的时候,将辅助数组A填充a[l,mid]部分,B填充b[1,rl]部分。
这时候就又是一个多项式乘法的模版了,求出F之后累加到a上即可。复杂度O(nlog2n)

code

多项式求逆

注意所有的多项式初等函数基本满足高位对低位无影响,所以可以直接先把 n 倍增到 2k1,方便计算。

问题描述,给定一个多项式F(x),求一个多项式G(x),满足 F(x)G(x)1(modxn)(对998244353取模),也即 F(x)G(x)只有常数项为1,其余项均为0。

在这里,我们设F(x)=f0+f1x+f2x2fn1xn1,G(x)=g0+g1x+g2x2+g3x3gn1xn1,其中f是已知的。

那么由于仅有常数项为1,可以得到:g0=1f0,于是我们可以求出g0

方法1—分治FFT

展开设A(x)=F(x)G(x),系数为a0,a1,得到:a0=1,ai=0(i[1,n1])

于是:

ai=k=0ifkgik=0

移项变式得到:

f0gi=k=1ifkgik可以给出gi=k=1igikfkf0

fk=fkf0(k>0),此题就变成了分治FFT,时间复杂度O(nlog2n)

代码的话仅仅需要上文分治FFT的主函数改为:

int main(){
//	freopen("data.in","r",stdin);
//	freopen("data.out","w",stdout);
	ios::sync_with_stdio(false);
	cin>>n;
	inv_g=power(g,p-2);
	for(int i=0;i<n;i++)cin>>b[i];
	b[0]%=p;
	a[0]=power(b[0],p-2);
	int INV=power(b[0],p-2);
	for(int i=1;i<n;i++)b[i]=-1ll*b[i]%p*INV%p,b[i]=(b[i]%p+p)%p; 
	cdq(0,n-1);
	for(int i=0;i<n;i++)cout<<a[i]<<" "; 
}

然后你去luogu上交了一发,发现:啊啊啊啊啊我怎么只有75啊啊啊啊啊

方法2—FFT+倍增

考虑倍增,设我们已经求解出来F(x)H(x)1(modxn2),并由此推出G(x)

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

故二者做差可得:

F(x)(G(x)H(x))0(modxn2)

所以可以得到:G(x)H(x)0(modxn2)G(x)2+H(x)22G(x)H(x)0(modxn)

同时乘上F(x)得到F(x)G(x)2+F(x)H(x)22F(x)G(x)H(x)0(modxn)

根据定义,得到:

G(x)+F(x)H(x)22H(x)0(modxn)G(x)2H(x)F(x)H(x)2(modxn)G(x)H(x)(2F(x)H(x))(modxn)

用FFT处理一下两个乘法即可。然后根据摊还分析可得复杂度为:T(n)=T(n2)+O(nlogn)=O(nlogn)

这里我们的所有推导都是基于常数项对于xn有逆元,故多项式可逆的充要条件是其常数项是否可逆。也即,其常数项不为零。若在模意义下,则需要常数项存在逆元

code

这里其实我们可以发现,分治FFT和多项式求逆是可以相互转化的,比如上文的分治FFT的例题可以这样解决:

题目:求ai=k=0i1akbik,a0=1

设数列a,b的生成函数分别是A(x)=a0+a1x+a2x2+anxn+,B(x)=b0+b1x+b2x2+bnxn+,设b0=0
设:A(x)B(x)=H(x)

则有:

hi=k=0iakbikhi=k=0i1akbik+aib0hi=ai(i1)

h0=a0b0=0

故可以得到:

A(x)B(x)=A(x)1

移项化简可得:

A(x)=11B(x)

1B(x)可以看作生成函数C(x)=x0·1+i=1i0·xi减去B(x),故bi=bi(i1),b0=1

多项式求逆即可解决。

多项式 ln

多项式导数积分及 ln 公式

必须有 f0=1

根据函数求导法则可得:多项式 F(x)=ixifi 的求导法则为:

F(x)=ifixi1,当然这必须要求 f0=0

ln 可以看作先求导再乱搞然后积分。

积分公式是 F(x)=xifi1i

我们对 lnF(x) 先求导再积分。

也即 (lnF(x))=1F(x)lnF(x)

	vector<int> derivation(vector<int>&f){
		vector<int>g(f.size()+1);
		for(int k=1;k<f.size();k++)g[k-1]=f[k]*k%p;
		return g;
	}
	vector<int> quadrature(vector<int>&f){
		vector<int>g(f.size()+1);
		for(int k=1;k<f.size();k++)g[k]=f[k-1]*inv[k]%p;g[0]=0;
		return g;
	}
	vector<int> getln(vector<int>f,int n){
		vector<int>f0=derivation(f);//求导
		f=getinv(f);//求逆
		for(int i=n;i<f.size();i++)f[i]=0;
		for(int i=n;i<f0.size();i++)f0[i]=0;
		int limit=1,d=0;
		while(limit<n+n)limit<<=1,++d;
		init(d);//不要忘了 
		ntt(f0,limit,1);ntt(f,limit,1);
		for(int i=0;i<limit;i++)f[i]=f[i]*f0[i]%p;
		ntt(f,limit,0);int inv=power(limit,p-2);
		for(int i=0;i<limit;i++)f[i]=f[i]*inv%p;
		return quadrature(f);//积分
	}

多项式平移

给定 F(x)=fixi,求 F(x+n)

F(x+n)=fi(x+n)i=fi(ij)xjnij=ixi(ki)nkifk

所以仅需计算 xik!fk×nki(ki)!

后者是一个差卷积,直接计算即可。

利用该方法可以倍增计算第一类斯特林数行:x(x+1)(x+n1)=xiS1(n,i)

多项式 exp

计算 expF(x),必须有 f0=0

先求导,得到:

(expF(x))=F(x)expF(x)(modxn)

而我们又知道,fi=(i+1)fi+1

因此有:

[xi](expF(x))=([xj]F(x))×([xij]expF(x))

然后根据导数性质得到:

[xi+1](i+1)(expF(x))=(j+1)([xj+1]F(x))×([xijexpF(x)])

不妨设 expF(x)=hixi

就可以得到:

hi+1(i+1)=(j+1)fj+1hijhi×i=j×fjhij

整理一下也就是 n·hn=j·fj·hnj

wj=j·fj,这就是个卷积了。

分治 FFT 即可。

改进的多项式 exp

假定 n=2k

利用牛顿迭代法可以得到以下结论:

F0(x)=expG(x)(modx2k)

F(x)=expG(x)(modx2k+1)F0(x)·(1+G(x)lnF0(x))

玄学错误:需要多倍增一次。

#include<bits/stdc++.h>
using namespace std;
#define N 1050500
#define int long long 
const int p=998244353,g=3,invg=(p+2)/3;
int n,m,r[N],invs[N];//数逆元
void init(int d){
    for(int i=0;i<(1<<d);i++)r[i]=(r[i>>1]>>1|((i&1)<<d-1));
}
int power(int a,int b){
    int res=1;
    while(b){
        if(b&1)res=res*a%p;
        a=a*a%p;b>>=1;
    }
    return res;
}
namespace NTT{
    void ntt(int f[],int d,int tag){
        for(int i=0;i<(1<<d);i++)if(i<r[i])swap(f[i],f[r[i]]);
        for(int len=1;len<(1<<d);len<<=1){
            int gn=power((tag?g:invg),(p-1)/(len<<1));
            for(int j=0;j<(1<<d);j+=(len<<1)){
                int g0=1;
                for(int k=0;k<len;k++,g0=g0*gn%p){
                    int x=f[j+k],y=f[j+k+len]*g0%p;
                    f[j+k]=(x+y)%p;f[j+k+len]=(x-y+p)%p;
                }
            }
        }
        if(!tag){
            int inv=power(1<<d,p-2);
            for(int i=0;i<(1<<d);i++)f[i]=f[i]*inv%p;
        }
    }
}
void Wf(int a[],int b[],int d){
    for(int i=0;i<(1<<d);i++)a[i]=b[i+1]*(i+1)%p;
}
void Jf(int a[],int b[],int d){
    for(int i=1;i<(1<<d);i++)a[i]=b[i-1]*invs[i]%p;a[0]=0;
}
namespace Inv{
    int A[N],B[N],C[N],S[N];
    void Inv(int a[],int b[],int d){
        S[0]=power(b[0],p-2);
        S[1]=0;
        for(int len=2,c=1;len<(1<<d);len<<=1,++c){
            int limit=len<<1;
            for(int i=0;i<len;i++)A[i]=b[i],B[i]=S[i];
            for(int i=len;i<limit;i++)A[i]=B[i]=0;
            init(c+1);
            NTT::ntt(B,c+1,1);NTT::ntt(A,c+1,1);
            for(int i=0;i<limit;i++)S[i]=2ll*B[i]%p+p-B[i]*B[i]%p*A[i]%p,S[i]%=p;
            NTT::ntt(S,c+1,0);
            for(int i=len;i<limit;i++)S[i]=0;
        }
        for(int i=0;i<(1<<d);i++)a[i]=S[i];
    }   
}
namespace Ln{
    int A[N];
    void Ln(int a[],int b[],int d){
        //LN(F(X))=F'(X)/F(X)
        for(int i=0;i<(1<<d);i++)A[i]=b[i];
        Wf(a,b,d);
        Inv::Inv(A,b,d);
        init(d);
        NTT::ntt(A,d,1);
        NTT::ntt(a,d,1);
        for(int i=0;i<(1<<d);i++)A[i]=a[i]*A[i]%p;
        NTT::ntt(A,d,0);
        // for(int i=(1<<d);i<(1<<d+1);i++)a[i]=0;
        Jf(a,A,d);
    }
}
int G[N],F[N];
namespace Exp{
    int A[N],B[N],C[N],S[N];
    void Exp(int g[],int f[],int d){//G(x)=exp(F(x)) mod x^d
    //G(x)=G0(X)*(1+f(x)-ln(G0(X)))
        for(int i=0;i<(1<<d);i++)S[i]=0;
        S[0]=1,S[1]=0;
        for(int len=2,c=1;len<(1<<d);len<<=1,++c){
            int limit=len<<1;
            for(int i=0;i<len;i++)A[i]=f[i],B[i]=S[i];
            for(int i=len;i<limit;i++)A[i]=B[i]=C[i]=0;
            Ln::Ln(C,B,c);
            for(int i=0;i<len;i++)C[i]=(p-C[i]+A[i])%p;
            C[0]+=1;
            init(c+1);
            NTT::ntt(B,c+1,1);
            NTT::ntt(C,c+1,1);
            for(int i=0;i<limit;i++)S[i]=B[i]*C[i]%p;
            NTT::ntt(S,c+1,0);
            for(int i=len;i<limit;i++)S[i]=0;
            // cout<<len<<"\n";
            // for(int i=0;i<len;i++)cout<<S[i]<<" ";cout<<"\n";
        }
        for(int i=0;i<(1<<d);i++)g[i]=S[i];
    }
}
signed main(){
    ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
    cin>>n;
    for(int i=0;i<n;i++)cin>>F[i];
    for(int i=0;i<(n<<2);i++)invs[i]=power(i,p-2);
    int l=0;
    while((1<<l)<=(n<<1))++l;
    Exp::Exp(G,F,l);
    for(int i=0;i<n;i++)cout<<G[i]<<" ";
}

posted @   spdarkle  阅读(326)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
点击右上角即可分享
微信分享提示