FFT 初步

Hexarhy

2021-03-29 20:25:39

Personal

# Introduction 快速傅里叶变换 (Fast Fourier Transform,FFT),一种支持在 $O(n\log n)$ 时间内计算两个 $n$ 次多项式乘法的算法。 **前置知识:** 复数与复平面,矩阵初步。 # Analysis 我们需要借助一些工具来帮助我们计算多项式。 **符号约定:** - $a/b$,表示 $a$ 除以 $b$ 向下取整。 - 为了区分,斜体的 $i$ 是一个数学变量,而正体的 $\rm i$ 表示虚数单位。 ## 多项式的表示 ### 系数表示法 即用系数表示一个多项式。 $$ F(x)=\sum_{i=0}^n a_ix^i \Leftrightarrow F(x)=\{a_0,a_1,\cdots,a_n\} $$ 显然,一个确定的多项式与系数构成双射。 ### 点值表示法 在函数 $F(x)$ 上取 $n+1$ 个横坐标不同的点来表示,也就是: $$ F(x)=\sum_{i=0}^n a_ix^i \Leftrightarrow F(x)=\{(x_0,F(x_0)),(x_1,F(x_1)),\cdots,(x_n,F(x_n))\} $$ 一个确定的多项式也与这 $n+1$ 个点构成双射。 证明:考虑待定系数法。用这 $n+1$ 个点可以组成一个 $n+1$ 元一次方程组。由于所有点的横坐标都互不相同,不存在等价或矛盾的方程,因此能解出唯一解,也就对应了唯一一个多项式。 ### 基本原理 有了先进便捷的工具,我们可以得到 FFT 的基本原理: - 系数表示 $\xrightarrow{\rm DFT} $ 点值表示 - 在点值上运算 - 点值表示 $\xrightarrow{\rm IDFT}$ 系数表示 当然,为了加快速度,我们要取特殊的横坐标,并用**分治**计算。 ## 复平面与单位根 ### 单位根的表示 对于同样的横坐标选取,用**点值表示**两个多项式 $F(x),G(x)$ 。那么 $H(x)=F(x)\cdot G(x)$ 也容易用点值表示: $$ H(x)=\{(x_0,F(x_0)G(x_0)),(x_1,F(x_1)G(x_1)),\cdots,(x_n,F(x_n)G(x_n))\} $$ 横坐标要怎么取才能快?$1,-1$ 的幂都很好算。但限制在实数里实在找不到 $n+1$ 个数这么多。联想到方程 $x^n=1$,我们取复数进行运算。 复平面上复数乘法的几何意义告诉我们,$1$ 的 $n$ 次方根即把单位圆等分成 $n$ 份,也就是说,对于第 $k(k=0,1,\cdots,n-1)$ 个根 $\omega_k$,根据复数的三角表示和欧拉公式,有: $$ \omega_k=\cos\dfrac{2\pi}{k}+\mathrm{i}\sin\dfrac{2\pi}{k}=e^\frac{2\pi\mathrm{i}}{k} $$ ### 单位根的性质 单位根有非常多性质,对于 $\forall n\in \mathbb{N}^+,k\in \mathbb Z$, - $\omega_{n}^n=1$ 证明:用三角表示法和复数乘法的几何意义即得证。 $$ \omega_{n}^n=\cos^n\dfrac{2\pi}{n}+\mathrm{i}\sin^n\dfrac{2\pi}{n}=\cos{2\pi}+\mathrm{i}\sin2\pi=1 $$ - $\omega_{2n}^{2k}=\omega_n^k$ 证明:同理。 $$ \begin{aligned} \omega_{2n}^{2k} &=\cos^{2k}\dfrac{2\pi}{2n}+\mathrm{i}\sin^{2k}\dfrac{2\pi}{2n}\\ &=\cos^k \dfrac{2\cdot 2\pi}{2n}+\mathrm{i}\sin^k\dfrac{2\cdot 2\pi}{2n}\\ &=\cos^k\dfrac{2\pi}{n}+\mathrm{i}\sin^k\dfrac{2\pi}{n}\\ &=\omega_n^k \end{aligned} $$ - $\omega_{2n}^{k+n}=-\omega_{2n}^k$ 证明:同理,中间用到了诱导公式。 $$ \begin{aligned} \omega_{2n}^{k+n}&=\cos^{k+n} \dfrac{2\pi}{2n}+\mathrm{i}\sin^{n+k}\dfrac{2\pi}{2n}\\ &=\cos \dfrac{(k+n)2\pi}{2n}+\mathrm{i}\sin\dfrac{(k+n)2\pi}{2n}\\ &=\cos\left(\dfrac{2k\pi}{2n}+\pi\right)+\mathrm{i}\sin\left(\dfrac{2k\pi}{2n}+\pi\right)\\ &=-\cos\dfrac{2k\pi}{2n}-\mathrm{i}\sin\dfrac{2k\pi}{2n}\\ &=-\omega_{2n}^k \end{aligned} $$ - $\omega_{n}^{k+n}=\omega_n^k$ 证明:$\omega_{n}^{k+n}=\omega_{n}^{k+n/2+n/2}=-\omega_n^{k+n/2}=\omega_n^k$。当然用三角表示也容易证明。 ## DFT 我们取横坐标 $x=\omega_n^k$ 来转化成点值表示。分治思想体现在将多项式按次数奇偶处理。 举例来说,对于 $F(x)=a_0+a_1x+a_2x^2+a_3x^3+a_4x^4+a_5x^5$,转化为: $$F(x)=(a_0+a_2x^2+a_4x^4)+(a_1x+a_3x^3+a_5x^5)$$ $$=(a_0+a_2x^2+a_4x^4)+x(a_1+a_3x^2+a_5x^4)$$ $$=G(x^2)+xH(x^2)$$ $$G(x)=a_0+a_2x+a_4x^2$$ $$H(x)=a_{1}+a_3x+a_5x^2$$ 利用单位根的性质得到: $$ \begin{aligned} F(\omega_{n}^{k})&=G(\omega_n^{2k})+\omega_{n}^{k}H(\omega_n^{2k})\\ &=G(\omega_{n/2}^k)+\omega_{n}^kH(\omega_{n/2}^k)\\ F(\omega_{n}^{k+n/2})&=G(\omega_n^{2k+n})+\omega_{n}^{k+n/2}H(\omega_n^{2k+n})\\ &=G(\omega_{n}^{2k})-\omega_{n}^kH(\omega_{n}^{2k})\\ &=G(\omega_{n/2}^k)-\omega_{n}^kH(\omega_{n/2}^{k}) \end{aligned} $$ 递归处理即可。每次递归时间复杂度为 $O(\log n)$。 但是递归处理要求多项式次数必须为 $2^m-1$。所以就在多项式后面用系数 $0$ 补齐即可。 代入 $2^m$ 个单位根 $\omega_n^0,\omega_n^1,\cdots,\omega_n^{n-1}(n=2^m)$ 即可得到多项式的点值表示。 显然,DFT 的时间复杂度为 $O(n\log n)$。 ### 位逆序置换 依然按照奇偶次数分治。我们并不需要像 DFT 那样实际操作,只需要知道递归后的系数对应的位置即可。 以 $7$ 次多项式为例: - 初始:$\{x_0,x_1,x_2,x_3,x_4,x_5,x_6,x_7,x_8\}$ - 第一次:$\{x_0,x_2,x_2,x_4\},\{x_1,x_3,x_5,x_7\}$ - 第二次:$\{x_0,x_4\},\{x_2,x_6\},\{x_1,x_5\},\{x_3,x_7\}$ - 第三次:$\{x_0\},\{x_4\},\{x_2\},\{x_6\},\{x_1\},\{x_5\},\{x_3\},\{x_7\}$ 找规律过程略去,毕竟我们只关心结论。 每个数初始下标用二进制表示,翻转 (reverse),得到的数字即为最终的下标。这个称为**位逆序置换**。 直接根据这个规律模拟,时间复杂度 $O(n\log n)$。 事实上,还有一种更优的实现方式,采用了递推。设 $R(x)$ 为二进制下 $x$ 翻转后的数。记 $k$ 为二进制下 $x$ 的长度。 已知 $R(0)=0$。从小到大递推,已知 $R(x/2)$ 的值,将 $x$ 右移一位,取反,右移一位,就得到了二进制下 $x$ **个位之外**的翻转结果。例如:$x=4=(100)_2$ 时,翻转后的结果是 $(001)_2=1$。根据刚才的操作演示一遍: $$ (100)_2\xrightarrow{\rm Move\ Right}(010)_2\xrightarrow{\rm Reverse}(010)_2\xrightarrow{\rm Move\ Right}(001)_2\xrightarrow{\rm Calculate}(001)_2 $$ 对于个位,如果是 $0$ ,翻转之后最高位为 $0$;如果为 $1$,则最高位为 $1$,等价于加上了 $2^{k-1}$。综上,得到递推式: $$ R(x)=\begin{cases}R(x/2)/2&,2\mid x\\ R(x/2)/2+2^{k-1}&,2\nmid x \end{cases} $$ 时间复杂度 $O(n)$。 ## IDFT 将点值转化为系数,有多种理解方式。个人觉得最好理解的是用矩阵运算。 先下结论:**DFT 中 $\omega_{n}^1$ 换成 $\omega_n^{-1}$,再除以 $n$**。 对于所取的 $n$ 个单位根,可以得到 $n$ 个方程,其中第 $k(k=0,1,2,\cdots,n-1)$ 条为: $$ F(\omega_n^k)=\sum_{i=0}^{n-1} a_i(\omega_{n}^{k})^i $$ 咦,这个形式好熟悉啊。就是矩阵运算!实际上 DFT 本身是线性变换,所以就有了以下理解方式: $$ \left[\begin{array}{c} F(\omega_n^0) \\ F(\omega_n^1) \\ F(\omega_n^2) \\ F(\omega_n^3) \\ \vdots \\ F(\omega_n^{n-1}) \end{array}\right]=\left[\begin{array}{cccccc} (\omega_n^0)^0 & (\omega_n^0)^1 & (\omega_n^0)^2 & \omega_n^0 & \cdots & (\omega_n^0)^{n-1} \\ (\omega_n^1)^0 & (\omega_{n}^{1})^1 & (\omega_{n}^1)^{2} & (\omega_{n}^1)^{3} & \cdots & (\omega_{n}^{1})^{n-1} \\ (\omega_n^2)^0 & (\omega_{n}^{2})^1 & (\omega_{n}^{2})^2 & (\omega_{n}^{2})^3 & \cdots & (\omega_{n}^{2})^{n-1} \\ (\omega_n^3)^0 & (\omega_{n}^{3})^1 & (\omega_{n}^{3})^2 & (\omega_{n}^{3})^3 & \cdots & (\omega_{n}^3)^{n-1} \\ \vdots & \vdots & \vdots & \vdots & \ddots & \vdots \\ (\omega_n^{n-1})^0 & (\omega_{n}^{n-1})^1 & (\omega_{n}^{n-1})^2 & (\omega_{n}^{n-1})^3 & \cdots & (\omega_{n}^{n-1})^{n-1} \end{array}\right]\left[\begin{array}{c} a_{0} \\ a_{1} \\ a_{2} \\ a_{3} \\ \vdots \\ a_{n-1} \end{array}\right] $$ 现在已经知道了左边的矩阵 $A$ 和中间的左乘的矩阵 $B$,要求系数也就不难。也就是将 $A$ 乘上 $B$ 的逆矩阵。 至于逆矩阵怎么求,一种可行的方法采用了拉格朗日插值,参考[这里](https://www.zhihu.com/question/309243881/answer/575306025)。 我们要求的逆矩阵就是 $B$ 中的每一项取倒数再乘上 $\dfrac{1}{n}$。至于为什么是正确的,代入计算验证即可。 取倒数相当于指数乘上 $-1$,因此我们可以在 DFT 中传参数,$1$ 为 DFT,$-1$ 为 IDFT 来简洁地实现。 # Code 题目链接:[P3803 【模板】多项式乘法(FFT)](https://www.luogu.com.cn/problem/P3803)。 [评测记录](https://www.luogu.com.cn/record/51595951),看起来常数不算太大(雾)。 ```cpp typedef const int cint; typedef const double cdb; cint MAXN=2e6+5; int n,m; cdb PI=acos(-1); cdb PI2=2*PI; int rev[MAXN<<1]; struct complex //STL 其实有这个库的,但众所周知常数大(吧 { double a,b; complex(){} complex(cdb& a,cdb& b):a(a),b(b){} complex operator+(const complex& x)const {return complex(a+x.a,b+x.b);} complex operator-(const complex& x)const {return complex(a-x.a,b-x.b);} complex operator*(const complex& x)const {return complex(a*x.a-b*x.b,a*x.b+b*x.a);} }f[MAXN<<1],g[MAXN<<1]; void FFT(complex *f,cint& Len,cint& inv) { if(Len==1) return; cint len=Len/2; FFT(f,len,inv); FFT(f+len,len,inv); const complex tg(cos(PI2/Len),sin(PI2/Len)*inv); complex b(1,0); for(int i=0;i<len;i++) { const complex& t=b*f[i+len]; f[i+len]=f[i]-t; f[i]=f[i]+t; b=b*tg; } } int main() { read(n,m); for(int i=0;i<=n;i++) read(f[i].a); for(int i=0;i<=m;i++) read(g[i].a); for(m+=n,n=1;n<=m;n<<=1); for(int i=0;i<n;i++) rev[i]=(rev[i>>1]>>1)|((i&1)?(n>>1):0); for(int i=0;i<n;i++) if(i<rev[i]) swap(f[i],f[rev[i]]); for(int i=0;i<n;i++) if(i<rev[i]) swap(g[i],g[rev[i]]); FFT(f,n,1); FFT(g,n,1); for(int i=0;i<n;i++) f[i]=f[i]*g[i]; for(int i=0;i<n;i++) if(i<rev[i]) swap(f[i],f[rev[i]]); FFT(f,n,-1); for(int i=0;i<=m;i++) printf("%d ",int(f[i].a/n+0.5)); printf("\n"); return 0; } ``` # Optimization 实际上 FFT 有很多种写法和优化方法,有些常数差异还比较大。 # Reference - [OI-Wiki](https://oi-wiki.org/math/poly/fft/)。 - [command_block 的博客。](https://www.luogu.com.cn/blog/command-block/fft-xue-xi-bi-ji)