0x00 前言
这篇主要是针对有基础的同学的,基础知识相信大家都学过。
这篇 blog 的诞生是因为 myh 要学 FFT,甚至疯狂到了要找别人语音解答的地步。
然后我就想起远古时候 WGY 好像学过这么个东西,就写了篇 blog 出来给 myh 各位看,顺便复习一下。
说一下学了这东西的感悟吧。我觉得只要学了一次函数和三角函数就能看懂这篇。
在那个远古时代,周 x 健还没讲一次函数,我天天抱着啃没啃懂……
后来,周 x 健讲了一次函数,我又在物竞那嫖了些三角函数。
然后整个人心态都变了,几个小时下来感觉 FFT 也就那样。(甚至不如 NTT 有用
所以大家不要畏难,在座各位的数学都果断吊打 WGY 对吧。
李琰之前也在 OJ 发过 FFT 的 note,但是递归转循环基本就是放了个代码,还留下了不少坑。
我这里也填了不少李琰的坑,但是一些基础的单位根性质的证明之类的东西我这里就懒得给了,自己考李琰的古吧。
因为时间原因一些可以从几何意义上来理解的东西我没给图,大家可以自己手% 一下。
(好罢主要是给 myh 看的手动 @ybmyh)
0x01 点值表示法
众所周知,一个
$$
F(x)=\sum_{i=0} ^ {n}a_{i}\times x ^ {i}
$$
形式的 $n$ 次多项式可以在平面直角坐标系中被 $n+1$ 个点唯一的表示出来。
点值表示的两个多项式可以在线性时间复杂度中解决相乘,就是对应的 $y$ 乘起来。
但是暴力的把系数表示法转化为点值表示法依然是 $\Theta(n ^ {2})$ 的。怎么办呢?
0x02 复数和单位根
说过不讲,我就放在这里方便我翻。
$(a+bi)+(c+di)=(a+c)+(b+d)i$
$(a+bi)-(c+di)=(a-c)+(b-d)i$
$(a+bi)\times(c+di)=ac+adi+bci-bd=(ac-bd)+(ad+bc)i$
除法可以不用,其实也不用讲,自然而然的东西。
方程 $x^{n}=1$ 的根,称作单位根用 $\omega_{n}^{k}$ 表示。
$k$ 表示第 $k$ 个 $n$ 次单位根,从 0 开始标号,$\omega ^ {0} _ {n},\omega ^ {1} _ {n},\cdots,\omega ^ {n-1} _ {n},$。其中 $\omega^{0}_{n}=1$。
虽然这样说,但是 $k\geq n$ 以及 $k < 0$ 的情况是被允许的。
原因看到后面就知道了。
从几何意义上来理解单位根即复数的坐标系单位圆的 $n$ 等分点。
复数相乘的性质:模长相乘,辐角相加。
模长指一个复数到原点的距离,$t=a+bi$ 的模长记作 $|t|$
辐角指原点到点的连线与 $x$ 轴的正方向的夹角,记作 $\arg(a+bi)$
接下来列举需要用到的公式。
$$
\begin{align}\label{2}
& \omega^{k} _ {n}=e^{\frac{2k\pi}{n}i}=\cos\frac{2k\pi}{n}i+i\sin\frac{2k\pi}{n} \tag{2.1} \\
& \omega^{0} _ {n}=1 \tag{2.2} \\
& \omega^{k} _ {n}=\omega^{k\operatorname{mod} n} \tag{2.3} \\
& \omega^{k} _ {n}\times\omega^{j} _ {n}=\omega^{k+j} _ {n} \tag{2.4} \\
& (\omega^{1} _ {n})^{k}=\omega^{k} _ {n} \tag{2.5} \\
& \omega^{pk} _ {pn}=\omega^{k} _ {n} \tag{2.6} \\
& \omega^{k+\frac{n}{2}} _ {n}=-\omega ^ {k} _ {n} \tag{2.7} \\
\end{align}
$$
靠这段在 vsc 上显示不出来我自毙
0x03 继续研究多项式
我们设一个多项式
$$F(x)=\sum_{i=0}^{n-1}a_{i}\times x^{i}$$
保证 $n=2^{p}+1$
我们按 $i$ 的奇偶性把 $F$ 分为两个部分
$$F(x)=\sum_{i=0}^{n-1}a_{i}\times x^{i}=\sum_{i=0}^{\frac{n}{2}-1}a_{2i}\times x^{2i}+\sum_{i=0}^{\frac{n}{2}-1}a_{2i+1}\times x^{2i+1}$$
继续定义
$$L(x)=\sum_{i=0}^{\frac{n}{2}-1}a_{2i}\times x^{i}$$
$$R(x)=\sum_{i=0}^{\frac{n}{2}-1}a_{2i+1}\times x^{i}$$
也就是说
$$\begin{array}{l} F(x)&=\sum_{i=0}^{n-1}a_{i}\times x^{i} \\ &=\sum_{i=0}^{\frac{n}{2}-1}a_{2i}\times x^{2i}+\sum_{i=0}^{\frac{n}{2}-1}a_{2i+1}\times x^{2i+1} \\ &=\sum_{i=0}^{\frac{n}{2}-1}a_{2i}\times (x^{2})^{i}+\sum_{i=0}^{\frac{n}{2}-1}a_{2i+1}\times (x^{2})^{i}\times x \\ &=\sum_{i=0}^{\frac{n}{2}-1}a_{2i}\times x^{2i}+\sum_{i=0}^{\frac{n}{2}-1}a_{2i+1}\times x^{2i+1}\\ &=L(x^{2})+xR(x^{2}) \end{array}$$
我们可以代入一个数进去。一般我们肯定想着代个看起来可爱的数字。
看看,这就是我等凡人与傅里叶这等神仙的区别。人家代入的是什么?没错,单位根!(不然我 TM 前面罗列一大堆单位根的性质干嘛)
$$F(x)=L(x^{2})+xR(x^{2})$$
$$F(\omega_{n}^{k})=L(\omega_{n}^{2k})+\omega_{n}^{k}R(\omega_{n}^{2k})$$
$$F(\omega_{n}^k)=L(\omega_{2\times \frac{1}{2}n}^{2k})+\omega_{n}^{k}R(\omega_{2\times \frac{1}{2}n}^{2k})$$
由公式 (2.6)
$$F(\omega_{n}^{k})=L(\omega_{\frac{1}{2}n}^{k})+\omega_{n}^{k}R(\omega_{\frac{1}{2}n}^{k}) \tag{3.1}$$
回到
$$F(x)=L(x^{2})+xR(x^{2})$$
此时我们代入 $\omega^{k+\frac{n}{2}}_{n}$
同理可得
$$F(\omega_{n}^{k+\frac{n}{2}})=L(\omega_{\frac{n}{2}}^{k})-R(\omega_{\frac{n}{2}}^{k}) \tag{3.2}$$
可以发现 (3.1) 和 (3.2) 只差了符号,也就是说只要知道了 $L(\omega_{\frac{n}{2}}^{k})$ 和 $R(\omega_{\frac{n}{2}}^{k})$ 我们就可以同时得到 $F(\omega_{n}^{k})$ 和 $F(\omega_{n}^{k+\frac{n}{2}})$。然后就递归求解。
这样我们就可以在 $\Theta(n\log_{2}n)$ 求取多项式的点值表示了。
算法名叫 DFT。
#include <cstdio>
#include <iostream>
#include <algorithm>
#include <cstring>
#include <queue>
#include <complex>
using namespace std;
const double PI = acos(-1);
const int MAXN = 1e6 + 3e5 + 5;
int n;
struct Complex {
double real;
double imag;
Complex(double t_real = 0, double t_imag = 0) { real = t_real, imag = t_imag; }
Complex operator + (Complex const& rhs) const {
return Complex(real + rhs.real, imag + rhs.imag);
}
Complex operator - (Complex const& rhs) const {
return Complex(real - rhs.real, imag - rhs.imag);
}
Complex operator * (Complex const& rhs) const {
return Complex(real * rhs.real - imag * rhs.imag, real * rhs.imag + imag * rhs.real);
}
void to_real(const double t_real) {
real = t_real;
}
void to_imag(const double t_imag) {
imag = t_imag;
}
double to_real() {
return real;
}
double to_imag() {
return imag;
}
} F[MAXN << 2], t[MAXN << 2];
void dft(Complex *f, int __n) {
if (__n == 1) return ;
Complex *L = f;
Complex *R = f + (__n >> 1);
for (int k = 0; k < __n; ++k) t[k] = f[k];
for (int k = 0; k < (__n >> 1); ++k) L[k] = t[k << 1], R[k] = t[k << 1 | 1];
dft(L, (__n >> 1));
dft(R, (__n >> 1));
Complex omega;
Complex now;
omega.to_real(cos(2 * PI / __n));
omega.to_imag(sin(2 * PI / __n));
now.to_real(1);
now.to_imag(0);
for (int k = 0; k < (__n >> 1); ++k) {
t[k] = L[k] + now * R[k];
t[k + (__n >> 1)] = L[k] - now * R[k];
now = now * omega;
}
for (int k = 0; k < __n; ++k) f[k] = t[k];
}
signed main() {
scanf("%d", &n);
int temp = n;
double x;
for (n = 1; n < temp; n <<= 1) ;
for (int i = 0; i < temp; ++i) scanf("%lf", &x), F[i].to_real(x), F[i].to_imag(0);
dft(F, n);
for (int i = 0; i < n; ++i) printf("(%.2lf %.2lf)\n", F[i].to_real(), F[i].to_imag());
return 0;
}
subarashi
但是我们现在求到的只是一堆没用的点值,还需要求到的点值表示还原成系数表示。
结论是把代入的 $\omega_{n}^k$ 换成 $\omega^{-k}$ 然后除以 $n$。
即 DFT 的逆运算 IDFT。
IDFT 的证明比较繁琐,涉及到分类讨论。由于我最近被数学作业的多答案讨论和智障珠的 60 种情况毒瘤了,故懒得给出证明。反正我相信看我 blog 的人人均会单位根反演
我们记 $\mathcal{F}(F(x))$ 是 $F(x)$ 的离散傅里叶变换/傅里叶变换,$\mathcal{F’}(F(x))$ 是 $F(x)$ 的逆离散傅里叶变换/傅里叶变换。
用看起来很高大上很 nb 的数学语言表示就是
记
$$G=\mathcal{F}(F(x))$$
则
$$
n\times f_{k}=\sum_{i=0}^{n-1}\omega_n^{-ki}g_{i}
$$
其中 $f_{i}$、$g_{i}$ 分别为 $F$、$G$ 的第 $i$ 项系数。
我们只需要改一下代码就好了。
#include <cstdio>
#include <iostream>
#include <algorithm>
#include <cstring>
#include <queue>
#include <complex>
using namespace std;
const double PI = acos(-1);
const int MAXN = 1e6 + 3e5 + 5;
int n, m;
struct Complex {
double real;
double imag;
Complex(double t_real = 0, double t_imag = 0) { real = t_real, imag = t_imag; }
Complex operator + (Complex const& rhs) const {
return Complex(real + rhs.real, imag + rhs.imag);
}
Complex operator - (Complex const& rhs) const {
return Complex(real - rhs.real, imag - rhs.imag);
}
Complex operator * (Complex const& rhs) const {
return Complex(real * rhs.real - imag * rhs.imag, real * rhs.imag + imag * rhs.real);
}
void to_real(const double t_real) {
real = t_real;
}
void to_imag(const double t_imag) {
imag = t_imag;
}
double to_real() {
return real;
}
double to_imag() {
return imag;
}
} A[MAXN << 2], B[MAXN << 2], t[MAXN << 2];
void dft(Complex *f, int __n, int flag) {
if (__n == 1) return ;
Complex *L = f;
Complex *R = f + (__n >> 1);
for (int k = 0; k < __n; ++k) t[k] = f[k];
for (int k = 0; k < (__n >> 1); ++k) L[k] = t[k << 1], R[k] = t[k << 1 | 1];
dft(L, (__n >> 1), flag);
dft(R, (__n >> 1), flag);
Complex omega;
Complex now;
omega.to_real(cos(2 * PI / __n));
omega.to_imag(sin(2 * PI / __n) * flag);
now.to_real(1);
now.to_imag(0);
for (int k = 0; k < (__n >> 1); ++k) {
t[k] = L[k] + now * R[k];
t[k + (__n >> 1)] = L[k] - now * R[k];
now = now * omega;
}
for (int k = 0; k < __n; ++k) f[k] = t[k];
}
signed main() {
scanf("%d %d", &n, &m);
double x;
for (int i = 0; i <= n; ++i) scanf("%lf", &x), A[i].to_real(x), A[i].to_imag(0);
for (int i = 0; i <= m; ++i) scanf("%lf", &x), B[i].to_real(x), B[i].to_imag(0);
for (m += n, n = 1; n <= m; n <<= 1) ;
dft(A, n, 1);
dft(B, n, 1);
for (int i = 0; i < n; ++i) A[i] = A[i] * B[i];
dft(A, n, -1);
for (int i = 0; i <= m; ++i) printf("%d ", (int)(A[i].real / n + 0.49));
return 0;
}
递归版常数过大,我们想想能不能把递归转为循环(迭代)。
这里给一个结论,给出一个序列。比如 $\texttt{0 1 2 3 4 5 6 7}$。
对其进行 DFT 后:$\texttt{0 4 2 6 1 5 3 7}$。多试几组可以发现 DFT 后每个位置数是原序列对应位置上的数的二进制反转后的结果。
$$\texttt{0 1 2 3 4 5 6 7}$$
$$\texttt{(000) (001) (010) (011) (100) (101) (110) (111)}$$
$$\texttt{(000) (100) (010) (110) (001) (101) (011) (111)}$$
$$\texttt{0 4 2 6 1 5 3 7}$$
证明也好证,留作思考吧。
然后我们就可以预处理出序列 DFT 后的位置,然后向上合并。就不用从上至下递归了。
具体来说,我们设 $rev_{i}$ 为数字 $i$ 的二进制翻转,$lim$ 为最多的二进制位数。
翻转操作相当于把当前数的二进制最后一位接到之前部分翻转的前面。
之前部分的翻转即 $rev_{i\operatorname{shr} 1}\operatorname{shr} 1$
其中 $\operatorname{shr}$ 相当于右移操作,$\operatorname{shl}$ 同理。
然后判一下最后一位,是 1 的话就让 $2^{lim-1}$ 对 $rev_{i\operatorname{shr} 1}\operatorname{shr} 1$ 按位与。因为 $2^{p}$ 的二进制位始终是 1 后面跟着 $p$ 个 0。
这里建议自己手推一下。
int lim = 0;
while ((1 << lim) < n) ++lim;
for (int i = 0; i < n; ++i) rev[i] = (rev[i >> 1] >> 1) | ((i & 1) << (lim - 1));
完整代码
#include <cstdio>
#include <iostream>
#include <algorithm>
#include <cstring>
#include <queue>
#include <complex>
using namespace std;
const double PI = acos(-1);
const int MAXN = 1e6 + 3e5 + 5;
int n, m;
struct Complex {
double real;
double imag;
Complex(double t_real = 0, double t_imag = 0) { real = t_real, imag = t_imag; }
Complex operator + (Complex const& rhs) const {
return Complex(real + rhs.real, imag + rhs.imag);
}
Complex operator - (Complex const& rhs) const {
return Complex(real - rhs.real, imag - rhs.imag);
}
Complex operator * (Complex const& rhs) const {
return Complex(real * rhs.real - imag * rhs.imag, real * rhs.imag + imag * rhs.real);
}
void to_real(const double t_real) {
real = t_real;
}
void to_imag(const double t_imag) {
imag = t_imag;
}
double to_real() {
return real;
}
double to_imag() {
return imag;
}
} A[MAXN << 2], B[MAXN << 2], t[MAXN << 2];
int rev[MAXN << 2];
void get_rev() {
int lim = 0;
while ((1 << lim) < n) ++lim;
for (int i = 0; i < n; ++i) rev[i] = (rev[i >> 1] >> 1) | ((i & 1) << (lim - 1));
}
void fft(Complex *f, int __n, int flag) {
for (int i = 0; i < n; ++i) if (i < rev[i]) swap(f[i], f[rev[i]]);
for (int mid = 1; mid < lim; mid <<= 1) {
Complex omega;
omega.to_real(cos(PI / mid));
omega.to_imag(sin(PI / mid) * flag);
for (int i = 0; i < n; i += (mid << 1)) {
Complex now;
now.to_real(1);
now.to_imag(0);
for (int j = 0; j < mid; ++j) {
Complex first = f[i + j];
Complex second = now * f[i + j + mid];
f[i + j] = first + second;
f[i + j + mid] = first - second;
now = now * omega;
}
}
}
}
signed main() {
scanf("%d %d", &n, &m);
double x;
for (int i = 0; i <= n; ++i) scanf("%lf", &x), A[i].to_real(x), A[i].to_imag(0);
for (int i = 0; i <= m; ++i) scanf("%lf", &x), B[i].to_real(x), B[i].to_imag(0);
for (m += n, n = 1; n <= m; n <<= 1) ;
get_rev();
fft(A, n, 1);
fft(B, n, 1);
for (int i = 0; i < n; ++i) A[i] = A[i] * B[i];
fft(A, n, -1);
for (int i = 0; i <= m; ++i) printf("%d ", (int)(A[i].real / n + 0.49));
return 0;
}
0 条评论