[关闭]
@dxbdly 2023-07-16T11:51:19.000000Z 字数 8113 阅读 293

快速傅里叶变换(FFT)

OI-算法


部分内容参考文献
本文作者,引用或转载请注明原文链接

1. 前言

是一种用来实现 (离散傅里叶变换)的高效算法。

它一般用于加速多项式乘法,可使其达到 的速度,有时也用于加速大整数乘法。

注: 算法涉及较多前置数学知识,本文仅会介绍重要的前置知识,其他不清楚的请读者自行百度。


2. 前置知识1:多项式表示法

系数表示法

通常,我们表示多项式的方式是系数表示法,即使用一个系数数列来表示多项式。

也就是形如:


即:

但如果依照这种表示方式进行多项式乘法,乘积的系数数列会十分复杂,难以研究。

所以我们考虑使用其他方式表示多项式。

点值表示法

我们把多项式看成一个函数,由于我们可以使用 个此函数上的点,来唯一表示此函数。

也就是说,这 个点同样可以唯一表示此多项式。

也就是当:

就有:

这样表示有何好处呢?

我们会发现,在点值表示法下,多项式的乘积将很好表示。

设:有两多项式

那么乘积 可表示为:

也就是说,如果我们能将 表示为点值表示法,那么就能用 的时间求出 的点值表示。

那么留给我们的就是两个问题:

如何将 从系数表示变为点值表示
如何将 从点值表示变回系数表示


3. 前置知识2:单位复根

我们发现,在解决这两个问题之前,我们需要先找出 个点

并且它们适于我们进行转换,换句话说,我们希望这 个点满足某种规律,且对所有多项式都适用。

在这里,我们需要引入一个概念—— 单位复根

单位复根的定义

PS:不知道复数是什么的读者请看

下面附上手写的复数类型,包括复数的三种运算:

  1. //The code is from dxbdly
  2. struct Complex {
  3. double x, y;
  4. Complex(double x0 = 0, double y0 = 0) { x = x0, y = y0; }
  5. }g1[maxn], g2[maxn], sum[maxn];
  6. Complex operator + (Complex a, Complex b) { return Complex(a.x + b.x, a.y + b.y); }
  7. Complex operator - (Complex a, Complex b) { return Complex(a.x - b.x, a.y - b.y); }
  8. Complex operator * (Complex a, Complex b) { return Complex(a.x * b.x - a.y * b.y, a.x * b.y + a.y * b.x); }

我们称 在复数意义下的解为 次复根

显然,这样的复根有 个,我们记它们为

我们考虑这个方程的几何意义:

在复平面上作一个单位圆,则此单位圆上的复数向量模长都为
所以我们可以将 理解成复平面上的一个单位向量,它的俯角的 倍正好是
就是 单位复根,且这 次复根将这个圆周 等分。

所以,我们可以利用俯角求出

单位复根的性质

单位复根有三个重要的性质:

对于任意正整数 与整数 ,有:


  • 解释:相当于转一周回到原点。

  • 解释:在 份中取 份与 在 中取 份显然相同

  • 解释:相当于转半圈,坐标取反

  • 解释:分别取 份与 份显然就是共取

后续的 推导过程中,需要反复运用这三个式子,请好好理解。


4.

确定了要带入的 个点后,我们先来解决系数表示变点值表示的问题。

这个过程称为

如果直接带入点,复杂度仍是 的,不能接受。

由于我们取的点是存在规律的,我们考虑能否一次性将所有点的函数值求出。

分治

很自然的想到了分治。

这里积累一种套路:如果要对多项式分治,可以考虑尝试将奇数项与偶数项分开,构造子问题。

举个例子:

设:

奇偶分组:

可以发现两部分都是 的子结构。

设:

表示

接下来,考虑带入点

由于要在 的时间内,所以每次需要将求解的范围缩小。

考虑单位复根性质 ,则可尝试表示

这样,我们就可以用 同时算出两个值,保证了 的复杂度。

位逆序置换(蝴蝶变换)

上面的分治算法虽以达到 的复杂度。

可实际上由于要使用递归,并且需传输大量的数组,指针,实现起来常数巨大。

我们考虑将其用迭代实现,这样,我们就可以从最后的状态倍增的向上合并。

也就是说,我们首先要想办法求出最后分组的状态,也就是确定合并的顺序。

举个例子:

原数列:

最终数列:

而这个序列是有规律的:

对于原来的序列,将每个数用二进制表示,然后将二进制翻转,就是对应的最终位置的下标
我们称这种翻转为位逆序置换,又称蝴蝶变换

根据定义,很容易想到用 的方式求解每个数变换后的结果。

但是这还不够,实际上,我们可以通过递推求出 的位逆序置换值。

设: 表示 中的位逆序置换值

边界:

考虑转移,将 的翻转分为其个位的翻转与个位前的翻转,即: 的翻转与 的翻转

则有:

注意:

是因为去掉个位反转后,首位补的 会翻转到个位,要去掉。
则是如果个位为 ,那么首位为 ,否则补

位逆序置换部分代码:

  1. //The code is from dxbdly
  2. inline void Change(Complex f[], int len) {
  3. for(register int i = 0; i < len; ++i) {
  4. rev[i] = rev[i >> 1] >> 1;
  5. if(i & 1) rev[i] |= (len >> 1);
  6. }
  7. for(register int i = 0; i < len; ++i)
  8. if(i < rev[i]) swap(f[i], f[rev[i]]);
  9. }

这样就可以用 的时间预处理,然后按照 的过程将其合并就好,具体实现后面会讲。


5.

,即傅里叶反变换算法,它可以解决将点值表示变回系数表示的问题。

它与 算法极为类似,实现时一般也写在一起(后面细讲)。

线性代数意义下的

可以发现, 本质是一个线性变换,我们尝试将这个过程用矩阵表示出来:

现在我们相当于知道 点值矩阵 与 点矩阵,要求 系数矩阵。

同时乘上 的逆矩阵,也就是这样:

这就是 的矩阵表示,可以发现,这是个类似 的矩阵。

所以,我们只需要将所有 改为 就好了。

而根据定义:

所以,我们只要在 中带一个标记,就可以实现 的功能。

的实现

自己写的时候容易晕,注解一下各个循环在枚举什么,先上代码:

  1. //The code is from dxbdly
  2. inline void DFT(Complex f[], int len, int op) {
  3. Change(f, len);
  4. for(register int h = 2; h <= len; h <<= 1) {
  5. Complex step(cos(2 * Pi / h), op * sin(2 * Pi / h));
  6. for(register int i = 0; i < len; i += h) {
  7. Complex w(1, 0);
  8. for(register int k = i; k < i + h / 2; ++k) {
  9. Complex G = f[k], H = w * f[k + h / 2];
  10. f[k] = G + H, f[k + h / 2] = G - H;
  11. w = w * step;
  12. }
  13. }
  14. }
  15. if(op == -1) {
  16. for(register int i = 0; i < len; ++i) f[i].x /= len;
  17. }
  18. }

注意:
为标志变量,用于区分
第一层循环, 枚举的是当前合并层每一组的数量,其中 为单位复数。
第二层循环, 枚举的是每一组的编号起点
第三层循环, 枚举的是当前要求解的下标(另一个是
注意这里用了滚动数组,并没有新记录 函数与 函数。

6. 例题

一道模板题:

模板题

代码为了图方便封装过了的。

代码:

  1. //The code is from dxbdly
  2. #include<bits/stdc++.h>
  3. using namespace std;
  4. inline int read() {
  5. int x = 0;
  6. char c = getchar();
  7. bool f = 0;
  8. while(!isdigit(c)) f |= (c == '-'), c = getchar();
  9. while(isdigit(c)) x = (x * 10) + (c ^ 48), c = getchar();
  10. return f ? -x : x;
  11. }
  12. namespace FFT {
  13. const double Pi = acos(-1);
  14. const int maxn = 3e6 + 5;
  15. int rev[maxn];
  16. struct Complex {
  17. double x, y;
  18. Complex(double x0 = 0, double y0 = 0) { x = x0, y = y0; }
  19. }g1[maxn], g2[maxn], sum[maxn];
  20. Complex operator + (Complex a, Complex b) { return Complex(a.x + b.x, a.y + b.y); }
  21. Complex operator - (Complex a, Complex b) { return Complex(a.x - b.x, a.y - b.y); }
  22. Complex operator * (Complex a, Complex b) { return Complex(a.x * b.x - a.y * b.y, a.x * b.y + a.y * b.x); }
  23. inline void Change(Complex f[], int len) {
  24. for(register int i = 0; i < len; ++i) {
  25. rev[i] = rev[i >> 1] >> 1;
  26. if(i & 1) rev[i] |= (len >> 1);
  27. }
  28. for(register int i = 0; i < len; ++i)
  29. if(i < rev[i]) swap(f[i], f[rev[i]]);
  30. }
  31. inline void DFT(Complex f[], int len, int op) {
  32. Change(f, len);
  33. for(register int h = 2; h <= len; h <<= 1) {
  34. Complex step(cos(2 * Pi / h), op * sin(2 * Pi / h));
  35. for(register int i = 0; i < len; i += h) {
  36. Complex w(1, 0);
  37. for(register int k = i; k < i + h / 2; ++k) {
  38. Complex G = f[k], H = w * f[k + h / 2];
  39. f[k] = G + H, f[k + h / 2] = G - H;
  40. w = w * step;
  41. }
  42. }
  43. }
  44. if(op == -1) {
  45. for(register int i = 0; i < len; ++i) f[i].x /= len;
  46. }
  47. }
  48. inline void Work(int n, int m, int num1[], int num2[]) {
  49. int len = 1;
  50. while(len < n * 2 || len < m * 2) len <<= 1;
  51. for(register int i = 0; i < n; ++i) g1[i] = Complex(num1[i], 0);
  52. for(register int i = 0; i < m; ++i) g2[i] = Complex(num2[i], 0);
  53. for(register int i = n; i < len; ++i) g1[i] = Complex(0, 0);
  54. for(register int i = m; i < len; ++i) g2[i] = Complex(0, 0);
  55. DFT(g1, len, 1), DFT(g2, len, 1);
  56. for(register int i = 0; i < len; ++i) sum[i] = g1[i] * g2[i];
  57. DFT(sum, len, -1);
  58. for(register int i = 0; i < n + m - 1; ++i) printf("%d ", (int)(sum[i].x + 0.5));
  59. }
  60. }
  61. const int maxn = 1e6 + 5;
  62. int n, m, num1[maxn], num2[maxn];
  63. int main() {
  64. n = read() + 1, m = read() + 1;
  65. for(register int i = 0; i < n; ++i) num1[i] = read();
  66. for(register int i = 0; i < m; ++i) num2[i] = read();
  67. FFT::Work(n, m, num1, num2);
  68. return 0;
  69. }

几个要注意的点:
几乎所有的操作都要求在 的整数幂下进行,所以需要对多项式先补
的复数类型很慢,这里直接自己写的,三种运算,应该算基础吧。
最后关于精度,四舍五入输出,大部分时候 够用。

7.

刚学的 ,没做什么题,文章有不对的地方请私信博主。

最后, 是一种很重要的多项式算法,之后的许多多项式有关的算法都可以用 优化。

其中有许多算法思想更是值得我们学习。

解决 问题的算法还有很多,例如 快速数论变换(NTT)(任意模数 ),(快速沃尔什变换)

但这些算法都是以 为基础衍生而来,所以读者需要将 理解清楚后进行深入的学习

希望本文对大家有所帮助,感谢观看。

部分内容参考文献
本文作者,引用或转载请注明原文链接

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注