蒟蒻数论笔记:讲给数论小白的快速傅里叶变换
题外话
我感觉自己的数论笔记从来没有写完过,从极限到微积分到积型函数到FFT
2021.11.10 为啥过了一个晚上就有15个人看了???
正题
1.傅里叶变换:
有关傅里叶变换,##先看视频##
2.前置芝士之泰勒展开
(这里不理解也没有什么问题,知道结论即可)
泰勒展开的本质就是用一个多项式来拟合一个函数在\(x_0\)处的变化,为此要做到每一次求导都与原函数相同,以此来拟合函数的变化。
分母是幂函数求导带来的常数项约去
所以,我们可以用它来展开几个函数:\(e^{ix},i\sin(x),\cos(x)\)其中\(i\)为\(\sqrt{-1}\)
已知:
拓展到复数:
放缩:
已知:
乘上一个复数:
已知:
得出:
也就是欧拉公式。
带入\(\pi\)得:
可以理解为,随着\(x\)的增大,\(e^{ix}\)也在一个圆上运动。
于是结合上面的视频可以理解傅里叶变换的公式:
这里变成了向量
加负号是因为变换旋转的方向。
普通的傅里叶是\(O(n^2)\)的,而快速傅里叶可以做到\(O(n \log n)\)。
2.前置芝士之复数运算
看到前面的复数运算,有些同学可能已经有点搞不懂了,我一开始也是这样的。
这里就简单讲一下复数的运算,就是数学选修2-2的最后一章,(可以自己看)。
简单来说,复数集是对实数集的一个扩充,在定义上,我们这样来看复数:
我们把这个集合中的数,形如\(a+bi\)的叫做复数,\(i\)是虚数单位,\(C\)是复数集。
为了简洁,我们直接开始四则运算:
3.前置芝士之多项式卷积
先推荐一个回答
博主讲的很好。\(QWQ\)
像乘法,也可以变成多项式乘法,然后卷积。
快乐的\(O(n^2)\)
在FFT中,我们就是把两个多项式变成了两组向量,然后两组向量卷积求新的向量。
4.前置芝士之点乘法
n阶的函数由n+1个点唯一确定。
传统的多项式是用系数表示法,我们可以换为点值表示法来表示。
两个与\(x\)有关的多项式:
两个相加就直接加上即可,相乘的话比较麻烦。
因为\(C(x)\)的项数\(2n+1\),所以两个式子都要补上一堆项,然后项多项式加法一样按位运算。
乘法:
现在我们见证了奇迹的诞生!!\(O(n)\)的时间复杂度!!!
所以,我们到现在就知道了FFT的流程:
1.得到两个多项式A,B;
2.对于每个多项式,选取n+1个点,得出点值表达\((O(n^2))\)
3.点乘法
4.将\(C(x)\)的点值表达式转化为系数表达式。\(O(n^2)\)
5.前置芝士之拉格朗日插值
虽然没有用,但是还是要讲一下。
插值主要用在将点值表达式转换为系数表达式上,也就是一个\(n\)阶的函数由\(n+1\)个不同的点唯一确定。
常见的插值法是拉格朗日插值法。
由于有n个点,求n-1阶函数。我们先枚举每一个点\(x_i\),建立一个函数\(f_i(x)\),使得\(f_i(x_i)=y_i\),而其他的\(x_j,j!=i\)带入,\(f_i(x_j)=0\)。
然后,一整个函数就是由这\(n\)个函数累加起来的和,在没一个点\(x_i\)上的值都对应其\(y_i\)。
然后构造出公式:
这就可以用来完成FFT的第三步,但是时间复杂度是\(O(n^2)\)。FFT继续优化。
读到这里结合前面的芝士,你已经知道了DFT以及IDFT了,接下来就是FFT的诞生之路
FFT优化之路
6.前置芝士之单位复根
n次的单位复根是满足\(\omega^{n}=1\)的复数\(\omega\),因为\(\omega^{n}\)在复数平面上的模都是一,所以相乘之后还是一,那么所有的\(\omega_{i}\)都会均匀度分布在单位圆上,\(n=8\)时;
我们再考虑欧拉公式:
取\(x=2\pi\)
从而:
把此时的单位根为主次单位根:
对于其他单位根,
第一步的优化:快速傅里叶变换之\(O(n \log n)\)求点值
对一个多项式求点值,我们带入2n个点求点值,并强选\(2^t\)个单位根,保证n为2的整数次幂,那么把\(2^t\)带为n。
得:
有:
所以多项式\(A(x)\)的点值为:
考虑把每个点值按照奇偶分开:
左边提一个\(\omega_n^i\)
然后,把左边变成一个系数为\(a_0,a_2,a_4,a_6\cdots\)的多项式\(FL(x)\),带入\(\omega_{n}^{2i}\),也就是\(\omega_{n/2}^{i}\)(看上面的芝士即可懂了)。
把右边也变成一个系数为\(a_1,a_3,a_5,a_7\cdots\)的多项式\(FR(x)\),带入\(\omega_{n/2}^{i}\)。
得:
当\(n=8\)时:
这时我们发现前后的值是一样的,就可以折半计算了:
\(T(N)=2T(\frac{N}{2})+O(n)=O(n \log n)\)
啊啊啊啊!!冯巨讲的时候好迷茫现在看来真NB!!
第三步的优化:快速傅里叶逆变换之\(O(n \log n)\)求系数
在前置芝士之拉格朗日插值中,我们在求了\(C(x)\)的点值表达式后,运用拉格朗日插值法,\(O(n^2)\)求出系数表达式。
在这里,我们可以用快速傅里叶变换的优秀性质来进行逆变换。
首先,多项式的点值
我们用\(b_i\)来表示上面的式子进行快速傅里叶变换后的\(A(\omega_{n}^i)\)。
我们对这些\(b_i\)又进行一次快速傅里叶变换:
恒等变换:
基本操作交换sum:
由单位复根的性质得,当\((j+x)=n\)时,\(j+x\)才有值,后面的\(\sum\)才有意义。
所以:
所以这就得到了系数了,IFFT万岁!!!
后置甜点之代码
首先是递归版本,常数有点大(板子都过不了)
转载大佬的伪代码:
点击查看代码
int Lim = 1, N, M ;
function FFT(int lenth, complex *A, int flag){
IF (Lim == 1) return ;
complex A0[lenth >> 1], A1[lenth >> 1] ;//分成两部分
for(int j : 0 to lenth by_grow 2) A0[j >> 1] = A[j], A1[j >> 1] = A[j + 1] ;
FFT(lenth >> 1, A0, flag) ;
FFT(lenth >> 1, A1, flag) ;
complex Wn = unit(,) , w = (1, 0) ;
//Wn是单位根,w用来枚举幂,即我们令主次单位根不变,由于其余单位根都是其整次幂,所以可以用一个w来记录到第几次幂
/*此处求单位根的时候会用到我们的参数flag……嗯没错就用这一次,并且flag的值域为(-1, 1)……是的,只会有两个值*/
for(int j : 0 to (lenth >> 1) by_grow 1 with w = w * Wn){
A[i] = A0[i] + A1[i] * w ;//应用公式,下同
A[i + (lenth >> 1)] = A0[i] - A1[i] * w ; //顺便求出另一半,由折半引理可显然。
}
}
function Main{
input(N), input(M) ;
for(i : 0 to N by_grow 1) => input(A) ;
for(i : 0 to M by_grow 1) => input(B) ;
while(Lim < N + M) Lim <<= 1 ;//Lim为结果多项式的长度(暂时),化为2的幂的便于分治(二分)
FFT(Lim, A, 1) ;//两遍FFT表示从系数化为点值
FFT(Lim, B, 1) ;
for(i : 0 to Lim by_grow 2) => A[i] *= B[i] ;//点乘法,此处需要重定义乘号,因为每一项现在表示的是一个点,有x和y两个属性qwq
}
cpp:
点击查看代码
void FFT(int Lim,complex *A,int flag){
if(Lim == 1) return ;
complex A0[Lim >> 1], A1[Lim >> 1] ;
for(int i = 0; i <= Lim ; i += 2)
A0[i >> 1] = A[i], A1[i >> 1] = A[i+1] ;
FFT(Lim >> 1, A0, flag) ;
FFT(Lim >> 1, A1, flag) ;
complex unit = (complex){cos(2.0 * Pi / Lim) , flag * sin(2.0 * Pi / Lim)}, w = complex(1, 0) ;//欧拉公式
for(int i = 0;i < (Lim >> 1) ; i ++, w = w * unit) {
A[i] = A0[i] + w * A1[i] ;
A[i + (Lim>>1)] = A0[i] - w*A1[i];
}
}
int main(){
......................
FFT(A, 1), FFT(B, 1) ;
for(i = 0; i <= Lim; i ++) A[i] = A[i] * B[i] ;
FFT(A, -1) ;
......................
}
最后
本蒟蒻的第一篇写完了的数论笔记。
后面来更FFT的优化。
感谢冯巨,
引用:https://www.cnblogs.com/pks-t/p/9251147.html
%%%%%%%%%%%%%%%%%%%%%%%%