FFT算法学习笔记
写在前边
1.辣鸡RRRR_wys之前csdn的博客,千年不更。。。还很水。。。于是开了这个Blog。。。妄图拯救一下自己
2.最近接触了一些多项式理论。于是翘掉了愉快的高频自控,通过《算导》稍稍学习了一下
3.算法竞赛中,FFT主要解决多项式的乘法等问题
FFT基本概念
1.FFT即快速傅里叶变换, 是离散傅里叶变换的加速算法。可以在o(nlogn)的时间内,完成DFT和DFT-1
2.DFT即离散傅里叶变换, 这里主要就是多项式的系数向量转换成点值表示的过程
多项式的表示
1.系数表达:对于一个次数上界为n的多项式 A(x)=∑(j=0 - n-1) aj*xj 而言,系数表达式便是由系数组成的向量a = (a0, a1, a2, a3, ... , an-1)
(1)计算A(x0)的值即:A(x0) = a0+x0(a1+x0(a2+x0(a3+x0(a4+...+x0(an-2+x0*an-1))...))) 时间为o(n)
(2)通过系数向量计算两个多项式的乘法:设输出向量为c, 而a,b长度n=max(na,nb)那么显然两个c的长度为2n-1,计算方法为:cj = ∑(k=0 ~ j) ak*bj-k,
即向量a,b的卷积
2.点值表达式:对于一个次数上界为n的多项式 A(x)=∑(j=0 ~ n-1) aj*xj 而言,点值表达式便是由n个点值所构成的集合{ (x0,y0), (x1,y1), (x2,y2), ... , (xn-1,yn-1) }
(1)运用之前提到的方法,一个个计算点值时间为o(n2),之后可以看到如果我们巧妙地选取点xk可以使时间变为o(nlogn)
(2)将点值表达式转换为系数表达式:将已知的点值带入,多项式可已得到一个有n个未知数的n个线性方程
a0 + a1*x0 + a2*x02 + a3* x03 + ... + an-1*x0n-1 = y0 (1)
a0 + a1*x1 + a2*x12 + a3* x13 + ... + an-1*x1n-1 = y1 (2)
.......
a0 + a1*xn-1 + a2*xn-12 + a3* xn-13 + ... + an-1*xn-1n-1 = yn-1 (n)
可以写成矩阵形式,既为一个范德蒙矩阵,通过线代知识解这个方程就可以求出系数向量,复杂度o(n3)。另一种方法是采用拉格朗日插值法求解,复杂度o(n2)。因此,n个点的求值运算和插值运算,是定义完备的互逆运算。
(3)通过点值计算两个多项式的乘积,c = { (x0,yA0*yB0), (x1,yA1*yB1), (x2,yA2*yB2), ... , (xn-1,yA(n-1)*yB(n-1)) }, 可以看到复杂度为o(n)
(4)计算多项式乘法的过程:1)o(nlogn)分别求出两个多项式的点值表达式 2)点值乘法o(n) 3)插值o(nlogn) 4)求出答案多项式的系数表达式
DFT与FFT
下面开始讨论,如何在o(nlogn)的时间内,完成求值和插值运算
1.单位复数根
n次单位复数根满足ωn=1的复数ω,n此单位复数根的数目恰好有n个,这些根是:ωnk = e2πik/n 这n个根均匀的分布在复平面上,其他n次单位根都是ωn的幂次。n个n次单位根在乘法意义下形成一个群,该群与模n意义下的加法群,有相同的结构,即ωni ωnj = ωn(i + j) mod n
(1) 消去引理:ωndkd =ωnk (n>=0, k>=0, d>0)
(2) ωnn/2 =ω2 =-1
(3) 折半引理:如果n>0为偶数,那么n个n次单位复数根的平方的集合就是n/2个n/2次单位复数根的集合
(4) 求和引理:∑(j=0 ~ n-1) (ωnk)j = 0 (n>=1, 不能被n整除的非负整数k)
2.DFT
我们希望计算ωn0, ωn1, ωn2, .... , ωnn-1处的值(在多项式乘法中其实是2n个点值)。假设给定系数a=(a0, a1, a2, ... , an-1), 即对于k=0, 1, 2, 3, ... , n-1求出yk = A(ωnk),记向量y=(y1, y2, y3, ... ,yn-1)就是系数向量a的离散傅里叶变换DFT,记为 y = DFTn(a)
3. FFT
通过快速傅里叶变换FFT,利用单位复数根的性质,我们可以在o(nlogn)的时间内计算DFTn(a)。首先假设次数n恰好是2的整数幂,不足在高次位置项添0。
定义两个新的次数界为n/2的多项式 A[0] = a0 + a2*x + a4*x2 + a6* x3 + ... + an-2*xn/2-1 , A[1] = a1 + a3*x + a5*x2 + a7* x3 + ... + an-1*xn/2-1
A[0]包含所有偶数下标的系数,A[1]包含所有奇数下标的系数,于是有A(x) = A[0](x2) + x*A[1](x2) 。
所以,求解A(x)的DFN,转换为了求解多项式A[0], A[1]在(ωn0)2, (ωn1)2, (ωn2)2 , ... , (ωnn-1)2 处的取值。
根据折半引理,(ωn0)2, (ωn1)2, (ωn2)2 , ... , (ωnn-1)2 并不是由n个不同的值组成,而是由n/2个n/2次单位根复数根所组成,每个根正好出现两次。因此,我们递归的对次数界为n/2的多项式A[0], A[1]在n/2个n/2次单位复数根处进行求值。这样我们求出了次数界为n的次多项式在n次单位复数根处的值。
注意到除了递归调用的时间外,每次所需时间为o(n),其中n是输入向量的长度。因此有如下递归式:T(n) = 2T(n/2) + o(n) = o(nlogn),时间为o(nlogn)。
计算在单位复数根处插值,求解系数向量。根据点值表达式,我们可以列出线性方程组:
a0 + a1 + a2 + a3+ ... + an-1 = y0 (1)
a0 + a1* (ωn)1+ a2*(ωn)2 + a3* (ωn)3 + ... + an-1*(ωn)n-1 = y1 (2)
a0 + a1* (ωn)2+ a2*(ωn)4 + a3* (ωn)6 + ... + an-1*(ωn)2(n-1) = y2 (3)
......
a0 + a1* (ωn)n-1+ a2*(ωn)2(n-1) + a3* (ωn)3(n-1) + ... + an-1*(ωn)(n-1)(n-1) = yn-1 (4)
根据这个线性方程组,我们可以令单位复数根组成的系数矩阵为Vn,运用yVn-1求解系数。可以证明:Vn-1的(j, k)处的值为(ωn)-kj/n (j,k = 0, 1, 2, ... , n-1)
有了上面的式子我们能推导出DFT-1:aj =(1/n)* ∑(k=0 ~ n-1) yk(ωn)-kj (j = 0, 1, 2, ... , n-1)
观察可得,相对于DFT,我们把a与y替换,用ωn-1代替ωn,并将每个元素除以n。这样,我们也可以在o(nlogn)的时间内计算出DFT-1
卷积定理:a(卷积)b = DFT2n-1( DFT2n(a)*DFT2n(b) ) , 其中向量a, b用0填充使其长度达到2n。
[UR#34]多项式乘法
递归实现:参考hzwer的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | #include <bits/stdc++.h> #define pi acos(-1.0) typedef long long ll; using namespace std; int n,m; complex< double > a[300000],b[300000]; void fft(complex< double > *x, int n, int ty){ if (n==1) return ; complex< double > l[n>>1],r[n>>1]; for ( int i=0;i<=n;i+=2) l[i>>1]=x[i],r[i>>1]=x[i+1]; fft(l,n>>1,ty);fft(r,n>>1,ty); complex< double > wn( cos (2*pi/n), sin (ty*2*pi/n)),w(1,0),t; for ( int i=0;i<n>>1;i++,w*=wn) t=w*r[i],x[i]=l[i]+t,x[i+(n>>1)]=l[i]-t; } int main() { scanf ( "%d%d" ,&n,&m); for ( int i=0,x;i<=n;++i) scanf ( "%d" ,&x),a[i]=x; for ( int i=0,x;i<=m;++i) scanf ( "%d" ,&x),b[i]=x; m=n+m; for (n=1;n<=m;n<<=1); fft(a,n,1);fft(b,n,1); for ( int i=0;i<=n;++i)a[i]=a[i]*b[i]; fft(a,n,-1); for ( int i=0;i<=m;++i) printf ( "%d " ,( int )(a[i].real()/n+0.5)); return 0; } |
然而,实际应用中,需要算法尽量地快速,因此,我们引出一种小常数FFT的迭代实现。
高效FFT实现
1.蝴蝶操作:
1 2 | for ( int i=0;i<n>>1;i++,w*=wn) t=w*r[i],x[i]=l[i]+t,x[i+(n>>1)]=l[i]-t; |
2.考虑递归调用的过程,将每次输入向量构成一颗树,类似于zkw线段树,如果可以实现自底向上更新,相比递归常数就很优秀了。
(1)首先,我们成对的取出输入向量树最底层的元素,利用蝴蝶操作计算出每对的DFT,这样我们就有了n/2个二元素DFT,同理,下一步取出这n/2个DFT,计算四元素DFT,最终形成n元素DFT
(2)那么如何确定最底层的元素排列呢?尝试一下就能发现,元素出现的顺序就是是一个位逆序置换,即:n = 8时,{0, 4, 2, 6, 1, 5, 3, 7}, 写成二进制形式,即{000, 100, 010, 110, 001, 101, 011, 111},将所有元素二进制位逆序可得{000, 001, 010, 011, 100, 101, 110, 111} = {0, 1, 2 ,3, 4, 5, 6, 7}
[UR#34]多项式乘法
迭代实现:参考hzwer的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | #include <bits/stdc++.h> #define pi acos(-1.0) typedef complex< double > E; using namespace std; int n,m,L,R[300000]; E a[300000],b[300000]; void fft(E *a, int f){ for ( int i=0;i<n;++i) if (i<R[i])swap(a[i],a[R[i]]); for ( int i=1;i<n;i<<=1){ E wn( cos (pi/i),f* sin (pi/i)); for ( int p=(i<<1),j=0;j<n;j+=p){ E w(1,0); for ( int k=0;k<i;++k,w*=wn){ E x=a[j+k],y=w*a[j+k+i]; a[j+k]=x+y;a[j+k+i]=x-y; } } } } int main() { scanf ( "%d%d" ,&n,&m); for ( int i=0,x;i<=n;++i) scanf ( "%d" ,&x),a[i]=x; for ( int i=0,x;i<=m;++i) scanf ( "%d" ,&x),b[i]=x; m=n+m; for (n=1;n<=m;n<<=1)L++; for ( int i=0;i<n;++i)R[i]=(R[i>>1]>>1)|((i&1)<<(L-1)); fft(a,1);fft(b,1); for ( int i=0;i<=n;++i)a[i]=a[i]*b[i]; fft(a,-1); for ( int i=0;i<=m;++i) printf ( "%d " ,( int )(a[i].real()/n+0.5)); return 0; } |
NTT(Number Theory Transform)
可是,复数运算的常数依然是很巨大的,而且精度不高。当题目给的模数很特殊的时候,我们就可以利用数论变换来加速算法。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 【自荐】一款简洁、开源的在线白板工具 Drawnix