【学习笔记】 - 基础数论:快速傅里叶变换

FFT从入门到回头是岸

快速傅里叶变换,即 FFT(Fast Fourier Transform),用于O(nlogn)计算两个多项式之间的卷积

听起来似乎很难,但其实没有那么难理解

在学习快速傅里叶变换之前,我们需要一些简单的前置知识

前置知识:

  • 复数

    复数可以表示为 a+bi ,其中 a 称作实部 bi 称作虚部

    其中i=1

    比起向量更优秀的是,复数可以进行加减乘除,还可以带入多项式

    复数相乘的规则是 模长相乘,幅角相加

    模长就是这个向量的模长(也可以理解为这个点到原点的距离)

    幅角就是 x 轴正方向逆时针旋转到与这个向量共线所途径的角(也可以理解为从原点出发指向 x 轴正方向的射线逆时针旋转到这个点所经过的角)

    • 复数的实现

      • STL自带

        C++ 的 STL 为我们提供了复数

        • 使用方法

          定义complex<double> x;

          这样直接对其进行加减乘除即可

      • 模拟

        因为 STL 中的复数常数较大,而一般FFT的题都卡常

        所以选择手动模拟

        struct Complex{
            double x=0;
            double y=0;
            Complex operator+(const Complex&t)const{
                return {x+t.x , y+t.y};
            } 
            Complex operator-(const Complex&t)const{
                return {x-t.x,y-t.y};
            }
            Complex operator*(const Complex&t)const{
                return {x*t.x-y*t.y,x*t.y+y*t.x};
            }
        }a[N],b[N];
        
    • n次单位根

      快速傅里叶变换需要用到的是一些特殊的复数

      在复平面建立一个单位圆(半径为1),将其进行n等分

      n 个点的横坐标为实部、纵坐标为虚部可以构成 n 个复数

      从点(1,0)开始,逆时针将这n个点从0开始编号,第k个点对应的虚数记作ωnk

      我们称 ωn1n 次单位根

      • 单位根的性质

        • . ωniωnj

        • . ωnk=cos2kπn+isin2kπn

        • . ωn0=ωnn=1

        • . ω2n2k=ωnk

        • . ωnk+n2=ωnk

  • 什么叫多项式乘积?

    就是卷积

    设两个多项式 A(x)=a0+a1x+a2x2B(x)=b0+b1x+b2x2

    其卷积就是两个多项式相乘(A(x)B(x))

    我们发现直接做再合并同类项就是n2的时间复杂度

    快速傅里叶变换可以在O(nlogn)的复杂度求出两个多项式卷积的值

  • 多项式的性质

    一个n次多项式,我们可以用n+1个不同点唯一确定这个多项式

    这意味着我们可以用n+1个点来表示多项式,这被称作点表示法

    • 点表示法有什么好处

      如果我们要求A(x)B(x)的乘积C(x)

      我们可以用点表示法去求乘积

      如果我们要求C(x1),我们发现C(x1)=A(x1)B(x1)

      那么求出来C所用的复杂度为O(n)

      这样乘法不再是多项式乘法的瓶颈

正文

虽然乘法不再是多项式乘法的瓶颈,但是我们可以发现把多项式转换成点值表示的朴素算法是O(n2)的,另外直接暴力插值转回多项式也是O(n2)

那么我们只要解决这个问题就可以了

  • FFT

    • 系数表示法转化为点表示法

      A(x)=a0+a1x+a2x2+...+an1xn1

      我们需要让奇偶次数分开

      (1)A(x)=a0+a1x++an1xn1(2)=(a0+a2x2++an2xn2)+(a1x+a3x3++an1xn1)

      我们让A1(x)=(a0+a2x+a4x2++an2xn21)

      A2(x)=(a1+a3x+a5x2++an1xn21)

      那么

      A(x)=A1(x2)+xA2(x2)

      ωnk带入,大力分讨

      • k[0,n21)

        带入得到

        (3)A(ωnk)=A1(ωn2k)+ωnkA2(ωn2k)(4)=A1(ωn2k)+ωnkA2(ωn2k)

      • k[n2,n1)

        带入得到

        (5)A(ωnk+n2)=A1(ωn2k)+ωnk+n2A2(ωn2k)(6)=A1(ωn2k)ωnkA2(ωn2k)

      那么求出A(wnk)k(0,n]区间的所有值就可以处理了

      因为A(wnk)=A1(wn2k)A2(wn2k)

      只需要递归求n2的两个区间即可

      最多会递归logn层,每层都是O(n)

      所以复杂度是O(nlogn)

    • 点表示法求出系数表达式

      FFT的逆变换怎么搞?

      已有n个点(ωnk,A(ωnk))

      我们为了方便将其称之为(xk,yk)

      Ck=i=1n1yi(ωnk)i

      我们可以把这个当做一个关于y的多项式,B(x)=y0+y1x++yn1xn1

      Ck=B(ωnk)

      我们可以再做一次傅里叶变换来求解系数表示式

      但是Ck并不是原多项式的系数,需要再除以一个n

      (7)Ck=i=1n1yi(ωnk)i=i=1n1(j=0n1ajωnij)ωnki(8)=i=0n1(j=0n1aj(ωnjk))i(9)=j=0n1aj(i=0n1(ωnjk)i)

      我们把i=0n1(ωnjk)i可以看做一个多项式

      S(x)=1+x+x2+xn1

      这么这里就相当于S(ωnk)

      分两种情况来看

      • . k0

        S(ωnk)ωnkS(ωnk)

        发现这两个式相等

        所以(1ωnk)S(ωnk)=0

        因为ωnk1

        所以S(ωnk)=0

      • . k=0

        S(ωnk)=S(1)=n

      因此

      j=0n1aj(i=0n1(ωnjk)i)=nak

  • 实现

    因为递归实现的 FFT 常数巨大,所以我们要用迭代实现

    我们对于要计算的去分为奇数项和偶数项

    分成后的多项式发现可以再分

    分到最后,我们发现,每一位对应的系数的二进制表示是这个位置原本系数对应二进制翻转过来

    比如a1(001)a4(100)

    还有a1(0001)a7(1000)

  • 代码

    本代码为迭代版FFT,已使用蝴蝶操作,手打复数,常数因子可能较小?

    const double PI=acos(-1);
    struct Complex{
    	double x,y;
    	Complex operator+(const Complex&t)const{
    		return {x+t.x , y+t.y};
    	} 
    	Complex operator-(const Complex&t)const{
    		return {x-t.x,y-t.y};
    	}
    	Complex operator*(const Complex&t)const{
    		return {x*t.x-y*t.y,x*t.y+y*t.x};
    	}
    }a[N],b[N];
    int rev[N],bit,tot;
    int n,m;
    void fft(Complex a[],int inv){
    	for(int i=0;i<tot;i++)
    		if(i<rev[i])
    			swap(a[i],a[rev[i]]);
    	for(int mid=1;mid<tot;mid<<=1){
    		auto w1=Complex({cos(PI/mid),inv*sin(PI/mid)});
    		for(int i=0;i<tot;i+=mid*2){
    			auto wk=Complex({1,0});
    			for(int j=0;j<mid;j++,wk=wk*w1){
    				auto x=a[i+j],y=wk*a[i+j+mid];
    				a[i+j]=x+y,
                    a[i+j+mid]=x-y;
    			}
    		}
    	}
    }
    signed main(){
    	// fire();
    	cin(n,m);
    	for(int i=0;i<=n;i++) cin(a[i].x);
    	for(int i=0;i<=m;i++) cin(b[i].x);
    	while((1<<bit)<n+m+1) bit++;
    	tot=1<<bit;
    	for(int i=0;i<tot;i++)
    		rev[i]=(rev[i>>1]>>1)|((i&1)<<(bit-1));
    	fft(a,1),fft(b,1);
    	for(int i=0;i<tot;i++)
    		a[i]=a[i]*b[i];
    	fft(a,-1);
    	for(int i=0;i<=n+m;i++)
    		cout((int)(a[i].x/tot+0.5)," ");
    }
    

    解释一下最后的 (int)(a[i].x/tot+0.5)

    在最后取出的复数是包含虚部的,我们只看实部即可

    后面的 +0.5 用于四舍五入

posted @   Vsinger_洛天依  阅读(151)  评论(4编辑  收藏  举报
相关博文:
阅读排行:
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本

阅读目录(Content)

此页目录为空

点击右上角即可分享
微信分享提示