• 快速傅里叶变换(FFT)


    写在前面的话:

    感觉最近每次考试都是同样的一种情形:考试的时候绞尽脑汁地去思考正解无果,在Solution发下来以后,却发现这是一种我没有学习过的算法......并且感觉这些东西貌似就我一个人不会了,赶紧来学习了一下,首先,便是FFT

    (一)预备知识

    要学习FFT,首先得学习一些关于复数的东西
    这一篇写得不错,理解起来很容易,具体看here

    (二)开始学习

    附上我在学习FFT的时候用的3篇博客,很多内容都是一样的,但是中间有些知识可以互补
    首先学习博客1,这篇博客很多东西的证明写得很清楚,有利于初学者
    然后再看一看博客2,这篇博客中有两副图有助于理解,并且也详细解释了使用矩阵来计算IDFT的过程
    全部理解了以后,自然是要写代码的呀,看一看博客3,这篇代码写得还是蛮详细的。

    (三)重要知识

    为了方便我以后复习,先将3篇博客的重要部分浓缩至此,略去所有证明以及基础概念性的东西,为什么?因为我懒得打字啊~~~

    (1)多项式乘法

    定义$ A(x)=sum_{i=0}^{n-1} a_{i}x^{i} $ 与 (B(x)=sum_{i=0}^{n-1} b_{i}x^{i}) 相乘的结果为(C(x)),那么(C(x))的值为

    $$C(x)=sum_{k=0}^{2n-2}(sum_{k=i+j} a_{i}b_{j})x^{k}$$
    

    这样做的时间复杂度无疑是(O(n^{2})),显然不行
    但是如果我们知道了两个多项式在2n-1个点处所取得的点值表示,即将那2n-1个点作为自变量x带入A与B之后所得的结果,那么我们就可以在(O(n))时间之内计算出(C(x))
    那么我们该如何做到这一点呢?这就是为什么要用FFT的原因了。

    (2)单位根

    在复平面上,以原点为圆心,1为半径作圆,所得的圆叫做单位圆。以原点为起点,单位圆的n等分点为终点,作n个向量。设所得的幅角为正且最小的向量对应的复数为(omega_{n}),称为n次单位根。

    在代数中,若(z^{n}=1),我们把z称为n次单位根

    性质一 :(omega_{2n}^{2k}=omega_{n}^{k})

    性质二 :(omega_{n}^{k+ frac{n}{2}}=-omega_{n}^{k})

    (3)快速傅里叶变换

    考虑多项式(A(x))的表示。将n次单位根的0到n-1次幂带入多项式的系数表示,所得点值向量((A(omega_{n}^{0}),...,A(omega_{n}^{n-1})))称为其系数向量的离散傅里叶变换。

    Cooley-Tukey 算法是FFT的最常用算法,具体见3个博客

    (4)傅里叶逆变换

    将点值表示的多项式转化为系数表示,同样可以使用快速傅里叶变换,这个过程叫做傅里叶逆变换。

    ((y_{0},...,y{n-1}))((a_{0},...,a{n-1})) 的傅里叶变换。考虑另一个向量 ((c_{0},...,c_{n-1})) 满足

    [c_{k}=sum_{i=0}^{n-1} y_{i}(omega_{n}^{-k})^{i} ]

    即多项式 (B(x)=y_{0}+y_{1}x+y_{2}x^{2}+...+y_{n-1}x^{n-1})(omega_{n}^{0},omega_{n}^{-1},...,omega_{n}^{-(n-1)})处的点值表示。
    。。。。。。(此处省略一大堆证明)
    然后,我们可以得到:使用单位根的倒数代替单位根,做一次类似快速傅里叶变换的过程,再将结果每个数除以 n,即为傅里叶逆变换的结果。

    (5)流程图

    总得来说,FFT的程序流程图长这样

    FFT流程图

    图还是蛮好的,很形象

    (6)迭代实现

    图像更清晰

    迭代实现

    但是细心的话可以发现,其实这个图右下角有些问题,画反了,反正不是我画的,所以呢,这锅我就不背了,哈哈。

    (7)蝴蝶操作

    懒得写了,看博客1,写得很详细

    (8)代码

    哈哈,终于可以上代码了!
    原题,洛谷P3803

    #include<cstdio>
    #include<iostream>
    #include<algorithm>
    #include<cstring>
    #include<cmath>
    typedef long long ll;
    typedef double dd;
    #define For(i,j,k) for (int i=j;i<=k;++i)
    #define Forr(i,j,k) for (int i=j;i>=k;--i)
    #define Set(a,p) memset(a,p,sizeof(a))
    using namespace std;
    
    template<typename T>bool chkmax(T& a,T b) {return a<b?a=b,1:0;}
    template<typename T>bool chkmin(T& a,T b) {return a>b?a=b,1:0;}
    
    const int maxn=5e6+1e2;
    const dd Pi=acos(-1.0);
    struct Complex {
    	dd x,y;
    }a[maxn],b[maxn];
    int n,m,N,cnt;
    int p[maxn];
    
    inline int read() {
    	int x=0,p=1;
    	char c=getchar();
    	while (!isdigit(c)) {if (c=='-') p=-1; c=getchar();}
    	while (isdigit(c)) {x=(x<<1)+(x<<3)+(c-'0'); c=getchar();}
    	return x*p;
    }
    
    Complex operator + (const Complex &aa,const Complex &bb) {
    	return (Complex){aa.x+bb.x,aa.y+bb.y};
    }
    
    Complex operator - (const Complex &aa,const Complex &bb) {
    	return (Complex){aa.x-bb.x,aa.y-bb.y};
    }
    
    Complex operator * (const Complex &aa,const Complex &bb) {
    	return (Complex){aa.x*bb.x-aa.y*bb.y,aa.x*bb.y+aa.y*bb.x};
    }
    
    inline void FFT(Complex *s,int type) {
    	For (i,0,N-1)
    		if (i<p[i]) swap(s[i],s[p[i]]);//得到新序列
    	for (int mid=1;mid<N;mid<<=1) { //枚举合并区间的中点
    		Complex Wn=(Complex){cos(Pi/mid),type*sin(Pi/mid)}; //单位根
    		for (int len=mid<<1,j=0;j<N;j+=len) { 
                            //len是区间的长度,j是区间的左端点
    			Complex w=(Complex){1,0}; //幂
    			for (int k=0;k<mid;++k,w=w*Wn) {
                                      //枚举左区间
    				Complex x=s[j+k],y=w*s[j+mid+k];
    				s[j+k]=x+y; s[j+mid+k]=x-y;
                                     //蝴蝶操作
    			}
    		}
    	}
    }
    
    int main() {
    	n=read(); m=read();
    	For (i,0,n) a[i].x=read();
    	For (i,0,m) b[i].x=read();
    	for (N=1;N<=n+m;N<<=1,++cnt) ;
    	For (i,0,N-1)
    		p[i]=(p[i>>1]>>1) | ((i&1)<<(cnt-1));
    /*	这里就是翻转,对于原序列的第i个数,将i在二进制下翻转之后就是原来元素在新序列
    	中的位置。这里预处理,因为要翻转i,其实就是先将i/2翻转之后,再看i的奇偶,判
    	断最高位。*/
    	FFT(a,1); FFT(b,1);
    	For (i,0,N) a[i]=a[i]*b[i];
    	FFT(a,-1);
    	For (i,0,n+m) printf("%d ",(int)(a[i].x/N+0.5));
    	return 0;
    }
    

    (8)注意

    (Pi) 是doule型的,不要习惯性的定义为int

  • 相关阅读:
    go 算法之插入排序算法 running
    Go多协程并发环境下的错误处理 running
    go 协程控制之sync.Once{} running
    php 生成指定范围内不重复的随机数 running
    php 正则函数preg_replace_callback running
    go 算法之冒泡算法 running
    googleStyle
    ubuntu搭建APT源简单方法
    request.getParameterMap()快速转化为bean
    多线程编程CountDownLatch和CyclicBarrier
  • 原文地址:https://www.cnblogs.com/Wuweizheng/p/8553081.html
Copyright © 2020-2023  润新知