引入

什么是 \(\text{FFT}\) ?

反正我看到 \(\text{wiki}\) 上是一堆奇怪的东西。

快速傅里叶变换(英语:Fast Fourier Transform, FFT),是快速计算序列的离散傅里叶变换(DFT)或其逆变换的方法。傅里叶分析将信号从原始域(通常是时间或空间)转换到频域的表示或者逆过来转换。FFT会通过把DFT矩阵分解为稀疏(大多为零)因子之积来快速计算此类变换。—— \(\text{wikipedia}\)

反正我没脑子我看不懂。

对我来说,\(\text{FFT}\) 就是能把多项式乘法从 \(O(n^2)\) 变成 \(O(n\log n)\) 的神仙玩意。

正文

系数表示法和点值表示法

对于系数表示法,就是用多项式的系数来表示这个多项式。

比如说:

\[f(x)=a_1x^3+a_2x^2+a_3x+a_4\Leftrightarrow f(x)=\{a_1,a_2,a_3,a_4\}
\]

那么对于点值表示法,相对应的就是用该函数上的若干个点表示多项式。

学过小学数学的同学们一定知道:\(n+1\) 个点确定一个 \(n\) 次多项式。

证明的话可以考虑数学归纳法。/xyx

同样举一个例子,点值表示法是这样的:

\[f(x)=a_0+a_1x+a_2x^2+\cdots+a_nx^n⇔f(x)=\{(x_0,y_0),(x_1,y_1),(x_2,y_2),\cdots,(x_n,y_n)\}\\
\]

上面讲到要把系数表示法转换成点值表示法。那么这是为什么呢?

下面就先来展示一下点值表示法的多项式乘法:

\[f(x)=\{(x_0,f(x_0)),(x_1,f(x_1)),(x_2,f(x_2)),\cdots,(x_n,f(x_n))\}\\
g(x)=\{(x_0,g(x_0)),(x_1,g(x_1)),(x_2,g(x_2)),\cdots,(x_n,g(x_n))\}\\
F(x) = f(x)\times g(x)\\
F(x)=\{(x_0,g(x_0)\times f(x_0)),(x_1,g(x_1)\times f(x_1)),\cdots,(x_n,g(x_n)\times f(x_n))\}
\]

复数

\(复数 = 实数 + 虚数\)

实在一点吧,直接上干货,我们定义 :

\[i=\sqrt{-1}
\]

这样我们就可以表示我们在实数范围内不能表示的数了。

那么如何表示一个复数呢:

\[Num=a+bi\ \ (a,b \in \mathbb{R})
\]

接着我们把 \(Num=a+bi\) 看成一个函数,把 \(a\) 和 \(b\) 分别对应 \(x\) 轴和 \(y\) 轴。

就可以得到复数平面,大概长这样:



其中横坐标是实数轴,纵坐标是虚数轴,这样就可以把每个虚数看为一个向量了。

对应的,虚数可以用普通坐标和极坐标表示:

\[(x,y)\quad和\quad (r,\theta)
\]

下面给出两个复数相乘的意义:

\[\begin{split}&\quad
(a+bi)\times(c+di)\\&=
ac+adi+bci+bdi^2\\
&=(ac+bd)+(ad+bc)i
\end{split} \\\quad \\
(r_1,\theta_1)\times(r_2,\theta_2)=(r_1\times r_2,\theta_1+\theta_2)
\]

\(\tt DFT\) (离散傅里叶变换)

现在已经介绍完了点值表示法复数的相关知识,接下来就是干货部分了。

上面我们已经通过这样的例子说明了点值表示法算多项式乘法的方便。

接下来我们来看怎么先把多项式从系数表示法转换为点值表示法,这种过程叫 \(\text{DFT}\) 。

所谓的点值表示法,也就是在 \(n\) 多项式上取 \(n+1\) 个点,来进行表示。

形式化的,可以表示成这样:

\[F(x)=a_0+a_1x+a_2x^2+\cdots+a_{n-1}x^{n-1}+a_nx^n\\
\rightarrow F(x)=\{(x_0,F(x_0)),(x_1,F(x_1)),(x_2,F(x_2)),\cdots,(x_n,F(x_n))\}
\]

然后可以惊喜的发现,随便带几个 \(x_i\) 进去在算算 \(F(x_i)\) 就好了。

但是如果你小学毕业了,你就可以发现这样的话不如直接 \(O(n^2)\) 暴力。

所以该怎么办?

我们猜想是否存在一些 \(x\) 使得 \(x^n\ (n\in \tt Z^+)\) 的结果都是 \(1\) 。

这看上去是一个非常好的思路,但是这样的数有多少个呢?

我能脱口说出两个 \(1\) 和 \(-1\) ,想一想可以发现其实 \(i\) 和 \(-i\) 也都可以。

但是经过认真思考(看题解)可以发现下图的单位圆上所有的点都满足条件。

为了方便,我们在取这 \(n\) 个点时会把这个单位圆平分。

我们从 \((1, 0)\) 这个点开始,按照逆时针的方向从 \(0\) 开始进行编号,形如 \(\omega_n^k\) 。

其中 \(n\) 表示一共选择了 \(n\) 个点,\(k\) 表示当前点的编号。

由我们之前介绍的复数乘法的 模长相乘,度数相加

\[(r_1,\theta_1)\times(r_2,\theta_2)=(r_1\times r_2,\theta_1+\theta_2)
\]

并且结合单位圆的性质(所有的点到原点的距离为 \(1\))。

可以得到由 \(\omega_n^1\) 转换到 \(\omega_n^k\) 的公式:

\[(\omega_n^1)^k=\omega_n^k
\]

我们称 \(\omega_n^1\) 为 \(n\) 次单位根。

所以可以发现,我们直接带入 \(\omega_n^i\) 就可以了。

单位根的一些有用的性质

在了解一切的性质之前,我们要先知道单位根 \(\omega_n^i\) 如何表示:

\[\omega_n^k=\cos\frac{k}{n}2\pi+i\times\sin\frac{k}{n}2\pi
\]

这东西的证明你直接照着单位圆上画一个点然后三角函数入门知识即可。

性质一

\[\omega_n^k=\omega_{2n}^{2k}
\]

证明的话直接照着上面给出的式子套即可,然后发现可以约分。

那我认为进一步的可以得到:

\[\omega_n^k=\omega_{Pn}^{Pk} \quad (p\in \tt Z^+)
\]

很显然不过好像没有什么大用。

性质二

\[\omega_n^{k+\frac{n}{2}}=-\omega_n^k
\]

证明的话稍微写一下吧:

\[\omega_n^k=\cos\frac{k}{n}2\pi+i\times\sin\frac{k}{n}2\pi\\
\begin{split}
\omega_n^{k+\frac{n}{2}}&=
\cos\frac{k+\frac{n}{2}}{n}2\pi+i\times\sin\frac{k+\frac{n}{2}}{n}2\pi\\
&=\cos(\frac{k}{n}2\pi+\pi)+i\times\sin(\frac{k}{n}2\pi+\pi)
\end{split}
\]

都化成这一步了就不在进行下一步证明,还看不懂的建议重修初中数学。

性质三

\[\omega_n^0=\omega_n^n
\]

比较憨,我就不讲为什么了。

\(\tt FFT\) (快速傅里叶变换)

他来了,他来了,等到现在他终于来了。。。。

之前讲到我们直接带入 \(\omega_n^i\) 来计算点值。

是的,我认为这种方法高效,巧妙,逼格高,体现了人类智慧。

但是等等,虽然算系数的过程免掉了,但是对于每一个 \(\omega_n^i\) 我们还是要 \(O(n)\) 算结果啊。

然后我搬来搬手指算了一下,发现一共有 \(n\) 个 \(\omega_n^i\) 的值,然后就又 \(O(n^2)\) 了。

所以我们该怎么办?

认真地看看题解,发现可以从分治的角度入手。

注意:以下的内容保证 \(n\) 为 \(2\) 的整数次方。

我们设一个多项式:

\[\begin{split}
F(x)&=\sum_{i=0}^{n-1}a_ix^i\\
&=a_0 + a_1x + a_2x^2 + \cdots + a_{n-1}x^{n-1}
\end{split}
\]

然后想办法把 \(F(x)\) 分成两个部分。

这里采用的方法是按照 \(F(x)\) 下标的奇偶性分成两个部分。

\[\begin{split}
F(x)
&=a_0 + a_1x + a_2x^2 + \cdots + a_{n-1}x^{n-1}\\
&=(a_0+a_2x^2+\cdots+a_{n-2}x^{n-2})+x(a_1+a_3x^2+\cdots+a_{n-1}x^{n-2})
\end{split}
\]

接下来我们发现拆出来的这两个多项式的结构是一模一样的。

我们再分别设这两个多项式为 \(F_1(x)\) 和 \(F_2(x)\) 。

\[F_1(x)=a_0+a_2x^2+\cdots+a_{n-2}x^{n-2}\\
F_2(x)=a_1+a_3x^2+\cdots+a_{n-1}x^{n-2}\\
F(x)=F_1(x)+xF_2(x)
\]

发现这样的系数不连续,没有那么完美,于是我们再变化一下。

\[F_1(x)=a_0+a_2x^1+\cdots+a_{n-2}x^{\frac{n}{2}-1}\\
F_2(x)=a_1+a_3x^1+\cdots+a_{n-1}x^{\frac{n}{2}-1}\\
F(x) = F_1(x^2)+xF_2(x^2)
\]

此时看可以发现这样的形式非常的优美。

接下来就是直接带入 \(\omega_n^i\) 的操作了。

我们接着设 \(k<\frac{n}{2}\) 然后把 \(\omega_n^k\) 直接带入。

\[\begin{split}
F(\omega_n^k) &= F_1((\omega_n^k)^2)+\omega_n^kF_2((\omega_n^k)^2)\\
&=F_1(\omega_{n}^{2k})+\omega_n^kF_2(\omega_{n}^{2k})\\
&=F_1(\omega_{\frac{n}{2}}^{k})+\omega_n^kF_2(\omega_{\frac{n}{2}}^{k})
\end{split}
\]

第一步直接带入,有问题的话小学建议重修。

第二步的话我之前写过,公式是这样的:

\[(\omega_n^1)^k=\omega_n^k
\]

当然,在这里运用是具有普遍性的,有问题的话直接推一下。

至于第三步,直接算比例我认为会更加快速一些。

对于 \(F(\omega_n^{k+\frac{n}{2}})\) 直接带入:

\[\begin{split}
F(\omega_n^{k+\frac{n}{2}}) &= F_1((\omega_n^{k+\frac{n}{2}})^2)+\omega_n^{k+\frac{n}{2}}F_2((\omega_n^{k+\frac{n}{2}})^2)\\
&=F_1(\omega_n^{2k+n})+\omega_n^{k+\frac{n}{2}}F_2(\omega_n^{2k+n})\\
&=F_1(\omega_n^{2k}\omega_n^n)-\omega_n^{k}F_2(\omega_n^{2k}\omega_n^n)\\
&=F_1(\omega_n^{2k})-\omega_n^{k}F_2(\omega_n^{2k})\\
&=F_1(\omega_{\frac{n}{2}}^{k})-\omega_{n}^{k}F_2(\omega_{\frac{n}{2}}^{k})\\
\end{split}
\]

每一步一一介绍比较麻烦,大家直接手头一下或者翻翻前面的公式。

观察第一个式子和第二个式子,发现唯一不一样的地方就是符号了。

然后直接分治求解即可,时间复杂度 \(O(n\log n)\) 。

\(\tt IFF\) (快速傅里叶逆变换)

就是把点值表示法转换成为我们要的系数表示法。

这里给出结论,证明的话属实比较恶心,所以我就不证明了。

一个多项式在分治的过程中乘上单位根的共轭复数,分治完的每一项除以 \(n\) 即为原多项式的每一项系数

也就是再做一遍 \(\tt FFT\) 输出时每一位除以 \(n\) 就可以了。

代码实现及其优化

Code 复数类型封装

  1. struct cp {
  2. double x, y;
  3. cp (double xx = 0, double yy = 0) {x = xx; y = yy;};
  4. friend cp operator +(cp p, cp q) {return cp(p.x + q.x, p.y + q.y);}
  5. friend cp operator -(cp p, cp q) {return cp(p.x - q.x, p.y - q.y);}
  6. friend cp operator *(cp p, cp q) {return cp(p.x * q.x - p.y * q.y, p.y * q.x + p.x * q.y);}
  7. }a[N], b[N];

Code 无优化

不是我写的代码,反正就是照着之前的公式模拟,看看就好了。

点击查看代码
  1. #include<complex>
  2. #define cp complex<double>
  3. void fft(cp *a, int n, int inv) //inv是取共轭复数的符号
  4. {
  5. if (n == 1)return;
  6. int mid = n / 2;
  7. static cp b[MAXN];
  8. fo(i, 0, mid - 1)b[i] = a[i * 2], b[i + mid] = a[i * 2 + 1];
  9. fo(i, 0, n - 1)a[i] = b[i];
  10. fft(a, mid, inv), fft(a + mid, mid, inv); //分治
  11. fo(i, 0, mid - 1)
  12. {
  13. cp x(cos(2 * pi * i / n), inv * sin(2 * pi * i / n)); //inv取决是否取共轭复数
  14. b[i] = a[i] + x * a[i + mid], b[i + mid] = a[i] - x * a[i + mid];
  15. }
  16. fo(i, 0, n - 1)a[i] = b[i];
  17. }
  18. cp a[MAXN], b[MAXN];
  19. int c[MAXN];
  20. fft(a, n, 1), fft(b, n, 1); //1系数转点值
  21. fo(i, 0, n - 1)a[i] *= b[i];
  22. fft(a, n, -1); //-1点值转系数
  23. fo(i, 0, n - 1)c[i] = (int)(a[i].real() / n + 0.5); //注意精度

注意:\(\tt FFT\) 之前要先把 \(n\) 调成 \(2\) 的整数次幂。

很显然上面的那个是连模板题都过不了的。

所以在这里我们才需要去考虑怎么去优化 \(\tt FFT\) 。

观察一下原序列和反转后的序列,需要求的序列实际是原序列下标的二进制反转!

因此我们对序列按照下标的奇偶性分类的过程其实是没有必要的。

这样我们可以 \(O(n)\) 的利用某种操作得到我们要求的序列,然后不断向上合并就好了。

—— \(\tt luogu\) 某题解

Code 有优化,可过

点击查看代码
  1. #include <bits/stdc++.h>
  2. #define file(a) freopen(a".in", "r", stdin), freopen(a".out", "w", stdout)
  3. #define Enter putchar('\n')
  4. #define quad putchar(' ')
  5. #define N 3000005
  6. namespace IO {
  7. template <class T>
  8. inline void read(T &a);
  9. template <class T, class ...rest>
  10. inline void read(T &a, rest &...x);
  11. template <class T>
  12. inline void write(T x);
  13. }
  14. struct cp {
  15. double x, y;
  16. cp (double xx = 0, double yy = 0) {x = xx; y = yy;};
  17. friend cp operator +(cp p, cp q) {return cp(p.x + q.x, p.y + q.y);}
  18. friend cp operator -(cp p, cp q) {return cp(p.x - q.x, p.y - q.y);}
  19. friend cp operator *(cp p, cp q) {return cp(p.x * q.x - p.y * q.y, p.y * q.x + p.x * q.y);}
  20. }a[N], b[N];
  21. const double pi = acos(-1.0);
  22. int n1, n2, n, rev[N], c[N];
  23. inline void FFT(cp *a, int n, int inv) {
  24. int bit = 0;
  25. while ((1 << bit) < n) bit++;
  26. for (int i = 1; i < n; ++i) {
  27. rev[i] = (rev[i >> 1] >> 1) | ((i & 1) << (bit - 1));
  28. if (i < rev[i])
  29. std::swap(a[rev[i]], a[i]);
  30. }
  31. for (int mid = 1; mid < n; mid <<= 1) {
  32. cp temp(cos(pi / mid), inv * sin(pi / mid));
  33. for (int i = 0; i < n; i += mid * 2) {
  34. cp omega(1,0);
  35. for (int j = 0; j < mid; ++j, omega = omega * temp) {
  36. cp x = a[i + j], y = omega * a[i + j + mid];
  37. a[i + j] = x + y;
  38. a[i + j + mid] = x - y;
  39. }
  40. }
  41. }
  42. }
  43. signed main(void) {
  44. // file("P3803");
  45. IO::read(n1, n2);
  46. n = std::max(n1, n2);
  47. for (int i = 0, num; i <= n1; ++i) IO::read(num), a[i].x = num;
  48. for (int i = 0, num; i <= n2; ++i) IO::read(num), b[i].x = num;
  49. n = n1 + n2;
  50. for (int i = 0; i <= 30; ++i)
  51. if ((1 << i) > n) {
  52. n = (1 << i);
  53. break;
  54. }
  55. FFT(a, n, 1); FFT(b, n, 1);
  56. for (int i = 0; i < n; ++i) a[i] = a[i] * b[i];
  57. FFT(a, n, -1);
  58. for (int i = 0; i <= n1 + n2; ++i)
  59. c[i] = (int)(a[i].x / n + 0.5);
  60. for (int i = 0; i <= n1 + n2; ++i)
  61. IO::write(c[i]), quad;
  62. Enter;
  63. }
  64. namespace IO {
  65. template <class T>
  66. inline void read(T &a) {
  67. T s = 0, t = 1;
  68. char c = getchar();
  69. while ((c < '0' || c > '9') && c != '-')
  70. c = getchar();
  71. if (c == '-')
  72. c = getchar(), t = -1;
  73. while (c >= '0' && c <= '9')
  74. s = (s << 1) + (s << 3) + (c ^ 48), c = getchar();
  75. a = s * t;
  76. }
  77. template <class T, class ...rest>
  78. inline void read(T &a, rest &...x) {
  79. read(a);
  80. read(x...);
  81. }
  82. template <class T>
  83. inline void write(T x) {
  84. if (x == 0) putchar('0');
  85. if (x < 0) putchar('-'), x = -x;
  86. int top = 0, sta[55] = {0};
  87. while (x)
  88. sta[++top] = x % 10, x /= 10;
  89. while (top)
  90. putchar(sta[top] + '0'), top--;
  91. return ;
  92. }
  93. }

在这里推荐 某知乎专栏 ,把 \(\tt FFT\) 优化讲的很清楚。

\(\tt NTT\) 还是会看的,但是 \(\tt FFT\) 把我给些虚脱了。。。

FFT 学习笔记(自认为详细)的更多相关文章

  1. 再探快速傅里叶变换(FFT)学习笔记(其三)(循环卷积的Bluestein算法+分治FFT+FFT的优化+任意模数NTT)

    再探快速傅里叶变换(FFT)学习笔记(其三)(循环卷积的Bluestein算法+分治FFT+FFT的优化+任意模数NTT) 目录 再探快速傅里叶变换(FFT)学习笔记(其三)(循环卷积的Blueste ...

  2. Linux 学习笔记之超详细基础linux命令(the end)

    Linux学习笔记之超详细基础linux命令 by:授客 QQ:1033553122 ---------------------------------接Part 14---------------- ...

  3. Linux 学习笔记之超详细基础linux命令 Part 14

    Linux学习笔记之超详细基础linux命令 by:授客 QQ:1033553122 ---------------------------------接Part 13---------------- ...

  4. Linux 学习笔记之超详细基础linux命令 Part 13

    Linux学习笔记之超详细基础linux命令 by:授客 QQ:1033553122 ---------------------------------接Part 12---------------- ...

  5. Linux 学习笔记之超详细基础linux命令 Part 12

    Linux学习笔记之超详细基础linux命令 by:授客 QQ:1033553122 ---------------------------------接Part 11---------------- ...

  6. Linux 学习笔记之超详细基础linux命令 Part 11

    Linux学习笔记之超详细基础linux命令 by:授客 QQ:1033553122 ---------------------------------接Part 10---------------- ...

  7. Linux 学习笔记之超详细基础linux命令 Part 10

    Linux学习笔记之超详细基础linux命令 by:授客 QQ:1033553122 ---------------------------------接Part 9----------------- ...

  8. Linux 学习笔记之超详细基础linux命令 Part 9

    Linux学习笔记之超详细基础linux命令 by:授客 QQ:1033553122 ---------------------------------接Part 8----------------- ...

  9. Linux 学习笔记之超详细基础linux命令 Part 8

    Linux学习笔记之超详细基础linux命令 by:授客 QQ:1033553122 ---------------------------------接Part 7----------------- ...

  10. Linux 学习笔记之超详细基础linux命令 Part 7

    Linux学习笔记之超详细基础linux命令 by:授客 QQ:1033553122 ---------------------------------接Part 6----------------- ...

随机推荐

  1. 详解javascript的eventloop(二):eventloop和dom渲染

    记住: JS是单线程的,他和dom渲染共用一个线程 JS执行的时候,会给dom渲染留一些时机 上一篇讲到eventloop的执行机制,但是在这个机制中的call stack执行完成后(包括第一遍的ev ...

  2. Firefox 国外换为国内同步的服务器地址

    地址栏键入 about:config点击 接受风险并同意查找 identity把右侧字符串包含 firefox.com 替换为 firefox.com.cn 即换为国内同步服务器反之把 firefox ...

  3. Python爬虫__微博某个话题的内容数据

    1 # -*- coding: utf-8 -*- 2 # @Time : 2020/8/18 15:39 3 # @Author : Chunfang 4 # @Email : 3470959534 ...

  4. 『现学现忘』Git基础 — 13、Git的基础操作

    目录 1.Git最基础的使用方式 (1)初始化本地版本库 (2)查看文件的状态 (3)把文件添加到暂存区 (4)把暂存区的内容提交到本地版本库 2.总结本文用到的Git命令 1.Git最基础的使用方式 ...

  5. 【笔记】排查CPU占用过高

    本文是该教程视频的笔记 https://www.bilibili.com/video/BV15T4y1y7eH 1. 问题演示 将演示项目打包放到服务器运行 执行 curl http://localh ...

  6. XCTF练习题---WEB---baby_web

    XCTF练习题---WEB---baby_web flag:flag{very_baby_web} 解题步骤: 1.观察题目,打开场景 2.观察题目内容,想想初始页面是哪个,再看看URL,尝试输入in ...

  7. WFP资源

    资源基础 WPF程序在代码中以及在标记中的各个位置定义资源,具有高效性.可维护性.适应性的优点. 资源的层次 <Windows.Resource> <ImageBrush x:key ...

  8. Linux文本工具-cat-cut-paste;文本分析-sort-wc-uniq

    1.1 查看文本文件内容  cat 1.1.1 cat可以查看文本内容 cat [OPTION]... [FILE]... 常见选项 -E: 显示行结束符$ -A: 显示所有控制符 -n: 对显示出的 ...

  9. os、json、sys、subprocess模块

    os模块 import os 1.创建目录(文件夹) os.mkdir(r'a') # 相对路径 只能创建单级目录 os.makedirs(r'a\b') # 可以创建单级和多及目录 2.删除目录 o ...

  10. 1903021121-刘明伟 实验一 19信计JAVA—Markdown排版学习

    项目 内容 班级博客链接 19信计班(本) 作业要求链接 实验一 课程学习目标 学习使用Markdown排版 这个作业帮助我们实现了什么学习目标 学会使用Markdown排版 任务一:在博客园平台注册 ...