快速傅里叶变换(FFT)详解

感谢 路人黑的纸巾, 理论部分来源于地址

FFT原理:将多项式的系数表示转换为点值表示,从而进行卷积运算,理论上从\(O(n^2)\)降低到\(O(nlogn)\)

\[f(x)= a_0 + a_1x + a_2x^2+\cdots+a_{n-1}x^{n-1} \\ g(x)= b_0+b_1x+b_2x^2 + \cdots + b_{m-1}x^{m-1} \]

\(f(x)\)\(n\)次多项式,\(g(x)\)\(m\)次多项式,\(f(x)\cdot g(x)\)\(m+n\)次多项式。

将多项式从系数表示法转为点值表示法更加方便两个多项式相乘(只要将他们的对应项的做乘积即可),同时为了方便对递归算法进行改写,将点值扩展到它们的和向上最近的那个2的幂次方个:

\[f(x):(f(x_0), f(x_1),\cdots,f(x_{r-1})) \\ g(x):(g(x_0), g(x_1), \cdots, g(x_{r-1})) \]

这里\(r\)大于\(m+n\)最小的2的幂次方。然后选择\(r\)个插值点,使得每个点的函数计算的时间花费最小。

如上图当\(r=8\)时可以在一维复空间上这样等分的选择这8个插值点(每个点是单位圆上的等分点),是因为这些点有以下的几个性质可以用来减少计算量

性质

  1. 每个插值点:\(w_r^k=cos\frac{k}{r}2\pi + isin\frac{k}{r}2\pi,(k=0,...,r-1)\)

  2. \(w_{2r}^{2k}=w_r^k\)

    证:\(w_{2r}^{2k}=cos\frac{2k}{2r}2\pi+isin\frac{2k}{2r}2\pi=cos\frac{k}{r}2\pi+isin\frac{k}{r}2\pi=w_r^k\)

  3. \(w_r^{k+\frac{r}{2}}=-w_r^{k}\)

    证:\(w_r^{k+\frac{r}{2}}=cos\frac{k+\frac{r}{2}}{r}2\pi+isin(\frac{k+\frac{r}{2}}{r}2\pi)=cos(\frac{k}{r}2\pi+\pi)+isin(\frac{k}{r}2\pi+\pi)=-w_r^k\)

  4. \(w_n^0=w_n^n\)

  5. \(w_n^iw_n^j=w_n^{i+j}\)(复数乘法运算规律)

过程

首先对多项式进行整理有:

\[\begin{align} t(x) & = \sum_0^{r-1}a_ix^i \\ & = \sum_{i=0}^{\frac{r}{2}-1}a_{2i}x^{2i} + x\sum_{i=0}^{\frac{r}{2}-1}a_{2i-1}x^{2i} \\ & = t_1(x^2) + xt_2(x^2) \end{align} \]

这里有\(t_1(x) = \sum_{i=0}^{i=\frac{r}{2}-1}a_{2i}x^i\)\(t_2(x) = \sum_{i=0}^{i=\frac{r}{2}-1}a_{2i+1}x^i\)

\((k<\frac{r}{2})\)\(w^k_r\)带入有:

\[\begin{align} t(w_r^k) & = t_1((w_r^k)^2) + w_r^kt_2((w_r^k)^2) \\ & = t_1(w_r^{2k})+w_r^kt_2(w_r^{2k}) \\ & = t_1(w_\frac{r}{2}^k) + w_r^kt_2(w^k_\frac{r}{2}) \end{align} \]

\(w_r^{k+\frac{n}{2}}\)带入有:

\[\begin{align} t(w_r^k) & = t_1((-w_r^k)^2) - w_r^kt_2((-w_r^k)^2)\\ & = t_1((w_r^k)^2) - w_r^kt_2((w_r^k)^2) \\ & = t_1(w_r^{2k})-w_r^kt_2(w_r^{2k}) \\ & = t_1(w_\frac{r}{2}^k) - w_r^kt_2(w^k_\frac{r}{2}) \end{align} \]

可以发现两个问题:

  1. 计算\(w_r^k\)时只需要在计算前一半的时候将符号进行改变,就能得到关于原点对称的另外一个值,就不需要单独的在计算一次计算。
  2. 计算\(w_r^k\)的过程实际上是将该问题分解成为两个不相交的子问题,每个子问题可以继续向下递归求解。

\(w_8^0\)的计算过程为例:

  • 每个公示的上面的数字表示的是当前多项式的系数
  • 如果当求解的数值变成了\(w_8^k\),只需要对每个黑框中的数值进行相应的替换

如果考虑到不同的\(w_8^k\)的求解,那么上面的求解过程实际上是在下面的表中遍历了一棵二叉树

  • 第一个\(+0\)表示的是左边的分支(\(t_1(w_4^0)\)) ,加上(也就是”+“的意思)\(w_8^0\)(“0”代表的是右上标)乘上右边的分支(\(t_2(w_4^0)\))。
  • 由于两个关于原点对称的单位复数的符号相反,所以后一部分第二项前面乘的系数可以\(+w_8^{4+k}\)写成\(-w_8^k\)(简写成为“\(-k\)")
  • 中间的黑色竖杠将区间在不同深度上分成了不相交的子区间

下面是几种情况的求解过程,可以理解下

可以看出所有的多项式的求解都是在这个\(4\times8\)的表格上进行的,从下到上维护整个表格便可以得到傅里叶变换,每一层维护的时间复杂度是\(O(n)\),从\(w_2\)\(w_8\)一共三层(每层都是类似开方的降系数),所以层数是\(O(logn)\),因此总的时间复杂度是\(O(nlogn)\)

傅里叶变化的优化实际上利用\(w_{2r}^{2k}=w_r^k\) 这个性质,巧妙的将计算的复杂度降了下来。如果递归求解,实际上也是要递归到最后一层,将该层的信息整体维护好供上层使用。

逆傅里叶变换(IFFT)(最近有点忙,有时间再看)未完待续。。。。

模板:

代码的一些细节,比如rev[i]的作用和如何实现的,可以参考别人的文章。

#include <cmath>
#include <complex>
using namespace std;

typedef complex<double> cpx;
const double pi = acos(-1.0);
int rev[maxn];

void fft_init(cpx *coe1, cpx *coe2, int n, int &bit, int &bit_len) {
    bit = 0;
    while ((1<<bit) < n) bit++;
    bit_len = 1<<bit; // 找到最小的2的幂次方数
    for (int i = 0; i < bit_len; i++) coe1[i] = coe2[i] = 0;
    for (int i = 0; i < bit_len; i++) //初始化每个元素对应的最后的位置
        rev[i] = (rev[i>>1]>>1) | ((i&1)<<(bit-1));
}

void fft(cpx *coe, int bitn, int bit, int inv) {
    for (int i = 0; i < bitn; i++)
        if (i < rev[i]) swap(coe[i], coe[rev[i]]);
    // mid枚举隔板的长度
    for (int mid = 1; mid < bitn; mid <<= 1) {
        cpx tmp(cos(pi/double(mid)), inv*sin(pi/mid));
        // 枚举当前在哪一个隔板那里
        for (int i = 0; i < bitn; i += (mid<<1)) {
            cpx omega(1.0, 0.0);
            // 计算两边函数的值,隔板的左边是x+y,隔板右边是x-y
            for (int j = 0; j < mid; j++, omega *= tmp) {
                cpx x = coe[i + j], y = omega*coe[i+j+mid];
                coe[i+j] = x + y, coe[i+j+mid] = x - y;
            }
        }
    }
    // 逆映射
    if (inv < 0) for (int i = 0; i < bitn; i++)    coe[i] /= double(bitn);
}
posted @ 2019-12-04 20:12  zprhhs  阅读(4720)  评论(0编辑  收藏  举报
Power by awescnb