OI笔记 | 快速傅里叶变换(FFT)

模板题:

洛谷 P3803

洛谷 P1919

本文大部分思路来源于 Reducible 的视频

推荐观看:形象展示傅里叶变换 - 3Blue1Brown 的视频

FFT 的概念和作用

快速傅立叶变换(Fast Fourier Transform,FFT)可以在 $O(n\log n)$ 的时间内解决多项式乘法问题,在 OI 中,是用来操纵生成函数,优化效率的重要算法。

当然,FFT 在其他领域也有广泛应用,甚至与核试验的检测有关,不过那是另外的事了。

如果要完全掌握 FFT 的思想与正确性证明,你可能需要熟练掌握线性代数、复数、积分等前置知识;又因为实际上傅里叶变换(Fourier Transform)是一种分析信号的方法,你可能需要了解时域与频域的转换等知识。

其实我也没有熟练掌握上述内容。然而如果只考虑它在 OI 中的应用,我们大可从多项式乘法出发来了解 FFT 的思想。

从多项式乘法谈起

我们最熟悉的表示多项式的方法是 系数表示

\[A(x)=p_0 + p_1 x +p_2 x^2+ \dots + p_{n-1} x^{n-1} \to A(x)= \left[ p_0,p_1,\dots,p_{n-1} \right]\]

用系数表示进行的多项式乘法为 $O(n^2)$ 的,需要把两个多项式的每个系数都相乘。

显然一个 $n$ 次多项式可以被 $t$ 个点确定,当且仅当 $t\ge n + 1$。这里不证明这个结论。我们把用点来表示多项式的方法称作 点值表示

\[A(x)=[(x_0,A(x_0)),(x_1,A(x_1)),\dots(x_n,A(x_n))]\]

用点值表示进行的多项式乘法为 $O(n)$ 的,只需要对相应的点值做乘法。

事实上,FFT 能做的就是把 系数表示点值表示 的相互转换在 $O(n\log n)$ 内完成。

分割成偶函数进行分治

考虑什么情况下,我们已经知道一个点的点值 $(x_0,A(x_0))$,就能在 $O(1)$ 内得到另一个点值。可以想到如果这个多项式是偶函数,那么可以得出 $(-x_0,A(x_0))$ 是另一个点值。于是考虑把多项式分成两个有奇偶性的部分:

\[A(x)=(p_0 + p_2x^2 + p_4x^4 + \dots + p_{n-2}x^{n-2}) +\\ x(p_1 + p_3x ^2 + p_5x^4+\dots +p_{n-1}x^{n-2})\]

这里假定 $n$ 是 $2$ 的幂,这么取值方便进行分治。

不妨设 $A_e(x)=(p_0+p_2x+p_4x^2+\dots + p_{n-2}x^{n/2-1})$,$A_o(x)=(p_1 + p_3x+p_5x^2+\dots + p_{n-1}x^{n/2-1})$,则有:

\[A(x)=A_e(x^2)+xA_o(x^2)\]

并且这里的 $A_e(x^2), A_o(x^2)$ 都是偶函数,这个性质很妙。

而且上面这个式子说明我们如果可以递归地用 FFT 处理完 $A_e$ 和 $A_o$ 的点值表示,就可以在 $O(n)$ 的时间内合并答案,这样不就把时间复杂度降到了 $O(n\log n)$ 了吗。

用单位根取点

但是还有一个问题,就是你会发现我们括号里的是 $x^2$,如果 $x$ 是实数,则有 $x^2\ge 0$,那么我们递归完一层就递归不下去了,因为我们的设想是通过偶函数的性质取 $x_0$ 和 $-x_0$ 这一个点对来分治,但是 $-x_0\le 0$,不能成为另一个数的平方。

所以我们考虑使用复数,而且取点方式很巧妙,我们直接取 $x^n=1$ 的 $n$ 个单位根作为我们的点值。为什么这样可行呢?

取 $\omega=e^{\frac{2\pi i}{n}}=\cos \frac{2\pi}{n}+i \sin{\frac{2\pi}{n}}$。据复平面的知识,$n$ 次单位根把单位圆 $n$ 等分,容易知道 $\omega ^k(k\in {0,1,\dots, n-1})$ 就是逆时针旋转得到的第 $k$ 个根。

然后单位根有 $3$ 个巧妙的性质,根据复平面的几何直观很好理解:

\[\begin{aligned} \omega_n^n&=1\\ \omega_n^k&=\omega_{2n}^{2k}\\ \omega_{2n}^{k+n}&=-\omega_{2n}^k\\ \end{aligned}\]

那么当你递归地处理完 $P_e = [A_e(\omega^0),A_e(\omega^2),A_e(\omega^4),\dots,A_e(\omega^{n-2})]$,$P_o = [A_o(\omega^0),A_o(\omega^2),A_o(\omega^4),\dots,A_o(\omega^{n-2})]$ 后,可以顺利的用上面的公式合并出 $A(x)=P_e(x)+\omega^x P_o(x)$,$A(x + n/2)=P_e(x)-\omega^x P_o(x)$(这是因为单位根的性质, $\omega^{x+n/2}=-\omega^{x}$)。

算法的时间复杂度 $O(n\log n)$。

快速傅里叶逆变换

傅里叶逆变换可以用傅里叶变换表示。对此本文不想提供理解,因为我也不是很理解。

有没有大佬来告诉我一种简单的理解方式!

总之,代码实现上,你把 FFT 中的 $\omega$ 从 $e^{\frac{2\pi i}{n}}$ 改成 $e^{\frac{-2\pi i}{n}}$ ,就变成了 IFFT。很神奇吧。

代码实现

先手写一个复数。

struct cp {
	double x, y;
	cp(double _x = 0, double _y = 0) : x(_x), y(_y) {}
	cp operator + (const cp &t) const {return cp(x + t.x, y + t.y);}
	cp operator - (const cp &t) const {return cp(x - t.x, y - t.y);}
	cp operator * (const cp &t) const {return cp(x * t.x - y * t.y, x * t.y + y * t.x);}
};

按照上面的思路,容易写出递归版本:

void FFT(cp *P, int x, int type) {
    if(x == 1)  return;
    cp Pe[x], Po[x];
    for(int i = 0; i <= x; i += 2)
        Pe[i >> 1] = P[i], Po[i >> 1] = P[i + 1];
    FFT(Pe, x >> 1, type);
    FFT(Po, x >> 1, type);
    cp omega = cp(cos(PI * 2.0 / x), type * sin(PI * 2.0 / x)), now = cp(1, 0);
	for(int i = 0; i < (x >> 1); i++) {
        cp u = Pe[i], v = now * Po[i];
        P[i] = u + v;
        P[i + (x >> 1)] = u - v;
        now = now * omega;
    }
}

当然也有基于位逆序置换(蝴蝶变换)的迭代版本,不太好理解,不过效率会高一点。这里直接给出代码。


void pre(){
    while(len <= n + m)
        len <<= 1, k++;
    for(int i = 0; i <= len; i++)
        rev[i] = (rev[i >> 1] >> 1) | ((i & 1) << (k - 1));
}

void FFT(cp *P, int type) {
    pre();
    for(int i = 0; i < len; i++)
        if(i < rev[i])  swap(P[i], P[rev[i]]);
    for(int i = 1; i < len; i <<= 1) {
        cp omega(cos(PI / i), type * sin(PI / i));
        for(int k = 0; k < len; k += (i << 1)) {
            cp now(1, 0);
            for(int l = 0; l < i; l++) {
                cp u = P[k + l], v = now * P[k + i + l];
                P[k + l] = u + v;
                P[k + i + l] = u - v;
                now = now * omega;
            }
        }
    }
}

Joy © 2023 Powered by Jekyll and Theme by solid

今日诗词API Valine 评论管理 Github