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

知识点

声明: 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 个两两不同的位置”为 n n 次单位根,记录 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
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值