快速傅里叶变换-FFT

本蒟蒻终于来补证明了!!!终于在学完三角函数和复数后理解了一遍

本博客参考自command_block的FFT学习笔记,作本蒟蒻平时复习用


1. DFT & IDFT的思想

众 所 周 知,一个 \(n\) 次的函数 \(f(x)\) 可以由 \(n+1\) 个点表示

因此 \(\{(x_0,y_0),(x_1,y_1),(x_2,y_2),...,(x_{n+1},y_{n+1})\}\) 可以来表示 \(f(x)\)

同时 \(\{(x_0,y_0'),(x_1,y_1'),(x_2,y_2'),...,(x_{n+1},y_{n+1}')\}\) 来表示 \(g(x)\) (注意只有 \(y\)\('\)

如果 \(W(x)=f(x)*g(x)\) ,那么显然 \(W(x_i)=y_i*y_i'\)

那么我们又得到了 \(W(x)\)\(n\) 个点

但不对啊,\(w(x)\) 似乎是 \(2n\) 次的啊

没关系,我们在 \(f(x)\)\(g(x)\) 那取 \(2n+1\) 个点就行了

然后我们再将他转化成系数表示就行了

  • DFT:把系数表达转换为点值表达

  • IDFT:把点值表达转换为系 数表达(DFT的逆运算)

这就是 FFT 的算法流程

但这仍是 \(O(n^2)\)


2. DFT 的加速版本

首先,介绍 \(\omega_n^0\) ~ \(\omega_n^{n-1}\) 表示复数平面的单位根

记住!复数的三角表示 \(r(cos\theta+isin\theta)\)

有一个性质为 \(z_1*z_2=r_1r_2(cos(\alpha+\beta)+isin(\alpha+\beta))\)

因此有 \(\omega_n^i*\omega_n^j=\omega_n^{i+j}\)

好,回归正题。我们考虑一个 \(n-1\) 次的函数 \(f(x)\)(也就是有 \(n\) 项)。为了下文方便,我们保证 \(n\)\(2\) 的整次幂

\(f(x)=k_0+k_1x+k_2x^2+...+k_{n-1}x^{n-1}\)

我们考虑将它分成两半:\(Lf(x)=k_0+k_2x^+k_4x^2+...+k_{n-2}x^{n/2-1}\) , \(Rf(x)=k_1+k_3x^+k_5x^2+...+k_{n-1}x^{n/2-1}\)

显然 \(f(x)=Lf(x^2)+x\times Rf(x^2)\)

那我们将单位根 \(\omega_n^k,\ k<n/2\) 代入 \(f(x)\)

  • 先代入 \(\omega_n^k\)

    \(f(\omega_n^k)=Lf(\omega_n^{2k})+\omega_n^k\times Rf(\omega_n^{2k})\)

    因为 \(\omega_n^{2k}=\omega_{n/2}^k\)\(n\) 为偶数!)

    所以 \(f(\omega_n^k)=Lf(\omega_{n/2}^k)+\omega_n^k\times Rf(\omega_{n/2}^k)\)

  • 再代入 \(\omega_{n}^{k+n/2}\)

    \(f(\omega_{n}^{k+n/2})=Lf(\omega_{n}^{2k+n})+\omega_{n}^{k+n/2}\times Rf(\omega_n^{2k+n})\)

    \(\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ =Lf(\omega_{n}^{2k})+\omega_{n}^{k+n/2}\times Rf(\omega_n^{2k})\)

    \(\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ =Lf(\omega_{n/2}^{k})+\omega_{n}^{k+n/2}\times Rf(\omega_{n/2}^{k})\)

    \(\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ =Lf(\omega_{n/2}^{k})-\omega_{n}^{k}\times Rf(\omega_{n/2}^{k})\)

也就是说,如果我们知道 \(Lf(x),Rf(x)\) 的点值表达式,我们就可以求出 \(f(x)\) 的点值表达式

我们又发现,其实 \(Lf(x),Rf(x)\) 是一个子问题,我们用分治就可以完成了!


3. IDFT 的实现

直接给结论:将 DFT\(\omega_n^1\) 改为 \(\omega_n^{-1}\) ,答案再除以 \(n\) 即可

我们设函数 \(F(x)\) 的点值数列为 \(G\)

\(G=DFT(F)\)

易得 \(G\) 的第 \(k\) 个数为 \(G[k]=\sum_{i=0}^{n-1}(\omega_n^k)^iF[i]\)

那么我们要证明的就是 \(n\times F[k]=\sum_{i=0}^{n-1}(\omega_n^{-k})^iG[i]\)

证明:

\[\begin{aligned} \sum_{i=0}^{n-1}(\omega_n^{-k})^iG[i]&=\sum_{i=0}^{n-1}(\omega_n^{-k})^i\sum_{j=0}^{n-1}(\omega_n^i)^jF[j]\\ &=\sum_{i=0}^{n-1}\sum_{j=0}^{n-1}\omega_{n}^{i(j-k)}F[j] \end{aligned}\]

我们考虑每个 \(j\) 单独贡献,则有分类讨论:

  • \(j=k\),则贡献为

    \[\sum_{i=0}^{n-1}\omega_{n}^0F[k]=n\times F[k] \]

  • \(j\not =k\),令 \(p=j-k\),则贡献为

    \[\sum_{i=0}^{n-1}\omega_{n}^{ip}F[k+p] \]

    易证 \(\sum_{i=0}^{n-1}\omega_n^{ip}=0\)

    因此当 \(j\not = k\) 时,贡献都为 \(0\)

这本质其实是单位根反演


4. 卡常

到上面,我们就可以码出 FFT

但由于常数巨大,我们不得不卡卡常

① 蝴蝶变换

FFT 是满足蝴蝶变换的,就是下面这样:

\[\begin{aligned} 0\ 1\ 2\ 3\ 4\ 5\ 6\ 7\\ 0\ 2\ 4\ 6|1\ 3\ 5\ 7\\ 0\ 4|2\ 6|1\ 5|3\ 7\\ 0|4|2|6|1|5|3|7\\ \end{aligned}\]

观察分治结束时各元素的位置变化(注意标号从 \(0\) 开始)

我们可以看到,元素 \(x\) 的二进制与它最后的位置 \(i\) 的二进制恰好相反

比如 \(6=(110)_2\),反转后就是 \(3=(011)_2\)

那我们是不是要用 \(O(nlogn)\) 的时间来预处理?

如果是那我要它干嘛

其实有 \(O(n)\) 的处理方法:

for(int i = 0; i < n; i++)
    t[i] = (t[i >> 1] >> 1) | (i & 1 ? n >> 1 : 0);

很好理解的啦

通过蝴蝶变换,我们就可以避免分治过程中的数组拷贝

再配合上迭代实现(就是不要递归),效率将大大提升

迭代实现代码


② 三次变两次

上述做法中,我们调用了三次的 DFT

那有没有什么方法可以减少次数呢?

还真有,我们考虑复函数 \(W(x)=f(x)+g(x)i\),也就是 \(f(x)\) 是它的实部,\(g(x)\) 是它的虚部

我们考虑将其平方,得到 \(P(x)^2=f(x)^2-g(x)^2+2f(x)g(x)i\)

也就是说,我们在将 \(P(x)\) 转化为点值表达再自己相差后,得到的函数的 虚部除以 \(2\) 就是我们要求的答案

最后做 IDFT 即可

两次DFT的代码

(不得不说,真的快了不少)

posted @ 2022-04-04 10:48  zuytong  阅读(81)  评论(0编辑  收藏  举报