浅谈FFT--从DFT到*CZT,及一些技巧
Piwry
2020-01-17 07:28:53
~~本文为日报特供版QAQ~~
考虑到前面已经有人写过FFT了,所以部分FFT的基础内容**不会**出现在本篇,如基本的公式,NTT(它仅涉及单位复根循环的性质)等。
本文主要是讲解一些竞赛中会用到的围绕着FFT算法的一些内容,如DFT过程的几何直观、卷积和\*CZT等,另外,最后也会介绍一些冷门却好用的tricks。
之前日报的FFT可以看[这里](https://www.luogu.com.cn/blog/command-block/fft-xue-xi-bi-ji)
如果是FFT入门建议看下我之前的[文章](https://www.luogu.com.cn/blog/105254/ru-he-kuai-su-li-xie-fftifft)(有NTT)(~~**排版警告QWQ**~~)(另外这篇文章我没太大精力校对,所以其中的小漏洞可能较多。但**对于FFT实现的部分**还是很准确的)
---
## 目录
1.DFT简介
2.DFT/IDFT的几何直观
3.卷积初步
4.\*CZT
5.卷积与多项式
6.\*卷积的意义浅谈
7.一些tricks
---
## 1. DFT简介
FT,即 **傅里叶变换**,在方式上,它实质就是·是用三角函数拟近函数。傅里叶变换在不同领域有着不同的意义,但它通常用来**便捷地计算卷积**。
不过连续的傅里叶变换显然不是计算机能够处理的。在此基础上,就有了 **离散傅里叶变换** (此处指变换前后均离散),即 **DFT**。
实际上,DFT 就是 Z变换 的一种,或者说 **Z变换 其实就是 DFT 的拓展**。具体关于 Z变换的内容会在 第四节 中讲到。
.
$F[k]=\sum\limits_{n=0}^{N-1}x[n]e^{-i\frac {2\pi} N nk}$
$x[n]=\frac 1 N \sum\limits_{k=0}^{N-1}F[k]e^{i\frac {2\pi} N nk}$
这即是DFT的式子。
**注意其中$-i$与$i$其实是不分前后的**。这和其几何直观息息相关,我们在下节马上就会讲到。
## 2. DFT/IDFT的几何直观
本节主要是尝试建立DFT/IDFT的一种几何直观。
.
首先,自不用说是单位根。设单位复根$\omega$满足$\omega^N=1$,且这里$N$是指满足式子的最小正整数。
![单位复根](https://cdn.luogu.com.cn/upload/image_hosting/ogvhl5x6.png)
由复数的运算法则(幅角相加模相乘),我们其实可以将 与$\omega$的积 视为 **绕原点旋转$\frac {2\pi} N$** ;
同时,我们可以发现由不同次数的单位根组成的点是**在单位圆上平均分布的。**
注意,这里的**旋转**和**平均**的思想在下文十分重要。
.
接下来我们分析式子的几何意义。
观察式子(已用单位复根替换):
$F[k]=\sum\limits_{n=0}^{N-1}x[n]\omega^{nk}$
$x[n]=\frac 1 N\sum\limits_{k=0}^{N-1}F[k]\omega^{-nk}$
DFT时,对于所有的$x[n]$,变换后的每个分别点**绕原点旋转了$N$次,每次都是第一个角度的倍数**。这即是DFT的过程。
我们可以画出它们:
![旋转](https://cdn.luogu.com.cn/upload/image_hosting/9l5t6008.png)
可以发现:
1.部分点所在的半径重叠
2.对于$x[n]$项,最长路径为$n$圈,每个点旋转的角度是$w^n$的倍数,且它们是在路径上**平均分布**的。
[1]即是我们FFT优化的根本(但这和本节没关系);
[2]是一个很有趣的性质,它是由遍历单位复根次数导来的,**它和IDFT的几何直观息息相关**。
我们先设设$P[n]=\sum\limits_{k=0}^{N-1}x[n]\omega^{nk}$
发现,若求$\sum\limits_{k=0}^{N-1}P[k]$,**其和为0!**(想像一下,每个“向量”都有一个相反的“向量”与它相互抵消)
但是,还有个唯一的特殊情况:若$\omega$指数的乘积式子中有一个数为$0$,这个点就“**不会旋转**”:它**旋转的第一个角度为$0$,增倍后仍为$0$**。
这样,$\sum\limits_{k=0}^{N-1}P[k]$的得数就**为$N$倍的**未旋转的向量了。同时,这个向量也**正好在实数轴上。**
这其实就和做IDFT主过程后不需要做转换,并要乘$\frac 1 N$有关。
.
然后我们再思考下$\omega^{-nk}$在式子中的作用。
为了便于区分,我们重新定义$\omega^{-nk}$为$\omega^{-n'k'}$。
对于每项$x[n]\omega^{nk}$,其IDFT后的值为$x[n]\omega^{nk-n'k'}$。
这个式子看起来没什么可化简的,但我们**注意到$k$和$k'$的值实际上是一样的**(可以观察下求和中$k$的变化)。
于是就有$x[n]\omega^{k(n-n')}$。这意味着对于$P[n]$ IDFT后的图像,只不过是旋转的角度等倍缩小了,每个向量的分布仍是**均匀**的;
再观察DFT的式子中$k$的变化范围,发现对于$x[n]$项旋转的的圈数为$n$,而$(n-n')$后是整数,所以圈数仍为**整数**。
因此,我们上面推出的**求和为$0$的结论**也是成立的。
.
那$\omega^{-nk}$有什么具体意义呢?
发现特别的当$n=n'$时,对于$x[n]$所有项旋转的角度**均为零**!所以$\omega^{-nk}$的实质就是**具体控制“特殊情况”出现在哪一项。**
这样,我们就构建起了DFT/IDFT的一种几何直观(~~但变换公式本身是如何推得的还要另外讨论~~)
## 3. 卷积初步
卷积的应用十分广泛,且在不同的领域有不同的意义。本节主要是单纯地介绍卷积和其代码实现,对其意义不做探讨。
.
卷积可以分为两种:**线性卷积** 和 **循环卷积** 。下面我会分别讲解它们:
(科普:$[P]$指当表达式$P$成立时其值为$1$,否则为$0$)
(约定:仅带条件的$\sum$中的条件变量通常为整数)
- ### I. 线性卷积
其式子为$c_k=\sum\limits_{0\leq i<n}\sum\limits_{0\leq j<m}[0\leq k-i]A[k-i]B[j](0\leq k < n+m-1)$
我们也可以从这个式子知道线性卷积结果的规模为$n+m-1$
可以做个图,更生动些:
![线性卷积](https://cdn.luogu.com.cn/upload/image_hosting/2q41v4op.png)
(图中限于篇幅只表示了一项)
仔细观察的话可以发现,这形式就像我们平时所做的 **多项式乘法** 。
- ### II. 循环卷积
其式子为$C[k]=\sum\limits_{0\leq i<n}\sum\limits_{0\leq j<n}A[(n+k-i)\mod n]B[j](0\leq k < n)$
或$C[k]=\sum\limits_{0\leq i<n}\sum\limits_{0\leq j<n}[(i+j)\mod n=k]A[i]B[j](0\leq k<n)$
注意到,特别的是,循环卷积要求两个序列**长度相等**,且结果的规模是**不变**的。
我们可以将取模想象为在序列后面加一个**负数编号的“假序列”**,做一张图:
![循环卷积](https://cdn.luogu.com.cn/upload/image_hosting/ogv89eg6.png)
或者,也可以形象地理解为先将一个序列反序,再将两个序列**分别**按 逆时针 和 顺时针 顺序放到两个同心圆上,逐位旋转相乘,每种旋转状态下**的积的和**就是结果序列该项的值(细节实现可能还要一些规定)。
就像这样:
![循环卷积2](https://cdn.luogu.com.cn/upload/image_hosting/y6qj7e5c.png)
- ### III. DFT 与 循环卷积,及一些序列操作
扯了这么多,难道我们只能朴素地$O(n^2)$计算卷积吗?
并不,因为 DFT 和 卷积其实是大有关系的。
#### III.I 谈相加
首先扯一点简单的结论:DFT后对序列做逐项相加后IDFT,是**等同**于 对原序列做逐项相加的。
我们可以想到,幅角相同的复数相加后,其**幅角是不变的**;
也就是说,DFT后对序列做逐项相加,其**贡献**和 对原序列做逐项相加是一样的:每一位的每一个变换映射都对应相加了(如果您真的看懂了第二节,这个应该是很显然w)。由此得证。
#### III.II 谈相乘
接下来是一个关键的结论:**DFT后对序列做逐项相乘后IDFT,是等同于 对原序列做循环卷积的。**
这是一个非常重要的性质,也正是这个性质使得我们可以用 FFT 优化序列的操作。
.
但我尝试了很多办法,都不能找到一个简洁的图像表示法证明这个结论(~~窝太太菜惹QAQ~~),这里没办法,只能给出一个利用**单位根**(这里定义循环节终点一定是$1$)**反演**($[n|k]=\frac 1 n \sum\limits_{0\leq d<n}\omega_n^{dk}$)的证明:
$C[k]=\sum\limits_{0\leq i<n}\sum\limits_{0\leq j<n}[(i+j)\mod n=k]A[i]B[j]$
$C[k]=\sum\limits_{0\leq i<n}\sum\limits_{0\leq j<n}A[i]B[j][n|(i+j-k)]$
$C[k]=\sum\limits_{0\leq i<n}\sum\limits_{0\leq j<n}A[i]B[j](\frac 1 n\sum\limits_{0\leq d<n}\omega_n^{d(i+j-k)})$
$C[k]=\frac 1 n\sum\limits_{0\leq d<n}\omega_n^{-dk}(\sum\limits_{0\leq i<n}A[i]\omega_n^{di})(\sum\limits_{0\leq j<n}b_j\omega_n^{dj})$
- ### IV. 循环卷积和线性卷积的关系
经过尝试,我们可以发现:**如果将两个序列补零拓展到$n+m-1$项,再对它们做循环卷积,其结果和对它们做线性卷积是一样的**
还是上面那张图,我们对$0$相关的元素高光处理下就知道为什么:
![卷积](https://cdn.luogu.com.cn/upload/image_hosting/qq1c0qpa.png)
可以发现,除去和$0$相乘的“无效项”后,其形式其实就是个**线性卷积**!
而通过式子我们也可以发现,对于$k-i<0$时,两项总有一项是补上去的零(即下标超出原序列范围,可显然得证),**使这组的贡献无效。**
这样,我们便可以用 循环卷积 来计算 线性卷积,从而弥补 DFT只能快速计算循环卷积 的缺陷。
- ### V. 一种常见卷积形式及代码实现
在题目中,我们常常会遇到这种卷积形式:
$P[k]=\sum\limits_{i=0}^{n-1}f(k-i)g(i)$
我们可以对$f(x)$在`[-n+1, n-1]`的整数取值$x$从小到大对应序列编号从小到大做一个长$2n-1$的序列,并同样地对$g(x)$取值`[0, n-1]`地做一个序列。
(即$f(x)$对应$f[n-1+x]$,$g(x)$对应$g[x]$)
上式即可化成:
$P[k]=\sum\limits_{i=0}^{n-1}f[n-1+k-i]g[i]$
我们可以发现这很像一个完整的**线性卷积**形式。实际上,它就是在计算$f[i]$与$g[i]$的线性卷积的`[n-1, 2n-2]`项。
我们可以形象地做张图:
![形式](https://cdn.luogu.com.cn/upload/image_hosting/amm1yz0q.png)
另外,如果$f(x)$的代入值是$(i-k)$的话,我们可以 **在函数定义上更改(紧贴着变量加负号) 或 将序列反转** ,以化成上述形式。
#### IV.I. \*小优化
其实这里的卷积长度可以只做到`[0, 2n-2]`,或者说不做末$n-1$项。我们可以保证答案不受影响。
具体的证明可以详见 ***6. IV*** 。
#### IV.II.代码示例
这里给出关键部分的代码(且已经使用了 *3.IV.I* 所述的优化):
```cpp
//常见卷积形式代码
//n 如上文所述,N 是指第一个大于等于 2n-1 的2的幂次数(即 FFT的规模)
//数组缺省值为 0
inline void Conv(){
for(int i =-n+1; i <= n-1; ++i) f[n-1+i].real =F(i)/*计算 f的函数*/;
for(int i =0; i < n; ++i) g[i].real =G(i)/*计算 g的函数*/;
pre()/*预处理*/, FFT(f), FFT(g);
for(int i =0; i < N; ++i) Q[i] =f[i]*g[i];
IFFT(Q);
for(int i =0; i < n; ++i) ans[i] =int(Q[i+n-1].real/N+0.5);
}
```
## 4. \*CZT
我们知道DFT是为了快速计算卷积,但为什么要计算卷积呢?
原因之一是卷积其实描述了一种非常常见的情景--"信号的叠加"。DFT,就可以视为快速计算的桥梁。
但显然真实的情景肯定不止这一种,于是就有了定义更广泛的 **Z变换**。
其式子的一种形式为:
$Z[k]=\sum\limits_{n=0}^{N-1}x[n]z[k]^{-n}$
注意这里的$z[k]$是规定的变换对序列。而变换对的规定,就和**具体需要变换的序列**有关。
如做DFT实际上就可以规定$z[k]=w^k$。
当然Z变换的变换对还有很多,每个变换对也有不同的含义;同时其通用的逆变换形式也较复杂。这些和下文没有太大联系,这里就不展开了。
- ### I. 计算Z变换
显然朴素计算Z变换是$O(n^2)$的,我们需要更优的算法。可Z变换**既不一定满足(类似的)蝴蝶定理,变换的规模$N$也不一定可以表达成$2$的幂次**,这要怎么办?
其实若对于一般的Z变换,确实是很难找到快速计算的算法的。不过如果限定一些条件,我们还是有比较显然的方法的。
- ### II. CZT
现在我们限定$z[k]$是**对复平面上一段螺线的等分角抽样。**
即$z[k]=AW^{-k}(0<k<M)$。其中$A,W$是任意复数,$M$和$N$无关。
这样就可以尝试把变换化成卷积的形式了:
$Z[k]=\sum\limits_{n=0}^{N-1}x[n]z[k]^{-n}$
$Z[k]=\sum\limits_{n=0}^{N-1}x[n]A^{-n}W^{nk}$
接下来我们要想办法分别化出**仅两个(在求和式子中)自变量与$k-n$、与$n$成线性关系的函数**。
最简单地,可以想到用**平方式**:
$Z[k]=\sum\limits_{n=0}^{N-1}x[n]A^{-n}W^\frac {(k-n)^2-k^2-n^2} 2$
$Z[k]=W^\frac {-k^2} 2\sum\limits_{n=0}^{N-1}A^{-n}W^\frac {-n^2} 2x[n]\cdot W^\frac {(k-n)^2} 2$
显然,只要设$f(k-n)=W^\frac {(k-n)^2} 2,g(n)=A^{-n}W^\frac {-n^2} 2x[n]$,这就是我们上节讲的“**常用卷积形式**”了,直接套模板就行了。
代码就不放惹。
这即是 **CZT算法**。
.
当然 CZT 本身的含义、其几何直观也是可以展开来讲的。但和 Z变换 一样,其坑太深并且和 DFT、卷积 没太大关系,这里就不展开来讲了。
读者如果有兴趣的话,可以参考 [这篇ppt](https://wenku.baidu.com/view/62110fae0c22590102029d7a.html) 对CZT的讲解。
## 5. 卷积与多项式
我们发现多项式相乘,其系数的变化规律其实就是**做线性卷积**。
于是直接带入模板求解即可。
## 6. \*卷积的意义浅谈
上文我们曾提到:“卷积其实描述了一种非常常见的情景--"信号的叠加"”。但具体又是怎样的呢?下面,我会给出一个简单的例子:
.
我们先定义信号强度函数$h(T)(0\leq T<N)$,其中$T$指**离信号发生的时刻的距离**,负数指发生前,正数亦然;$N$指总的统计范围。
然后再定义一个信号密度函数$g(t)$,指$t$时刻**发生的信号的数量**。
定义单位时刻信号总强度为**该时刻各个信号强度和**,题中单位均给定统一。
.
现在,我们要求求所有可能的时刻$t$的信号总强度。
朴素的模拟每个信号是$O(n^2)$的,但其实我们可以从另一个角度入手。
我们设$P(k)$为$k$时刻信号总强度。若我们考虑**每个信号到达$k$时刻**时的强度,便可以列出式子:
$P(k)=\sum\limits_{t=0}^{N-1}h(k-t)g(t)$
这非常显然就是个卷积的式子。
## 7. 一些tricks
- ### I. 打包FFT
我们想到,既然变换的本质是对向量做**旋转**,我们可不可以构造斜率不同的过原点的直线作为数轴,将实数一一**映射**到它们上面(即调整斜率),从而一次打包多个多项式的FFT?
构造任意数轴并映射所需要的计算量是巨大的;但不同的是正交的两个数轴**虚数轴**和**实数轴**有特别的性质。
因此,接下来我们考虑打包两个序列做FFT,即将其中一个序列**映射到虚数轴**上。
#### I.I 对于IDFT的打包
我们只需将两个DFT后的序列(且DFT时的旋转方向一致)的其中一个沿DFT的旋转方向**再旋转90度**(即乘$i$或$-i$)后叠加,做完IDFT后直接检索 虚数轴 和 实数轴 即可(做旋转处理的序列会在**虚数轴**上)。
#### I.II 对于DFT的打包
DFT的打包比起IDFT的打包要复杂得多。
若我们要打包序列$A[k],B[k]$。
先设$P[k]=A[k]+iB[k],Q[k]=A[k]-iB[k]$
我们可以表示为
$P[k]$:
![Ax+iBx](https://cdn.luogu.com.cn/upload/image_hosting/ujkrbk2q.png)
$Q[k]$:
![Ax-iBx](https://cdn.luogu.com.cn/upload/image_hosting/pv9jxaua.png)
于是很显然的有:
$A[k]=\frac {P[k]+Q[k]} 2,B[k]=i\frac {Q[k]-P[k]} 2$
.
变换后:
$F_P[k]$:
![preAx+iBx](https://cdn.luogu.com.cn/upload/image_hosting/gukwiw6v.png)
$F_Q[k]$:
![preAx-iBx](https://cdn.luogu.com.cn/upload/image_hosting/s8smai98.png)
于是也有
$F_A[k]=\frac {F_P[k]+F_Q[k]} 2,F_B[k]=i\frac {F_Q[k]-F_P[k]} 2$
这个等式其实是转换前后始终满足的:因为DFT/IDFT就是对这些向量做**旋转**,而$P,Q$的变换方向我们保证是**一致的**。
.
现在我们要简化$F_P,F_Q$为一次DFT运算。
我们观察$F_P[N-1]$和$F_Q[N-1]$的值,发现它们正好是一对**共轭复数**;而我们又联想到$F_P,F_Q$的定义,便可以推测它们的值序列一定存在着某种**共轭映射**。
于是我们尝试做$F_Q[k]$的共轭复数
![共轭](https://cdn.luogu.com.cn/upload/image_hosting/9er6n6h1.png)
这不就是将$P$反向旋转么?!
即$F_Q[k]=conj(F_P[N-1-k])$($conj(z)$为取$z$的共轭复数)(**且对于$k=N-1$特别的有**$F_Q[N-1]=F_P[N-1]$,这就是我们上文提到的)。
.
整理一下:
1.设$P[k]=A[k]+iB[k],Q[k]=A[k]-iB[k]$
2.有$F_A[k]=\frac {F_P[k]+F_Q[k]} 2,F_B[k]=i\frac {F_Q[k]-F_P[k]} 2$
3.有$F_Q[k]=conj(F_P[N-1-k]) (0<k<N-1)$($conj(z)$为取$z$的共轭复数), $F_Q[N-1]=F_P[N-1]$
这样我们便成功将DFT也两两打包为一次。
.
_在[这篇论文](http://www.doc88.com/p-4912876232995.html)中其实有用式子推导DFT的打包的方法。但这种方法没有一个明显的几何直观,这里就不展开了。_
#### I.III 代码示例
为了简洁,这里仅给出打包的核心代码:
```cpp
//该程序计算了(A+B)*(C+D)(线性卷积),为了减小精度负担而拆开计算
//cp是复数类,ll是long long,conj()是取共轭复数
//MAXN2较小,MAXN较大
//变量缺省值均为 0
int N1 /*A, B长度*/, N2 /*C, D长度*/, A[MAXN2], B[MAXN2], C[MAXN2], D[MAXN2];
int N /*FFT规模。注意预处理时 N的具体大小(为两个序列相乘长度 )*/, M /*模数*/;
ll ans[MAXN];
cp P[MAXN], Q[MAXN], A2[MAXN], B2[MAXN], C2[MAXN], D2[MAXN];
inline void conv(){
/*这里P打包A, B; Q打包C, D*/
for(int i =0; i < N1; ++i) P[i].real =A[i], P[i].imag =B[i];
for(int i =0; i < N2; ++i) Q[i].real =C[i], Q[i].imag =D[i];
preFFT();/*预处理*/
FFT(P), FFT(Q);
for(int i =0; i < N; ++i){/*提取,这里把除法整合了,注意前面的cp(0, 0.5)|cp(0.5, 0)*/
cp P2 =conj(P[(N-i)&(N-1)]);
A2[i] =cp(0.5, 0)*(P[i]+P2), B2[i] =cp(0, 0.5)*(P2-P[i]);
cp Q2 =conj(Q[(N-i)&(N-1)]);
C2[i] =cp(0.5, 0)*(Q[i]+Q2), D2[i] =cp(0, 0.5)*(Q2-Q[i]);
}
/*这里P打包ac,ad,Q打包bc,bd*/
/*程序中有简化运算*/
for(int i =0; i < N; ++i) P[i] =A2[i]*(C2[i]+cp(0, 1)*D2[i]);
for(int i =0; i < N; ++i) Q[i] =B2[i]*(C2[i]+cp(0, 1)*D2[i]);
IFFT(P), IFFT(Q);
for(int i =0; i < N1+N2-1; ++i){/*最后检索虚实数轴即可*/
ll ac =(ll)(P[i].real/N+0.5)%M,
ad =(ll)(P[i].imag/N+0.5)%M,
bc =(ll)(Q[i].real/N+0.5)%M,
bd =(ll)(Q[i].imag/N+0.5)%M;
ans[i] =(ac+bd+ad+bc)%M;
}
}
```
#### \*I.IV 对于复数轴特殊性的一种代数体现
文章完成后,我又在评论区( *感谢* [*command_block*](https://www.luogu.com.cn/user/58705) )发现了一种非常初等的针对计算 `(A+B)*(C+D)=AC+BD+AD+BC` 的右边四项(通常是为了防止炸精度)的优化方法:
$(A+Bi)*(C+Di)=(AC-BD)+(AD+BC)i$
$(A-Bi)*(C+Di)=(AC+BD)+(AD-BC)i$
这样我们只需计算三次DFT和两次IDFT就可以加减计算出答案了(而上文介绍的方法只需四次,两者间仅差一次)。
具体实现可以参考[原文](https://www.luogu.com.cn/blog/command-block/solution-p4245#)(是一篇MTT的题解)
- ### II.预处理单位根+指针
这是一个很好用的技巧:**预处理单位根**,然后将代码中访问数组下标的操作**替换为指针**,可以节省不少时间。
#### II.I 核心代码
这里直接给出对于FFT示范代码(NTT可以照这样子写):
```cpp
//cp为复数类
inline void FFT(cp *f/*操作序列*/){
for(int i =0; i < N; ++i) if(i < S[i])
swap(f[i], f[S[i]]);
/*w的步长,N为总范围,除掉操作范围2,之后每次除2即可(操作范围在增倍)*/
for(int lim =1/*操作范围的1/2*/, step =N>>1; lim < N; lim <<=1, step >>=1){
for(int s/*起始点*/ =0; s < N; s +=lim<<1){
cp *w =W, *f0 =f+s, *f1 =f+s+lim;/*f0为左区间,f1为右区间*/
for(int i =0; i < lim; ++i){
cp res =*f1 * *w;
*f1 =*f0-res, *f0 =*f0+res;//这里的运算顺序按照值变化的顺序有所改变
w +=step, ++f0, ++f1;
}
}
}
}
```
#### II.II 预处理
FFT单位根预处理是这样子的:
```cpp
for(int i =0; i < N; ++i) W[i] =cp(cos(2*pi*i/N), sin(2*pi*i/N));//直接对于最小的步长求单位复根
//这里也可以递推求,但精度会下降很多
```
做IFFT时单位根,可以直接**对原单位根序列取共轭复数**即可。像这样:
```cpp
inline void op(){
for(int i =0; i < N; ++i) W[i] =cp(W[i].real, -W[i].imag);
}
```
.
NTT单位根的预处理也大同小异。
但做INTT的单位根时,是可以直接从 NTT的单位根 推得的。
我们可以发现这样一个式子:$\omega^i\cdot\omega^{n-i}\equiv1\pmod P$。
也就是说,$\omega^i, \omega^{n-1}$**互为乘法逆元**。即可以说$\omega^{n-1}\equiv\omega^{-i}\pmod P$
直观的理解的话可以参考上文中**打包FFT时那两个序列**的关系($P, Q$)。
代码如下:
```cpp
inline void getWinv(){
for(int i =0; i < N; ++i) Winv[i] =W[(N-1)&(N-i)];
}
```
#### II.III 实践经验小结
做题时,选择预处理的方式也是有技巧的。若题目**精度**要求不大,递推处理即可;否则一定要每一项都根据公式算,避免精度误差。同时**打包操作也是会一定地影响精度的**。
由于打包FFT的效率极高,个人还是推荐用公式保证精度再加打包;实在不行拆系数算也是比常规算法效率要高的。
- ### III. [已编辑]
这条被我验证过其实是无效的QWQ。
.
但为什么还要占一个位子呢?
原本我分享的是利用 `double` 的玄学精度,尽量地让操作数字转变为小数来提高精度。
但实际上 `double` 是按 **数字长度** 做精度的,和 **数字大小** 则没关系。
~~窝太菜re呜呜呜~~
- ### IV. 缩减规模
如像上面提到的"常见卷积形式",需要求A`[0, n-1]`与B`[0, 2n-2]`的卷积C`[0, 3n-2]`的`[n-1, 3n-3]`项,常规操作是把A, B全部扩充到`[0, 3n-3]`然后再做卷积,但实际上只需要扩充到`[0, 2n-2]`就行了。
.
我们观察下线性卷积的图解发现:使用`"0"`最多的是答案序列的最右项,且正好"用"完了所有的`"0"`(如果序列长度不一的话,上下统计结果也是不一样的。**这里取剩下最少的一组**):
![规模](https://cdn.luogu.com.cn/upload/image_hosting/f8tsfdtq.png)
而从最右项从右往左,"用"的`"0"`的数量也会逐个减少。
假设我们减少一位序列的长度,正好"用"完`"0"`的那项就会受"干扰"。
也就是说,假如我们逐个减小序列的长度,就会逐次地从右往左地影响答案。注意这里**原有序列**是不变量,这个结论要求减少的项是扩充的`"0"`
若我们只做`[0, 2n-2]`,就相当于不做末$n-1$项,也就是减少了$n-1$项,于是从右往左地影响了`[0, n-2]`,正好保证了答案不受影响。
代码就如上文提到的一样。
.
具体的式子也是可以列出来的,但没有图像直观,这里就不多提了。
- ### V. 任意长度循环卷积和Bluestein's Algorithm
有时候,题目会要求我们做任意长度的循环卷积,但显然我们不能用朴素的算法。我们需要一个复杂度和FFT近似的算法才能通过评测。
#### V.I 利用卷积性质
我们再做一次循环卷积的示意图,但这次并不做多余的序列。
![循环卷积2](https://cdn.luogu.com.cn/upload/image_hosting/amidv7x0.png)
我们发现,它其实是以答案的位置为分界点,把序列分成了 包括答案位置的右边 和 不包括答案位置的左边 两段,并分别做了一个"**对称**"的特殊乘法。
接下来我们再观察为了**做线性卷积**而拓展后的答案序列的 `[x]`项 与 `[x+n]`项 :
`[x]`:
![规模1](https://cdn.luogu.com.cn/upload/image_hosting/a4uedw8y.png)
`[x+n]`:
![规模2](https://cdn.luogu.com.cn/upload/image_hosting/dt0rcs8c.png)
我们发现,**`[x]`项所缺的部分正好就是`[x+n]`所有的部分!**
- ##### V.I.I `[x]`项组成
首先右边序列贡献不变是可以保证的。
我们再看左边序列:发现扩充时补零的长度至少为$n-1$,这样就保证了**左边序列的贡献为零**(可以想象一下)。
- ##### V.I.II `[x+n]`项组成
首先它的左边序列一定是全由`"0"`组成的,贡献也为零。
然后我们发现,若从右往左计数,至`[x+n]`项正好有$x+1$个`"0"`,这和`[x]`项右边序列的长度相等。也就是说,**保证了`[x]`右边序列的元素不做贡献。**
而对于中间那段,因为这种特殊乘法的"**对称性**",中间的贡献和原来`[x]`项左边序列**的贡献是等价的。**(这里也建议想象一下)
另外,特殊的对于$[x=n-1]$,不需要`[x+n]`项补充。(可自证)
- ##### V.I.III `[x+kn]`项的存在性
补足的长度**至少**为$n-1$;而对于$(x=n-1)$,是不需要`[x+n]`项补充的(可观察图)。
对于`[x+kn]`$(k>1)$的情况,我们可以置之不理,**实际上`[x+kn]`的贡献也是为零**的(这时从左往右计数的"0"数量已经超过原有序列长度了)。
但要注意,**保证存在的项只有`[n, 2n-2]`,实际使用时还是要特判的。**
- ##### V.I.IV 实现
我们只需做一次 线性卷积。**然后仅将`[x+n]`项与`[x]`项叠加**即可(特别的对于`[n-1]`项不考虑)。
代码如下:
```cpp
//此程序可以快速计算任意长度的 循环卷积
//cp是复数类
//n是序列规模,N是FFT的规模
//MAXN2较小,MAXN较大
//变量缺省值均为 0
cp P[MAXN], Q[MAXN];
int ans[MAXN2];
inline void Conv(int *f, int *g){
for(int i =0; i < n; ++i) P[i].real =f[i], Q[i].real =g[i];
preFFT()/*预处理*/, FFT(P), FFT(Q);
for(int i =0; i < N; ++i) P[i] =P[i]*Q[i];
IFFT(P);
for(int i =0; i < n-1; ++i) ans[i] =int(P[i].real/N+0.5)+int(P[i+n].real/N+0.5);
ans[n-1] =int(P[n-1].real/N+0.5);/*为了避免越界特判了对于 [n-1]项的计算*/
}
```
.
这种算法其实已经很优秀了。在不要求对DFT(未IDFT)的序列做操作时,它甚至**是最优的**。
但一旦题目要求做如序列(循环卷积)幂,这个算法最终会多出一个$O(log n)$的复杂度(封装),这是我们不想要的。
因此,我们需要一个**切实**能做 **任意长度DFT** 的算法。
#### V.II Bluestein's Algorithm
上文中我们曾提到 CZT算法。
$Z[k]=W^\frac {-k^2} 2\sum\limits_{n=0}^{N-1}A^{-n}W^\frac {-n^2} 2x[n]\cdot W^\frac {(k-n)^2} 2$
若设$A=1,W=\omega$,这个式子**就变成 DFT 的形式了**:
$F[k]=\omega^\frac {-k^2} 2\sum\limits_{n=0}^{N-1}\omega^\frac {-n^2} 2x[n]\cdot \omega^\frac {(k-n)^2} 2$
而对于 IDFT,我们只需改下符号重新推导,最后加个$\frac 1 N$即可:
$x[k]=\frac 1 N\cdot\omega^\frac {k^2} 2\sum\limits_{n=0}^{N-1}\omega^\frac {n^2} 2F[n]\cdot \omega^\frac {-(k-n)^2} 2$
这即是 Bluestein's Algorithm。
##### V.II.I 兼容NTT
但是,这种拆法在做NTT时是行不通的:我们无法保证幂数乘$\frac 1 2$后仍是整数。
所以我们可以拆成均是**相邻整数相乘**的式子:
$nk=\frac {(k-n)(k-n+1)-k(k
+1)-n(n-1)} 2$
(这个式子其实也可用组合数解释,具体的话就是$nk=\binom{n+k}2-\binom n2-\binom k2$)
然后直接带入即可:
$F[k]=\omega^\frac {-k(k+1)} 2\sum\limits_{n=0}^{N-1}\omega^\frac {-n(n-1)} 2x[n]\cdot \omega^\frac {(k-n)(k-n+1)} 2$
$x[k]=\frac 1 N\cdot\omega^\frac {k(k+1)} 2\sum\limits_{n=0}^{N-1}\omega^\frac {n(n-1)} 2F[n]\cdot \omega^\frac {-(k-n)(k-n+1)} 2$
##### V.II.II 代码示例 (NTT版)
设$g(x)=\omega^{op\cdot\frac {-x(x-1)} 2}A[x],f(x)=\omega^{op\cdot\frac {x(x+1)} 2}$,其中$op$是外部变量,作用是**控制转换方向**;$A[x]$是指当前转换"对应的序列"。
便有:
$F[k]=\omega^\frac {-k(k+1)} 2\sum\limits_{n=0}^{N-1}f(k-n)g(n)$
$x[k]=\frac 1 N\cdot\omega^\frac {k(k+1)} 2\sum\limits_{n=0}^{N-1}f(k-n)g(n)$
(注意这份代码也使用了 **6.IV** 的优化)
```cpp
//此程序可以快速计算任意长度的 DFT/IDFT;此处实例为NTT
//cp是复数类,ll是long long,Pow()是快速幂
//gg是原根,M是模数,n是规模,invn是模 M意义下 n的逆元,N是FFT的规模
//MAXN2较小,MAXN较大
//变量缺省值均为 0
inline int G(int x, int op, int *f){ return Pow(gg, op*-x*(x-1)/2)*f[x]%M; }
inline int F(int x, int op){ return Pow(gg, op*x*(x+1)/2); }
int f[MAXN2], g[MAXN2];
cp P[MAXN], Q[MAXN];
inline void CZT(int *A, int op /*1为正变换,-1为逆变换*/){
for(int i =-n+1; i <= n-1; ++i) f[n-1+i] =F(i, op);
for(int i =0; i < n; ++i) g[i] =G(i, op, A);
for(int i =0; i < 2*n-1; ++i) P[i].real =f[i], Q[i].real =g[i];
preFFT()/*预处理*/, FFT(P), FFT(Q);
for(int i =0; i < N; ++i) P[i] =P[i]*Q[i];
IFFT(P);
for(int i =n-1; i < 2*n-1; ++i){
A[i-n+1] =(ll)(P[i].real/N+0.5)%M*Pow(gg, op*-i*(i+1)/2);
if(op == -1) A[i-n+1] =A[i-n+1]*invn%M;
}
}
```
代码中有**很多部分是可以预处理的**。但为了简洁,这里就用最朴素的版本了。
---
## 后记
在审核回复我前一篇文章时,我发现除了一个通过的,只有我的文章没有明确不予通过QWQ。
所以就心血来潮又写了一篇。( _现在看来和上一篇已经有天差地别的区别惹w_ )
这篇和上一篇不同的是,里面的概念和技巧都是较深奥的,可能不适合FFT初学者看。但考虑到前面**已经有**入门教程,想让自己的文章上日报还是要拿出一些干货的。
本文最有价值的内容应该是最后一节的优化技巧。里面我整理了尤其是在 《再探快速傅里叶变换》--毛啸 里提到的打包技巧,和我平日里总结的优化方式。(~~有了这些一定可以让您的FFT爆踩NTT~~)
最后再扯件事。本来文章开头是想从最最基本的 FT,傅里叶变换 讲起的。但中途查了许多资料,最后也没有完全理解连续意义下的傅里叶变换,还险些推翻了之前对DFT的认识(~~要是真推翻了重写让我这个鸽子怎么办~~),于是最后这部分还是咕咕了(~~你可以试着读下本文链接,猜下原来的标题是什么QWQ~~)
希望最终可以通过审核把,毕竟我的博客太惨了QAQ(~~真·一个人都没~~)
.
_现在通过审核了www。_
_FT部分其实多少也快扯到了,不过当然仅仅是其冰山一角QAQ。_
_文章为了通俗易懂舍弃了部分严谨性,一些分析也没有采用更系统的办法(典型的如卷积那部分)。这点我在写文时就想到了,不过也无可奈何(有兴趣的读者可以百度了解一下信号处理的领域,那是一个很大的坑,且基本与竞赛无关)。_
_能看到这里,想必您已经读完全文了把。最后感谢您的阅读~QWQ 别忘了点赞_
$\color{#ffb464}\texttt{首次校对于2020/01/17/19:37}$
$\color{#ffcc64}\texttt{最后校对于 2020/02/11/16:54 (添加了卷积相关) }$
$\color{#ffb400}\texttt{最后修改于 2020/03/06/11:56}$