嘘~ 正在从服务器偷取页面 . . .

快速傅里叶变换 FFT


快速傅里叶变换 FFT

模板题P3803 【模板】多项式乘法 (FFT)

题意

给定一个 $n$ 次多项式 $F(x)$,和一个 $m$ 次多项式 $G(x)$。

请求出 $F(x)$ 和 $G(x)$ 的加法卷积,即

输入格式

第一行两个整数 $n,m$。

接下来一行 $n+1$ 个数字,从低到高表示 $F(x)$ 的系数。

接下来一行 $m+1$ 个数字,从低到高表示 $G(x)$ 的系数。

输出格式

一行 $n+m+1$ 个数字,从低到高表示 $F(x) \cdot G(x)$ 的系数。

数据范围

保证输入中的系数大于等于 $0$ 且小于等于 $9$。

对于 $100\%$ 的数据:$1 \le n, m \leq {10}^6$。

快速傅里叶变换 (FFT) 是一种用于快速计算 DFT 的算法。

在此之前,我们需要先了解什么是 DFT 。不过理论上来讲,OI 只要记住 FFT 板子就行了。


傅里叶变换

本部分仅作了解即可,OI 完全不会涉及。

傅里叶变换(Fourier Transform)是一种分析信号的方法,它可分析信号的成分,也可用这些成分合成信号。许多波形可作为信号的成分,傅里叶变换用正弦波作为信号的成分。

设 $f(t)$ 是关于时间 $t$ 的函数,则傅里叶变换可以检测频率 $\omega$ 的周期在 $f(t)$ 出现的程度:

它的逆变换为

逆变换的形式与正变换非常类似,分母 $2\pi$ 恰好是指数函数的周期。

傅里叶变换相当于将时域的函数与周期为 $2\pi$ 的复指数函数进行连续的内积,逆变换仍旧为一个内积。

傅里叶变换有相应的卷积定理,可以将时域的卷积转化为频域的乘积,同样也能逆转化。


离散傅里叶变换 DFT

离散傅里叶变换(Discrete Fourier transform, DFT)是傅里叶变换在时域和频域上都呈离散的形式,将信号的时域采样变换为其 DTFT(discrete-time Fourier transform)的频域采样。

也就是说,傅里叶变换是积分形式的连续的函数内积,离散傅里叶变换是求和形式的内积

设 $\left\{x_n\right\}_{n=0}^{N-1}$ 是某一满足有限性条件的序列,则它的离散傅里叶变换 (DFT) 为

通常以 $\mathcal{F}$ 表示这一变换,即 $\hat x = \mathcal{F}x$ 。

类似与积分形式,它的逆离散傅里叶变换 (IDFT)

记为 $x = \mathcal{F}^{-1}\hat{x}$ 。

实际上,DFT 和 IDFT 变换式中和式前面的归一化系数并不重要。

在上面的定义中,DFT 和 IDFT 前的系数分别为 $1$ 和 $\frac{1}{N}$ 。有时我们会将这两个系数都改为 $\frac{1}{\sqrt{N}}$ 。


离散傅里叶变换仍旧是时域到频域的变换。而其求和形式的特殊性,使其可以有其他的解释方法

如果把序列 $x_n$ 看作多项式 $F(x)$ 的 $x^n$ 项系数。

则计算得到的 $X_k$ 恰好是多项式 $F(x)$ 代入单位根 $\mathrm{e}^{-\frac{2k \pi\mathrm{i}}{n}}$ 的点值 $F(\mathrm{e}^{-\frac{2k \pi\mathrm{i}}{n}})$ 。

这便构成了卷积定理的另一种解释方法,即对多项式进行特殊的插值操作

补充知识:

插值 (Interpolation),是一种通过已知的、离散的数据点,在范围内推求新数据点的过程或方法。

$n$ 次单位根是 $n$ 次幂为 $1$ 的复数,且 $n$ 次的单位根恰好有 $n$ 个,即 $z_k = \mathrm{e}^{\frac{2k\pi\mathrm{i}}{n}}\,(k=0,1,\cdots,n-1)$ 。

而 DFT 恰好是多项式在单位根处进行插值。

例如计算

可以发现,这道题计算的组合数的 $k$ 都是模 $4$ 余 $3$ 的。

考虑定义函数 $F(x)$ 为

代入四次单位根 $\mathrm{i}$ 可得

于是下面的求和恰好可以把各项消掉

因此答案为

这道题在单位根处插值,恰好构成 DFT 。

矩阵公式

由于离散傅立叶变换是一个 线性 算子,所以它可以用矩阵乘法来描述。

在矩阵表示法中,离散傅立叶变换表示如下:

其中 $\alpha = \mathrm{e}^{-\frac{2k \pi\mathrm{i}}{n}}$ 。


快速傅里叶变换 FFT

快速傅立叶变换 (Fast Fourier Transform, FFT) 是一种高效实现 DFT 的算法。

它对傅里叶变换的理论并没有新的发现,

但是对于在计算机系统或者说数字系统中应用离散傅立叶变换,可以说是进了一大步。

快速数论变换(NTT)是快速傅里叶变换(FFT)在数论基础上的实现,本文不会讲述。

在 1965 年,Cooley 和 Tukey 发表了快速傅里叶变换算法。事实上 FFT 早在这之前就被发现过了,但是在当时现代计算机并未问世,人们没有意识到 FFT 的重要性。一些调查者认为 FFT 是由 Runge 和 König 在 1924 年发现的。但事实上高斯早在 1805 年就发明了这个算法,但一直没有发表。

分治法实现(递归)

FFT 算法的基本思想是分治。就 DFT 来说,它分治地来求当 $x = \omega_n^{k}$ 的时候 $f(x)$ 的值。

这里 $\omega_n^{k}=\mathrm{e}^{\frac{2 k\pi\mathrm{i}}{n}}=\cos \frac{2 k\pi}{n}+\mathrm{i} \sin \frac{2 k\pi}{n}$ 即为 $n$ 次单位根的第 $k$ 个。

基-2 FFT 的分治思想体现在将多项式分为奇次项和偶次项处理。

举个例子,对于 $8$ 次整系数多项式:(FFT 强制钦定 $n=2^m$ 为多项式的次数,为了方便处理)

把它按奇偶分成两组,并提一个 $x$ ,则

不妨记

利用偶数次单位根的性质 $\omega_n^i=-\omega_n^{i+n / 2}$ ,以及 $F_e(x^2),F_o(x^2)$ 为偶函数,

可知复平面上 $\omega_n^{i}$ 和 $\omega_n^{i+n/2}$ 的 $F_e(x^2)$ 和 $F_o(x^2)$ 对应的值相等,得到

因此我们求出了 $F_e(\omega_{n/2}^{k})$ 和 $F_o(\omega_{n/2}^{k})$ 后,就可以同时求出 $F\left(\omega_n^k\right)$ 和 $F(\omega_n^{k+n / 2})$ 。

于是对 $F_e$ 和 $F_o$ 分别递归即可。 时间复杂度 $\mathcal{O}(n \log n)$ 。

但是不放代码了,因为这个实现方式是过不了 $10^6$ 的,常数太大了。

倍增法实现(迭代)

所谓迭代,指的是将原来递归的“拆分”改为了合并答案。

依然以 $8$ 次多项式为例,模拟拆分的过程:

结论:可以发现,每个位置就是下标的二进制左右翻转一下,比如 $x_1$ 是 $\mathtt{001}$ ,翻转一下是 $\mathtt{100}$ ,即 $4$ 。

这个变换被称作位逆序置换,证明略。位逆序置换可以做到 $\mathcal{O}(n)$ 从小到大递推实现。

记 $L = 2^k$ ,其中 $k$ 表示二进制数的长度,并设 $R(x)$ 表示翻转后的数(高位补 $0$ )。显然 $R(0) = (0)_2$ 。

考虑递推。在求 $R(x)$ 时,显然 $R\left(\left\lfloor\frac{x}{2}\right\rfloor\right)$ 是已知的。

因此我们把 $x$ 右移一位后,翻转,再右移一位,就得到了 $x$ 除了二进制下个位以外其他位的翻转结果

考虑个位的翻转结果。如果个位是 $0$ ,翻转后最高位还是 $0$ ,反之需要加上 $\frac{L}{2} = 2^{k-1}$ ,则

这意味着我们把原来 $\mathcal{O}(n\log n)$ 的拆分转变为了 $O(n)$ 的翻转。

实际上总时间复杂度还是 $\mathcal{O}(n\log n)$ ,但是我们却省去了大量常数。

另外还有个蝶形运算优化,和位逆序置换类似,不过我没写这种优化的代码,想看可以看参考文献[1]。

等会儿,这还没结束呢,我们通过 FFT 计算出了原序列的点值形式,我们还要把它转回系数呢


快速傅里叶逆变换 IFFT

傅里叶逆变换可以用傅里叶变换表示。对此我们有两种理解方式。

1. 线性代数角度

IDFT(傅里叶反变换)的作用,是把目标多项式的点值形式转换成系数形式。

而 DFT 本身是个线性变换,可以理解为将目标多项式当作向量,左乘一个矩阵得到变换后的向量,以模拟把单位复根代入多项式的过程:

而我们已经得到左侧的结果了,现在要求最右边的。显然这是个矩阵求逆的问题。

由于这个矩阵的元素非常特殊,它的逆矩阵也有特殊的性质:

该矩阵每一项取倒数,再除以变换的长度 $n$ ,就可以得到它的逆矩阵。这里不作证明了。那么

好吧,我知道你们可能也不是很会线性代数,所以我们还是讲第二种理解方式吧。

2. 单位根的周期性

利用单位根的周期性同样可以理解 IDFT 与 DFT 之间的转换关系。

考虑构造法。我们已知 $y_i=f\left(\omega_n^i\right),~i = 0,1,\cdots,n-1$ ,求 $\{a_0,a_1,\cdots,a_{n-1}\}$ ,构造多项式如下:

相当于把 $\left\{y_0, y_1, y_2, \cdots, y_{n-1}\right\}$ 当作 $A$ 的系数表示法。

设 $b_i = \omega_n^{-i}$ (注意不是 $\mathrm{i}$ ),则 $A$ 在 $x = b_0,b_1,\cdots,b_{n-1}$ 处的点值表示法为 $\{A(b_0),A(b_1),\cdots,A(b_{n-1})\}$ 。

对 $A(x)$ 的定义式做一下变换,可以将 $A(b_k)$ 表示为

记 $S\left(\omega_n^a\right)=\sum_{i=0}^{n-1}\left(\omega_n^a\right)^i$

当 $a=0\pmod{n}$ 时,$S(\omega_n^{a}) = n$ 。

当 $a \ne 0 \pmod{n}$ 时,考虑错位相减

代回原式可得

也就是说,给定点 $b_i = \omega_n^{-i}$ ,则 $A$ 的点值表示法为

因此我们取其单位根为其倒数,对 $\left\{y_0, y_1, y_2, \cdots, y_{n-1}\right\}$ 跑一遍 FFT ,然后除以 $n$ 即可得到 $f(x)$ 的系数表示。


代码实现

至此,我们终于讲完了 FFT 算法的全部,放张思维导图

时间复杂度 $\mathcal{O}(n \log n)$

代码:

#include <bits/stdc++.h>
using namespace std;
// #define int long long
// #define INF 0x3f3f3f3f3f3f3f3f
typedef complex<double> Complex;
void up(int &x,int y) { x < y ? x = y : 0; }
void down(int &x,int y) { x > y ? x = y : 0; }
#define N ((int)(1e7 + 15))

const double pi = acos(-1.0);
Complex a[N], b[N];
int l, r[N], limit = 1;
void FFT(Complex *A, int type)
{
    for(int i = 0; i < limit; i++) if(i < r[i]) swap(A[i], A[r[i]]);
    for(int mid = 1; mid < limit; mid *= 2)
    {
        Complex Wn(cos(pi / mid), type * sin(pi / mid));
        for(int j = 0; j < limit; j += (mid * 2))
        {
            Complex w(1, 0);
            for(int k = 0; k < mid; k++, w = w * Wn)
            {
                Complex x = A[j + k], y = w * A[j + k + mid];
                A[j + k] = x + y; A[j + k + mid] = x - y;
            }
        }
    }
}
signed main()
{
    ios::sync_with_stdio(0);
    cin.tie(0);cout.tie(0);
    // freopen("check.in","r",stdin);
    // freopen("check.out","w",stdout);
    int n, m; cin >> n >> m;
    for(int i = 0, x; i <= n; i++) cin >> x, a[i] = x;
    for(int i = 0, x; i <= m; i++) cin >> x, b[i] = x;
    for(; limit <= n + m; l++, limit *= 2);
    for(int i = 0; i < limit; i++) r[i] = (r[i >> 1] >> 1) | ((i & 1) << (l - 1));
    FFT(a, 1); FFT(b, 1);
    for(int i = 0; i < limit; i++) a[i] = a[i] * b[i];
    FFT(a, -1);
    for(int i = 0; i <= n + m; i++)
        cout << (int)(a[i].real() / limit + 0.5) << " \n"[i == n + m];
    return 0;
}

参考文献

[1] https://oi-wiki.org/math/poly/fft/

[2] https://www.luogu.com.cn/blog/fusu2333/solution-p3803

[3] https://www.luogu.com.cn/blog/attack/solution-p3803


文章作者: q779
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-ND 4.0 许可协议。转载请注明来源 q779 !
评论
  目录