FFT快速傅里叶变换的实现

FFT快速傅里叶变换的实现

关于FFT的原理可以参照之前的一篇课程报告:[Report] 基于快速傅里叶变换的大数乘法原理分析
以及_Orchidany的博客:FFT·快速傅里叶变换

本文主要参照该博客实现方式,通过C++实现FFT,对博客中部分内容作了补充说明,包含递归法和迭代法。

首先有复数运算类(此处用不到乘法就不写了):

struct Complex {
    double x, y;
    Complex (double xx = 0, double yy = 0): x(xx), y (yy) { }

    inline Complex operator+(const Complex &c2) const {
        return Complex(x + c2.x, y + c2.y);
    }

    inline Complex operator-(const Complex &c2) const {
        return Complex(x - c2.x, y - c2.y);
    }

    inline Complex operator*(const Complex &c2) const {
        return Complex(x * c2.x - y * c2.y, x * c2.y + y * c2.x);
    }
};

我们总是设flag==0时为FFT,flag==1时为IFFT。
根据FFT原理,容易写出如下递归代码:

void fft(Complex *c, int len, int flag)
{
    if (len == 1) return;
    int mid = len >> 1;
    Complex A0[mid], A1[mid];
    for (int i = 0; i <= len; i += 2)
        A[i >> 1] = A[i], A[i >> 1] = A[i + 1];     //将系数按奇偶分开
    fft(A0, mid, flag);
    fft(A1, mid, flag);
    Complex unit(cos(2 * PI / len), sin(2 * PI / len));     //单位原根
    Complex w(1, 0);
    for (int i = 0; i < mid; i++, w = w * unit) {
        Complex t = w * A1[i];      //蝴蝶变换
        c[i] = A0[i] + t;
        c[i + mid] = A0[i] - t;
    }
}   //注意若flag==-1,应将最终结果除以区间长度

由于递归版本FFT常数很大,下面考虑将代码改进为迭代版本,我们观察递归过程中系数位置的变化规律,同时注意到其实只需要将系数交换到最终(最下方)的位置,然后倒着合并问题即可:

有如下规律(原博客第5个数字有误):
原来的序号 0 1 2 3 4 5 6 7
现在的序号 0 4 2 6 1 5 3 7
原来的二进制表示 000 001 010 011 100 101 110 111
现在的二进制表示 000 100 010 110 001 101 011 111

也就是最终位置的二进制串正好是起始位置的二进制串的逆序(不是取反)。
我们可以考虑使用递推来计算逆序对应的数字。
设有数字x,逆序rev(x)。
则x左移1位时,rev(x)右移了1位,也就是rev(x) >> 1。
而当x增加1时,此时x可以看作之前某个数y左移1位y << 1后最高位的数字发生变化,并且实际上和x+1最低位一样,所以取出最低位数字放到最右边,然后或上将x >> 1逆序右移1一位就是新的逆序。
因此得到如下的递推程序;

for (int i = 1; i <= lim; i++)
        rev[i] = (rev[i >> 1] >> 1) | ((i & 1) << (log2_lim - 1));

最后我们只需要通过三重for循环控制进行迭代即可,最外层循环实际上模拟递归深度,同时也控制子问题的左右两半,第二层模拟的是不同的子问题,最内层模拟的是子问题下待合并的数据:

void FFT(Complex *c, int flag)
{
    for (int i = 0; i < lim; i++)
        if (i < rev[i])                 // 防止重复交换
            swap(c[i], c[rev[i]]);

    for (int j = 1; j < lim; j <<= 1) {                         // log2_lim-j 递归层数
        Complex w(cos(PI / j), flag * sin(PI / j));             // 本来区间长度len时应为exp(flag*2*PI/len),但待合并区间长度正好为2j  
        int j2 = j << 1;
        for (int k = 0; k < lim; k += j2) {                     // k 每层待合并的子问题
            Complex t(1, 0);
            for (int l = 0; l < j; l++, t = t * w) {            // l 每个子问题下的数据偏移量
                int kl = k + l;
                Complex Nx = c[kl], Ny = t * c[kl + j];         // 蝴蝶变换,Nx为偶数项,Ny为奇数项
                c[kl] = Nx + Ny;                                // 左半部分合并
                c[kl + j] = Nx - Ny;                            // 右半部分合并
            }
        }
    }

    if (flag == -1)                     // 处理IFFT变换系数
        for (int i = 0; i <= lim; i++)
            c[i].x = c[i].x / lim;         // 虚部趋于0不作处理
}

注意交换时只能交换一次,这就是迭代版本的FFT。

例题:
P1919 【模板】A*B Problem 升级版(FFT 快速傅里叶变换)
P3803 【模板】多项式乘法(FFT)

贴个AC图[/斜眼笑]

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注