FFT 学习笔记
学习笔记
建议先学习普通生成函数。
多项式
确定一个多项式,往往只需要知道每一次项前的系数是多少即可。众所周知,一个朴素的多项式往往可以被写成
的形式,在这种形式下的两个多项式
的方式计算。也就是说,得到的新多项式的每一次项前的系数可以写作
这种形式被叫做卷积,其特征是两个相乘的数所对应的下标相加是一个定值。不难发现,如此计算,对于两个次数为
考虑
复数
即形如
接着引入复平面,也就是以实数为
复数的加减只需要将实数部分和虚数部分相加减即可。对于乘法则有
单位根
对于一个
的这
对于 互不相等
对于单位根的计算,可以利用三角函数简单求解,即
离散傅里叶变换
即
对于一个
如果我们分别设
那么显然有
同理有
因此当我们求出
由此我们考虑对于多项式
但是这样写常数很大,我们考虑模拟交换系数的过程:
我们考虑这些数的二进制表示:
不难发现最初系数的位置和最终系数的位置满足下标进行了一次二进制下的翻转。
于是我们不难联想到与处理出位置是如何变化的,并存储在
代码
void DFT(int n){
Set(n-1);
for(int i=0;i<n;i++){
if(i<r[i])swap(a[i],a[r[i]]);//按照预处理交换系数
}
for(int len=1;len<n;len<<=1){
Complex step(cos(Pi/len),sin(Pi/len));//单位根
for(int l=0;l<n;l+=(len<<1)){
Complex w(1,0);
for(int k=0;k<len;k++,w=w*step){
Complex A=a[l+k],B=w*a[l+len+k];
a[l+k]=A+B;
a[l+len+k]=A-B;
}
}
}
return ;
}
逆离散傅里叶变换
即
考虑
记点值矩阵为
考虑构造
不难发现
代码
void DFT(int n,int type){
Set(n-1);
for(int i=0;i<n;i++){
if(i<r[i])swap(a[i],a[r[i]]);
}
for(int len=1;len<n;len<<=1){
Complex step(cos(Pi/len),type*sin(Pi/len));//是否需要取反
for(int l=0;l<n;l+=(len<<1)){
Complex w(1,0);
for(int k=0;k<len;k++,w=w*step){
Complex A=a[l+k],B=w*a[l+len+k];
a[l+k]=A+B;
a[l+len+k]=A-B;
}
}
}
if(type==-1){
for(int i=0;i<n;i++)a[i].a/=n;//除以 n 才是正确结果
}
return ;
}
下面可以给出一个带封装的总代码:
const int N=2097152;//注意需要取到最大次数向上的最大二次幂数
const double Pi=acos(-1.0);//处理 2pi
int n,m;
int r[N];//预处理数组
struct Complex{
double a,b;
Complex(double a=0,double b=0):a(a),b(b){}
Complex operator +(Complex x){return {a+x.a,b+x.b};}
Complex operator -(Complex x){return {a-x.a,b-x.b};}
Complex operator *(Complex x){return {a*x.a-b*x.b,a*x.b+b*x.a};}
};//手写复数
struct Polynomial{
int len;
vector<Complex>a;
Complex& operator [](int x){return a[x];}
void Set(int len){this->len=len,a.resize(len+1);}//节省空间的做法
void DFT(int n,int type){
Set(n-1);
for(int i=0;i<n;i++){
if(i<r[i])swap(a[i],a[r[i]]);
}
for(int len=1;len<n;len<<=1){
Complex step(cos(Pi/len),type*sin(Pi/len));
for(int l=0;l<n;l+=(len<<1)){
Complex w(1,0);
for(int k=0;k<len;k++,w=w*step){
Complex A=a[l+k],B=w*a[l+len+k];
a[l+k]=A+B;
a[l+len+k]=A-B;
}
}
}
if(type==-1){
for(int i=0;i<n;i++)a[i].a/=n;
}
return ;
}
Polynomial operator *(Polynomial x){
Polynomial y=*this,z;
int len=x.len+y.len;
int n=1;
while(n<=len)n<<=1;//向上取到二次幂
z.Set(n-1);
for(int i=0;i<n;i++)r[i]=(r[i>>1]>>1)|((i&1)*(n>>1));
x.DFT(n,1);
y.DFT(n,1);
for(int i=0;i<n;i++)z[i]=x[i]*y[i];
z.DFT(n,-1);
z.Set(len);
return z;
}
};
Polynomial f,g;
int main(){
ios::sync_with_stdio(0);
cin.tie(0);
cin>>n>>m;
f.Set(n);
g.Set(m);
for(int i=0;i<=n;i++)cin>>f[i].a;
for(int i=0;i<=m;i++)cin>>g[i].a;
f=f*g;
for(int i=0;i<=f.len;i++)cout<<(int)(f[i].a+0.5)<<" ";//精度问题
return 0;
}
快速数论变换
即
考虑到此时单位根不再使用,我们考虑模意义下能够代替单位根的东西,也就是上面讲到的原根。考虑原根如何能够满足代替单位根进行运算,考虑一些性质。
-
对于 互不相等。若模数
是一个质数,那么对于原根 , 对于 在模 意义下互不相等,那么 对于 在模 意义下互不相等。 -
。 -
。 -
。 ,所以 。 -
。 。
因此,不难看出,单位根即为
那么当题目给出的模数不是符合要求的
代码
#define ll long long
const int N=1e7+5,M=1e5+5,R=262144,P=1004535809,G=3,GI=334845270;
int n,m,t,s;
int w[M],r[R];
ll ans;
ll f[N],g[N];
namespace Math{
ll QuickPow(ll a,int b,const int p){
ll res=1;
while(b>0){
if((b&1)==1)res=res*a%p;
a=a*a%p;
b>>=1;
}
return res;
}
ll Inv(int x,int p){
return QuickPow(x,p-2,p);
}
};
struct Polynomial{
int len;
vector<ll>a;
ll& operator [](int x){return a[x];}
void Set(int len){this->len=len,a.resize(len+1);}
void NTT(int n,int type){
Set(n-1);
for(int i=0;i<n;i++){
if(i<r[i])swap(a[i],a[r[i]]);
}
for(int len=1;len<n;len<<=1){
ll step=Math::QuickPow(type==1?G:GI,(P-1)/(len<<1),P);
for(int l=0;l<n;l+=(len<<1)){
ll w=1;
for(int k=0;k<len;k++,w=w*step%P){
ll A=a[l+k],B=w*a[l+len+k]%P;
a[l+k]=(A+B)%P;
a[l+len+k]=(A-B)%P;
}
}
}
if(type==-1){
ll inv=Math::Inv(n,P);
for(int i=0;i<n;i++)a[i]=a[i]*inv%P;
}
return ;
}
Polynomial operator *(Polynomial x){
Polynomial y=*this,z;
int len=x.len+y.len;
int n=1;
while(n<=len)n<<=1;
z.Set(n-1);
for(int i=0;i<n;i++)r[i]=(r[i>>1]>>1)|((i&1)*(n>>1));
x.NTT(n,1);
y.NTT(n,1);
for(int i=0;i<n;i++)z[i]=x[i]*y[i]%P;
z.NTT(n,-1);
z.Set(len);
return z;
}
}A,B;
int main(){
ios::sync_with_stdio(0);
cin.tie(0);
cin>>n>>m;
A.Set(n);
B.Set(m);
for(int i=0;i<=n;i++)cin>>A[i];
for(int i=0;i<=m;i++)cin>>B[i];
A=A*B;
for(int i=0;i<=A.len;i++)cout<<A[i]<<" ";
return 0;
}
常见模型
事实上,所有的模型都离不开以下两种情况:
上面的就是朴素的卷积,而下面的称作差卷积,即两个数下标的差为定值。对于这种情况,我们通常选择讲一个序列倒转,即构造
于是发现和始终为
字符串匹配
虽然很不想承认,但是
不难发现,当
很显然,除了中间那一项,其他的项均可以通过预处理
化简可得
不难看出只需要进行
背包问题
类似于求解给出
高精度乘法
据某不知名消息途径,
考虑一个十进制数的一种不常规写法
于是不妨假设
这和多项式没有区别,于是两个数相乘就可以抽象成两个多项式相乘,因此可以利用
分治
针对
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】