FFT入门——学习笔记
FFT入门
给一个非常好的入门视频:
复数与单位根
定义:
我们可以用复数在复平面上表示点
下文中,我们默认
我们以原点为起点,单位圆的
这里显然有:
引出欧拉公式:
那么由向量的运算法则(模长相乘,幅角相加),显然有
那么几个小性质:
关于性质4的证明,可以将
给张图
点值表示法
对于多项式
而带入
对于两个多项式
可见点值表示法求多项式的积是非常方便的,这引出了一个极为伟大的思想:求出
其中第二步可以
因为
FFT流程
我们现在讨论如何求出
因为
得到:
将
将
这个式子告诉我们,只需要我们处理左半区间的
可以写出代码:
//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
数组就给出了
至于我们为什么要搞一个tag
,会告诉你答案的。
点值化系数
直觉告诉我们,因为上文的solve
在对其进行系数化点值的时候,我们搞了个变量tag=1
,不难想到它的逆过程就是tag=-1
,就可以让点值化系数了!
但是,这个系数不是我们想要的。
设上述FFT把
展开:
将
因为
这就是FFT求逆。点值化系数。
这有什么用呢?我们在求出来
所以整个过程是这样的。盗用一张图:
代码实现:
迭代换递归
不难看出,递归的解法虽然简洁明了,但却有极大的空间和常数开销,我们考虑将其从递归转化为迭代:
观察这一张图。
我们将底层的数的下标写作二进制数:
原序列 | 000 | 001 | 010 | 011 | 100 | 101 | 110 | 111 |
---|---|---|---|---|---|---|---|---|
重排后 | 000 | 100 | 010 | 110 | 001 | 101 | 011 | 111 |
乍一看,我们好像发现:这个二进制数好像上下对应是反过来了。考虑证明这个性质,实际也不难,每一次划分就对一个二进制位进行了交换,自然会反过来。
将一个l
位的二进制数r[x]
为r[x]=(r[x>>1]>>1)|((x&1)<<(l-1))
,其中
所以我们可以预处理出这个倒置的数组r
,即可处理出合并的顺序,然后将长度为二的幂的区间自大到小合并即可,就省去了自顶向下带来的巨大常数和空间开销。
NTT
首先,类比FFT,我们利用了单位根的以下性质:
而NTT是解决在模意义下的多项式乘法。
为什么我们需要NTT?因为FFT它炸精了!
而NTT的重要思想就是在整数域,模意义下的同样具有以上性质的整数,这让我们发现了——原根!
前置知识
阶
定义:
若
例如
原根
设
注意,对于模数
对于
性质
对于原根,存在一个非常重要的定理:
设
用原根代替单位根
这里因为
设
证明:
根据定义显然
在模 下互不相同
根据定义显然
由于
,设 ,则 ,当 时, 。所以 ,这样我们就证明了后面一条定理
然后对于, ,根据费马小定理,可以得到 。然后因为 ,根据性质2可以得到 。所以带入即可得证。
当且仅当在 时为 ,否则为
同理当
时显然为
当时,根据等比数列求和公式可以得到 ,根据原根的定义和费马小定理: ,所以也可以得到分子为0。
综上,
在上面的FFT代码中,我们仅仅需要写个快速幂,再更改几行:
不过为什么我的NTT跑不过FFT啊,还慢得一批。
附上一张常用表:
//(
素数 | 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 |
int 范围内。 |
任意模数NTT
它大概是这样的,由于算法竞赛中常见的模数是998244353,1e9+7
这两个,于是对于多项式乘法1e9
级别的模数,比如上文所说的1e24
的,我们分别对于这些数跑NTT,这很方便,但需要9次NTT。然后我们便得到了如下这个方程组:
因为直接合并三个质数会爆long long
,于是我们需要科技。
根据CRT的方法,可以先合并前两个式子,这样long long
,得到:
其中
然后就有:
然后就给出了答案:
这里在做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代码:
细节蛮多的,我会尽量在代码中标注。
事实上:
简直慢的要死
MTT
同理解决任意模数的问题。
不妨将
然后我们来考虑加速,朴素的想法是:
那么不妨利用
因此
然后再有取
得到
分列方程,即可解出所需四个乘法。
也即
一共五次FFT。非常优秀。
注意复数除以实数:
分治FFT
我们一般的卷积式子是这样的:
这时候就是裸的FFT/NTT
但有时候会遇到如下毒瘤的式子:
我们发现,TA和正常的卷积并无什么差别,但问题是
这时候,正常的FFT/NTT不能满足我们的要求,于是考虑改变这个算法,食用分治法:
假设我们已经知道了
在每轮分治求这个卷积的值的时候,将辅助数组
这时候就又是一个多项式乘法的模版了,求出
多项式求逆
注意所有的多项式初等函数基本满足高位对低位无影响,所以可以直接先把
问题描述,给定一个多项式
在这里,我们设
那么由于仅有常数项为1,可以得到:
方法1—分治FFT
展开设
于是:
移项变式得到:
令
代码的话仅仅需要上文分治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+倍增
考虑倍增,设我们已经求解出来
故二者做差可得:
所以可以得到:
同时乘上
根据定义,得到:
用FFT处理一下两个乘法即可。然后根据摊还分析可得复杂度为:
这里我们的所有推导都是基于常数项对于
这里其实我们可以发现,分治FFT和多项式求逆是可以相互转化的,比如上文的分治FFT的例题可以这样解决:
题目:求
设数列
设:
则有:
而
故可以得到:
移项化简可得:
而
多项式求逆即可解决。
多项式
多项式导数积分及 公式
必须有
根据函数求导法则可得:多项式
取
积分公式是
我们对
也即
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);//积分
}
多项式平移
给定
所以仅需计算
后者是一个差卷积,直接计算即可。
利用该方法可以倍增计算第一类斯特林数行:
多项式
计算
先求导,得到:
而我们又知道,
因此有:
然后根据导数性质得到:
不妨设
就可以得到:
整理一下也就是
令
分治 FFT 即可。
改进的多项式
假定
利用牛顿迭代法可以得到以下结论:
记
则
玄学错误:需要多倍增一次。
#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]<<" ";
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!