【学习】快速傅里叶变化(FFT)

来源:互联网 发布:迭代器java 线程安全 编辑:程序博客网 时间:2024/06/04 20:12

知识点

声明:goodqt

复数

形如 a+bi,其中 aR, bR, i2=1 的数称为复数,记作z=a+biC

(a+bi)+(c+di)=(a+c)+(b+d)i

(a+bi)(c+di)=(ac)+(bd)i

(a+bi)×(c+di)=(acbd)+(bc+ad)i

a+bic+di=(a+bi)(cdi)(c+di)(cdi)=ac+bdc2+d2+bcadc2+d2

在复数域 C 中定义如下运算:
ez=n=0znn!

cosz=n=0(1)nz2n(2n)!

sinz=n=0(1)nz2n+1(2n+1)!

可以证明,当 z ∈ R 时,这里的定义和之前我们在实数域中给出的定义完全等价。

可以证明,在复数域中,下式依然成立:

ez1+z2=ez1ez2

z=ix,立即得到:

eix=cosx+isinx

对任意复数 z=a+bi0,必定存在唯一的 0θ<2π 和唯一的 r>0 满足:

z=a+bi=r(cosθ+isinθ)=reiθ

其中 |z|=r=a2+b2称作复数 z 的模,θ 称作复数 z 的辐角主值。
把复数 z=a+bi 看作平面直角坐标系中的点 (a,b)r, θ 便有了明确的几何意义。此平面叫做复平面。

注意到

z1z2=r1eiθ1r2eiθ2=r1r2ei(θ1+θ2)

z1z2=r1eiθ1r2eiθ2=r1r2ei(θ1θ2)

所以在复平面中复数加减按平行四边形法则,复数乘除为模乘除、辐角加减。

单位根

考虑方程

xn=1     (nN)

n 个两两不同的解分别为

xk=e2kπin     (k[0,n1]N)

几何意义非常明显,它们被称为 n 次单位根。

多项式

给出 n − 1 次多项式有多种方法,最常见的是给出每一项前面的系数,即:

f(x)=i=0n1aixi

还有其他的方法,即给出 n 个两两不同的位置处 f(x) 的函数值。
显然如果要做乘法,第一个的复杂度需要 O(n2),而第二个的复杂度只需 O(n)
快速傅立叶变换是用 O(nlogn) 的时间把前者转换到后者、把后者转化到前者,这样我们就可以 O(nlogn) 完成一次前者的乘法了。

快速傅立叶变换

我们首先要把 n 上调至 2 的幂,后面本来没有的系数都设置为 0 就好了。
然后我们取“n 个两两不同的位置”为 nn 次单位根,记录m=n2,则

f(e2kπin)=j=0n1aj(e2kπin)j=j=0m1a2j(e2kπim)j+ekπimj=0n1a2j+1(e2kπim)j

分别拆出奇数项和偶数项,直接递归即可。时间复杂度O(nlogn)

快速傅立叶逆变换

这里写图片描述

常数

虽然复杂度没啥问题,但是递归版的快速傅立叶变换常数还是非常大的,所以可能需要优化常数。
考虑分治的过程,不停的拆分奇数项和偶数项,所以我们直接把标号全部倒置,从低位开始不停的合并 0/1 就好了,非递归版的常数还是可以接受的。

代码详解

递归版

#include <cstdio>#include <cstring>#include <iostream>#include <algorithm>#include <cmath>using namespace std;const double pi = acos(-1);//定义复数类型struct Com{    double x, y;    Com(double X = 0, double Y = 0){x = X, y = Y;}    Com operator + (const Com &t) const{return Com(x+t.x, y+t.y);}    Com operator - (const Com &t) const{return Com(x-t.x, y-t.y);}    Com operator * (const Com &t) const{return Com(x*t.x - y*t.y, y*t.x + x*t.y);}};//A一开始是系数数组,在调用完函数后,会变成记录答案的数组,答案记录在实部void fft(Com *A, int n, int f){    //仅有一个数的时候直接返回,这时的系数恰好是答案    if(n == 1) return;     int m = n/2;    Com C1[m], C2[m];    //分奇偶划分,递归    for(int i = 0; i < m; i ++) C1[i] = A[i*2], C2[i] = A[i*2+1];    fft(C1, m, f), fft(C2, m, f);    //k等与0的时候的单位元,k等于1的时候的数值    Com t1(1, 0), t2(cos(pi/m), f*sin(pi/m));    //当i大于m时,$e^{i\pi}=-1$,所以后面的项顺便算出来    for(int i = 0; i < m; i ++){        A[i] = C1[i] + t1 * C2[i];        A[i+m] = C1[i] - t1 * C2[i];        //让k增大        t1 = t1 * t2;    }}Com a[300010], b[300010];int main(){    int n, m;    scanf("%d%d", &n, &m);    for(int i = 0; i <= n; i ++) scanf("%lf", &a[i].x);    for(int i = 0; i <= m; i ++) scanf("%lf", &b[i].x);    //为了容纳答案的长度    m += n;    //最多有n+m+1项,"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);     //有浮点误差,+0.5消除    for(int i = 0; i <= m; i ++) printf("%d ", int((a[i].x/n+0.5)));    return 0;}

非递归版

//与上边一样的就不打了#include <cstdio>#include <cstring>#include <iostream>#include <algorithm>#include <cmath>using namespace std;const int maxn = 300010;const double pi = acos(-1);int Bit, rx[maxn];struct Com{    double x, y;    Com(double X = 0, double Y = 0){x = X, y = Y;}    Com(const Com &t){x = t.x, y = t.y;}    Com operator + (const Com &t) const{return Com(x+t.x, y+t.y);}    Com operator - (const Com &t) const{return Com(x-t.x, y-t.y);}    Com operator * (const Com &t) const{return Com(x*t.x - y*t.y, y*t.x + x*t.y);}};void fft(Com *A, int n, int f){    //预处理二进制翻转之后的值是多少,只会执行一次    //原理:100111 进行反转,我们得到 10011 的翻转 110010,右移一位,空出最高位,把之前的那个补上即可    if(rx[n-1] != n-1) for(int i = 0; i < n; i ++) rx[i] = (rx[i>>1]>>1) | ((i&1) << (Bit-1));    //进行位数交换,只在有特定条件的时候进行    for(int i = 0; i < n; i ++) if(i < rx[i]) swap(A[i], A[rx[i]]);    //长度 -> 开头 -> 每一项    for(int i = 1; i < n; i <<= 1){        const Com Base(cos(pi/i), f*sin(pi/i));        for(int j = 0; j < n; j += i << 1){            Com mi(1, 0);            for(int k = 0; k < i; k ++, mi = mi * Base){                Com x = A[j+k], y = A[i+j+k] * mi;                //这一句等价于:当i大于m时,$e^{i\pi}=-1$,所以后面的项顺便算出来                A[j+k] = x+y, A[i+j+k] = x-y;            }        }    }}Com a[maxn], b[maxn];int main(){    int n, m;    scanf("%d%d", &n, &m);    for(int i = 0; i <= n; i ++) scanf("%lf", &a[i].x);    for(int i = 0; i <= m; i ++) scanf("%lf", &b[i].x);    m += n;    for(n = 1; n <= m; n <<= 1, Bit ++);    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].x/n+0.5)));    puts("");    return 0;}

练习

1. BZOJ 2179 FFT快速傅立叶

2. BZOJ 2194 快速傅立叶之二

3. BZOJ 3527 [Zjoi2014]力

0 0
原创粉丝点击