蒟蒻数论笔记:讲给数论小白的快速傅里叶变换
题外话
我感觉自己的数论笔记从来没有写完过,从极限到微积分到积型函数到FFT
2021.11.10 为啥过了一个晚上就有15个人看了???
正题
1.傅里叶变换:
有关傅里叶变换,##先看视频##
2.前置芝士之泰勒展开
(这里不理解也没有什么问题,知道结论即可)
泰勒展开的本质就是用一个多项式来拟合一个函数在处的变化,为此要做到每一次求导都与原函数相同,以此来拟合函数的变化。
分母是幂函数求导带来的常数项约去
所以,我们可以用它来展开几个函数:其中为
已知:
拓展到复数:
放缩:
已知:
乘上一个复数:
已知:
得出:
也就是欧拉公式。
带入得:
可以理解为,随着的增大,也在一个圆上运动。
于是结合上面的视频可以理解傅里叶变换的公式:
这里变成了向量
加负号是因为变换旋转的方向。
普通的傅里叶是的,而快速傅里叶可以做到。
2.前置芝士之复数运算
看到前面的复数运算,有些同学可能已经有点搞不懂了,我一开始也是这样的。
这里就简单讲一下复数的运算,就是数学选修2-2的最后一章,(可以自己看)。
简单来说,复数集是对实数集的一个扩充,在定义上,我们这样来看复数:
我们把这个集合中的数,形如的叫做复数,是虚数单位,是复数集。
为了简洁,我们直接开始四则运算:
3.前置芝士之多项式卷积
先推荐一个回答
博主讲的很好。
像乘法,也可以变成多项式乘法,然后卷积。
快乐的
在FFT中,我们就是把两个多项式变成了两组向量,然后两组向量卷积求新的向量。
4.前置芝士之点乘法
n阶的函数由n+1个点唯一确定。
传统的多项式是用系数表示法,我们可以换为点值表示法来表示。
两个与有关的多项式:
两个相加就直接加上即可,相乘的话比较麻烦。
因为的项数,所以两个式子都要补上一堆项,然后项多项式加法一样按位运算。
乘法:
现在我们见证了奇迹的诞生!!的时间复杂度!!!
所以,我们到现在就知道了FFT的流程:
1.得到两个多项式A,B;
2.对于每个多项式,选取n+1个点,得出点值表达
3.点乘法
4.将的点值表达式转化为系数表达式。
5.前置芝士之拉格朗日插值
虽然没有用,但是还是要讲一下。
插值主要用在将点值表达式转换为系数表达式上,也就是一个阶的函数由个不同的点唯一确定。
常见的插值法是拉格朗日插值法。
由于有n个点,求n-1阶函数。我们先枚举每一个点,建立一个函数,使得,而其他的带入,。
然后,一整个函数就是由这个函数累加起来的和,在没一个点上的值都对应其。
然后构造出公式:
这就可以用来完成FFT的第三步,但是时间复杂度是。FFT继续优化。
读到这里结合前面的芝士,你已经知道了DFT以及IDFT了,接下来就是FFT的诞生之路
FFT优化之路
6.前置芝士之单位复根
n次的单位复根是满足的复数,因为在复数平面上的模都是一,所以相乘之后还是一,那么所有的都会均匀度分布在单位圆上,时;
我们再考虑欧拉公式:
取
从而:
把此时的单位根为主次单位根:
对于其他单位根,
第一步的优化:快速傅里叶变换之求点值
对一个多项式求点值,我们带入2n个点求点值,并强选个单位根,保证n为2的整数次幂,那么把带为n。
得:
有:
所以多项式的点值为:
考虑把每个点值按照奇偶分开:
左边提一个
然后,把左边变成一个系数为的多项式,带入,也就是(看上面的芝士即可懂了)。
把右边也变成一个系数为的多项式,带入。
得:
当时:
这时我们发现前后的值是一样的,就可以折半计算了:
啊啊啊啊!!冯巨讲的时候好迷茫现在看来真NB!!
第三步的优化:快速傅里叶逆变换之求系数
在前置芝士之拉格朗日插值中,我们在求了的点值表达式后,运用拉格朗日插值法,求出系数表达式。
在这里,我们可以用快速傅里叶变换的优秀性质来进行逆变换。
首先,多项式的点值
我们用来表示上面的式子进行快速傅里叶变换后的。
我们对这些又进行一次快速傅里叶变换:
恒等变换:
基本操作交换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
%%%%%%%%%%%%%%%%%%%%%%%%
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效