【笔记篇】(理论向)快速傅里叶变换(FFT)学习笔记w
现在真是一碰电脑就很颓废啊...
于是早晨把电脑锁上然后在旁边啃了一节课多的算导, 把FFT的基本原理整明白了..
但是我并不觉得自己能讲明白...
Fast Fourier Transformation, 快速傅里叶变换, 是DFT(Discrete Fourier Transform, 离散傅里叶变换)的快速实现版本.
据说在信号处理领域广泛的应用, 而且在OI中也有广泛的应用(比如SDOI2017 R2至少考了两道), 所以有必要学习一波..
划重点: 其实学习FFT最好的教材是《算法导论》, 里面讲的很是清楚, 建议大家用心把FFT这一章节通读一遍, 我这种zz也就看了大约一节课的时间就看完了w
在信号处理领域的应用我也不会也讲不了, 所以我们就说说求多项式乘法吧..
首先是多项式的定义: (我们不生产定义, 我们只是算导的搬运工)
A(x)=\sum_{j=0}^{n-1}a_jx^j
\]
为什么循环变量不用$i$呢, 是因为要用到复数(后面会提到哒~
然后还有一些概念比如
- 系数: 每个$a_i$. 所有系数属于域F, 典型的情形是复数集合C
- 次数: 最高次的非零系数为$a_k$, 则次数为$k$.
- 次数界: 任何一个严格大于多项式次数的整数. 因此, 对于次数界为$n$的多项式, 次数的取值范围是${x\in N|0\leq x\leq n-1}$
然后是多项式的表达方式. 多项式的表达方式有两种: 系数表达和点值表达.
这个不难理解, 根据上面的定义, 一个次数界为$n$的多项式可以用一个由系数组成的向量$a=(a_0,a_1,...a_)$来唯一确定, 这个就是这个多项式的系数表达.
而点值表达则是说, 对于$A(x)=\sum_a_jxj$这个式子, 我们可以用一个至少包含$n$个不同的点的集合来唯一确定. 这个点集就是多项式的点值表达, 当然, 点值表达不是唯一的.
举个栗子: 有一个多项式$A(x)=x3+2x2+4$, 它的系数表达可以是$a=(1,2,0,4)\(, 而点值表达则可以是\){(-1,4),(0,4),(1,6),(2,16)}$.
然后我们就可以定义运算:
- 多项式的加法
对于系数表达来说, 大家已经非常熟悉了, 就是合并同类项嘛, 没什么好说的, 时间复杂度$O(n)$
对于点值表达来说, 我们可以选取$x$相同的$n$个点, 然后把$y$相加就行了, 时间复杂度也是$O(n)$ - 多项式的乘法
对于系数表达来说, 是大家熟悉的形式, 就是一个多项式的每一项分别与另一个多项式的每一项相乘, 然后再合并同类项, 那么时间复杂度就是$O(n^2)$的.
对于点值表达来说, 我们依然选$x$相同的$n$个点, 然后把$y$相乘就行了, 时间复杂度依然是$O(n)$
不过有一点要注意, 就是$C(x)$的次数界不再是$n$, 而是$2n$(因为次数界$2n-1$的多项式我们同样可以说次数界是$2n$, 所以为了方便我们就说$2n$了),
所以我们要对点值进行扩展, 选取$2n$个点逐个相乘, 不过并不影响复杂度.
所以说在点值表达的情况下我们可以$O(n)$计算多项式的乘法, 但系数表达则不行. 那我们能不能让系数表达的多项式乘法快一点呢?
我们可以试图在比较好的复杂度下把系数表达转化为点值表达, 然后乘完再转化会系数表达..
(P.S. 我们管系数表达转化为点值表达的操作叫求值, 求值的逆操作叫插值.)
然后我们省略关于求值和插值的一吨证明过程(因为看的云里雾里
不过我们可以知道, 如果只是看心情取$n$个点代入计算点值的话, 求值的时间复杂度就是$O(n2)$;
而采用拉格朗日插值法, 就可以用$O(n2)$的时间复杂度来进行插值.
但这显然不是我们想要的复杂度.
这时候就需要FFT了, 通过精心的挑选求值点, 我们可以巧妙地将两种表达间的转化的复杂度降为$O(nlogn)$.
那我们就可以通过系数->点值->系数的方式, 用$O(nlogn)$的复杂度完成系数表达下的多项式乘法.
那要怎么选点呢? 我们选取的是单位复数根.
蛤? 这是个什么玩意? (点上面的链接去baidu看一下咯~
$n$次单位复数根就是满足$\omegan=1$的复数$\omega$. 因为$n$次方程有$n$个复根, 所以$n$次单位复数根有$n$个.
这$n$个根分别是$e{2\pi ik/n} (k=0,1,2,...,n-1)$. 为了解释这玩意, 我们利用复数的指数形式的定义
e^{iu}=\cos(u)+i\sin(u)
\]
然后这几个根是均匀的分布在以复平面的原点为圆心的单位半径的圆周上的.
其中$\omega_n=e^{2\pi i/n}$称为主$n$次单位根, 所有其他$n$次单位复根都是它的幂次.
然后就是一堆引理... 只是贴一下~~(要不是有用贴都不想贴)~~, 证明见算导P532
消去引理: 对于任何整数$n\geq0,k\geq0$, 以及$d>0$, \(\omega_{dn}^{dk}=\omega_n^k\)
推论: 对于任意偶数$n>0$, 有$\omega_n^{n/2}=w_2=-1$.
折半引理: 如果$n>0$为偶数, 那么$n$个$n$次单位复根的平方的几何就是$n/2$个$n/2$次单位复数根的集合.
求和引理: 对于任意整数$n\geq 1$和不能被$n$整除的非负整数$k$, 有
$$
\sum_(\omega_nk)^j=0
$$
回到我们的多项式乘法问题, 我们希望计算次数界为$n$的多项式 (这里的$n$已经是原数据规模中$n'$的两倍了)
A(x)=\sum_{j=0}^{n-1}a_jx^j
\]
在$n$次单位复根处的取值, 而此时我们有一个系数向量$a=(a_0,a_1,...a_)$, 我们令
y_k=A(\omega_n^k)=\sum_{j=0}^{n-1}a_j{\omega_n^k}^j
\]
我们就可以获得一个点值向量$y=(y_0,y_1,...,y_)$., 我们称$y$为$a$的离散傅里叶变换(传说中的DFT) , 也可以记为$y=DFT_n(a)$.
好的现在重头戏登场, 我们来讲一下FFT.
首先显然直接计算DFT的复杂度是$O(n^2)$, 那怎么优化呢? 这就要用到了我们非常常见的一种思想: 分治!
我们第一步做一个合理(?)的假设, $n$是2的整数次幂. 那如果不是呢? 有别的(更好(nan)的)方法, 但是我们不用.
我们就强行扩充成2的整数次幂...(好像zkw线段树也是这么干的..) 如果原问题的数据规模是513, 我们也要扩充成1024, 然后再翻个倍变成2048.. (好像有点浪费?)
然后FFT利用了分治的策略, 将奇数项和偶数项分别提出来.
A(x)=(a_0+a_2x^2+a_4x^4+...+a_{n-2}^x{n-2})+(a_1x+a_3x^3+a_5x^5+...+a_{n-1}x^{n-1})
\]
然后我们把两个括号分别搞成两个式子, 从后面的括号里提一个$x$, 然后换元, 用$x$来表示$x^2$, 能得到
A^{[0]}(x)=a_0+a_2x+a_4x^2+...+a_{n-2}x^{n/2-1}\\
A^{[1]}(x)=a_1+a_3x+a_5x^2+...+a_{n-1}x^{n/2-1}\\
A(x)=A^{[0]}(x^2)+xA^{[1]}(x^2)
\]
这样我们就把问题转化为了求次数界为$n/2$的多项式$A^{[0]},A^{[1]}\(在点\)(\omega_n^0)2,(\omega_n1)2,...,(\omega_n)^2$处的取值.
而根据折半引理, 这$n$个取值是由$n/2$个值每个值出现两次构成的, 问题规模就从$n$变成了$n/2$, 所以我们继续递归分治下去就可以求出来了.
时间复杂度$T(n)=2T(n/2)+O(n)=>O(nlogn)$.
根据上面的思路我们就可以写出伪代码.. (决定向zky神犇一样用python的高亮...
# 伪代码哟~
FFT(a,n): # 求一个n维向量a的DFT
if(n==1):
return a # 递归终止的条件
wn=e^(2*pi*i/n)=cos(2*pi/n)+sin(2*pi/n)*i # 定义枚举(旋转)的方向, 这个是逆时针旋转的(编号递增)
a0=[a_0,a_2,...,a_n-2]
a1=[a_1,a_3,...,a_n-1] # 按照奇偶分成两半
y0=FFT(a0,n/2)
y1=FFT(a1,n/2) # 递归处理
for k in range(0,n/2): # 合并操作
y[k]=y0[k]+w*y1[k]
y[k+n/2]=y0[k]-w*y1[k] # 折半引理
w=w*wn #下一个单位复根
return y
差不多就是这样, 如果上面基本理解的话这里应该就没啥太大问题了.. 看不懂的话算导P534有将近一页的对伪代码的补充说明...
然后我们已经能求值了, 现在来考虑插值.
哎呀证明什么的又要用到矩阵 啊看不懂.... 知道能证明就行了...
我们可以欣赏编写算导的人一步一步推导出逆DFT(又称IDFT) \(DFT_n^{-1}(y)\):
a_j=\frac1n\sum_{k=0}^{n-1}y_k{\omega_n^{-k}}^j
\]
然后我们跟之前求DFT要求的
y_k=A(\omega_n^k)=\sum_{j=0}^{n-1}a_j{\omega_n^k}^j
\]
比较一下, 可以得出, 我们只需要把$a,y$互换, 用$\omega_n^{-1}\(替换\)\omega_n$, 最后将计算结果都除以$n$就行了.
这样我们就可以很轻松地写(chao)出伪代码: (顺便完成了练习30.2-4
IFFT(a,n): # 求一个n维向量a的DFT
if(n==1):
return a # 递归终止的条件
wn=e^(2*pi*i/n)=cos(2*pi/n)-sin(2*pi/n)*i # 顺时针
y0=[y_0,y_2,...,y_n-2]
y1=[y_1,y_3,...,y_n-1]
a0=FFT(y0,n/2)
a1=FFT(y1,n/2)
for k in range(0,n/2): # 合并操作
a[k]=a0[k]+w*a1[k]
a[k+n/2]=a0[k]-w*a1[k] # 折半引理
w=w*wn #下一个单位复根
return a
这样我们也完成了$O(nlogn)$的IDFT. 我们已经可以$O(nlogn)$解决FFT问题了.
我们研究一个算法肯定是要尽可能的快, 所以我们考虑能不能优化一下算法的常数.
首先我们看到循环里面有两个$\omega_nk*y_k{[1]}$, 我们可以采用一个局部变量$t$来存一下, 把循环搞成这样:
for k in range(0,n/2):
t=w*y1[k]
y[k]=y0[k]+t
y[k+n/2]=y0[k]-t
w=w*wn
这个操作有个很好听的名字, 叫"蝴蝶操作".
好像什么对称的东西都能想到蝴蝶?? (脊髓灰质瑟瑟发抖) 果然是贫穷限制了我的想象力吧~
然后我们就可以化一下递归树, 来找一下规律. 我们发现这棵树是长这样的:
这样我们发现其实调用的时候并非是自顶向下, 而是自底向上, 所以我们可以试着把递归改成迭代.
我们看一下最底层有什么规律. 我们把这些数的下标化成二进制:
000 100 010 110 001 101 011 111
那这不就是0~7分别的二进制倒过来嘛...
我们可以非常容易地处理出这个数组. 算导上甚至认为特别简单都没有给代码...
我们用C++写起来大约可以这样(各种奇怪的位运算):
void rev(cp *ar){
memset(vis,0,sizeof(vis));
for(ri i=1;i<n-1;++i){
int x=i,y=0;
if(vis[x]) continue;
for(ri j=1;j<n;j<<=1)
y=(y<<1)|(x&1),x>>=1;
vis[i]=vis[y]=1; swap(ar[i],ar[y]);
}
}
然后我们就可以把代码改成:
for s in range(1,(logn)+1):
for k in range(0,n-1,2**s):
combine... # 这一行太长了 想看的去算导翻吧, 反正写这一句也没啥用
那么我们把这一行拆开是得到下面的伪代码:
FFT2(a,n):
REVERSE(a) # 数组归位
for s in range(1,(logn)+1): # 枚举层数
m=2**s #处理的长度
wm=cos(2*pi/m)+i*sin(2*pi/m) #在这一层的单位根的旋转单位
for i in range(0,m/2): #上面的k
t=w*a[k+j+m/2]
u=a[k+j] #防止被覆盖 多申请一个变量
a[k+j]=u+t
a[k+j+m/2]=u-t
w=w*wm
return a
这样我们就成功把递归改成了迭代, 节约了常数..
复杂度是没有改变的, 证明见算导P538中间.
这样我们就讲完了..
有一道练习题, 高精度乘法
首先朴素的高精度乘法是$O(n2)$的, 好像$n\leq6*104$压位可过...
不过我们还是来练习一下FFT..
我们可以把一个大整数视为一个
\sum_{j=0}^{n-1}a_j10^j
\]
的一个多项式, 我们用FFT求出乘积的多项式的各个系数, 然后依次处理一下进位, 去掉前导0就可以咯~
C++实现代码:
#include <cmath>
#include <cstdio>
#include <vector>
#include <cstring>
#define ri register int
using namespace std;
const int N=150000;
const double pi=acos(-1);
const double eps=1e-9;
struct cp{
double r,i;
cp(double R=0,double I=0):r(R),i(I){}
}; //手写复数(据说用STL的complex会T?)
cp a[N],b[N];bool vis[N];int n=1;
cp operator+(const cp& a,const cp& b){return cp(a.r+b.r,a.i+b.i);}
cp operator-(const cp& a,const cp& b){return cp(a.r-b.r,a.i-b.i);}
cp operator*(const cp& a,const cp& b){return cp(a.r*b.r-a.i*b.i,a.r*b.i+a.i*b.r);}
void rev(cp *ar){
memset(vis,0,sizeof(vis));
for(ri i=1;i<n-1;++i){
int x=i,y=0;
if(vis[x]) continue;
for(ri j=1;j<n;j<<=1)
y=(y<<1)|(x&1),x>>=1;
vis[i]=vis[y]=1; swap(ar[i],ar[y]);
}
}
void fft(cp *y,bool f){ rev(y);//f=true表示IDFT f=false表示DFT
for(ri m=2;m<=n;m<<=1){
cp wm(cos(2*pi/m),f?sin(2*pi/m):-sin(2*pi/m));
for(ri k=0;k<n;k+=m){
cp w(1,0);
for(ri j=0;j<m/2;++j){
cp t=w*y[k+j+m/2],u=y[k+j];
y[k+j]=u+t;
y[k+j+m/2]=u-t;
w=w*wm;
}
}
}
if(!f) for(int i=0;i<n;++i) y[i].r/=n;
}
char c1[N],c2[N];int c[N];
int main(){
int nn,l1,l2; scanf("%d",&nn);
for(n=1;n<nn;n<<=1); n<<=1;
scanf("%s%s",c1,c2);
l1=strlen(c1),l2=strlen(c2);
for(ri i=0;i<l1;++i)a[i]=cp(c1[l1-i-1]-48);fft(a,1); //DFT a
for(ri i=0;i<l2;++i)b[i]=cp(c2[l2-i-1]-48);fft(b,1); //DFT b
for(ri i=0;i<n;++i)a[i]=a[i]*b[i];fft(a,0); //IDFT a*b
for(ri i=0;i<n;++i)c[i]=a[i].r+0.5; //这个地方要四舍五入(精度感人)
for(ri i=0;i<n;++i)c[i+1]+=c[i]/10,c[i]%=10; //处理进位
while(!c[n]&&n>0) --n; //干掉前导0
for(ri i=n;i>=0;--i)putchar(c[i]+48);
}
python实现代码:
n=int(input())
a=int(input())
b=int(input())
print(a*b)
(废话, 这种题有python谁写FFT啊..)
py大法好!!
就这样吧~
【笔记篇】(理论向)快速傅里叶变换(FFT)学习笔记w的更多相关文章
- 再探快速傅里叶变换(FFT)学习笔记(其三)(循环卷积的Bluestein算法+分治FFT+FFT的优化+任意模数NTT)
再探快速傅里叶变换(FFT)学习笔记(其三)(循环卷积的Bluestein算法+分治FFT+FFT的优化+任意模数NTT) 目录 再探快速傅里叶变换(FFT)学习笔记(其三)(循环卷积的Blueste ...
- 快速傅里叶变换(FFT)学习笔记(其一)
再探快速傅里叶变换(FFT)学习笔记(其一) 目录 再探快速傅里叶变换(FFT)学习笔记(其一) 写在前面 为什么写这篇博客 一些约定 前置知识 多项式卷积 多项式的系数表达式和点值表达式 单位根及其 ...
- 快速傅里叶变换(FFT)学习笔记(其二)(NTT)
再探快速傅里叶变换(FFT)学习笔记(其二)(NTT) 目录 再探快速傅里叶变换(FFT)学习笔记(其二)(NTT) 写在前面 一些约定 前置知识 同余类和剩余系 欧拉定理 阶 原根 求原根 NTT ...
- 快速傅里叶变换(FFT)学习笔记
定义 多项式 系数表示法 设\(A(x)\)表示一个\(n-1\)次多项式,则所有项的系数组成的\(n\)维向量\((a_0,a_1,a_2,\dots,a_{n-1})\)唯一确定了这个多项式. 即 ...
- 【文文殿下】快速傅里叶变换(FFT)学习笔记
多项式 定义 形如\(A(x)=\sum_{i=0}^{n-1} a_i x^i\)的式子称为多项式. 我们把\(n\)称为该多项式的次数界. 显然,一个\(n-1\)次多项式的次数界为\(n\). ...
- 快速傅里叶变换FFT学习小记
FFT学得还是有点模糊,原理那些基本还是算有所理解了吧,不过自己推这个推不动. 看的资料主要有这两个: http://blog.miskcoo.com/2015/04/polynomial-multi ...
- 【笔记篇】单调队列优化dp学习笔记&&luogu2569_bzoj1855股票交♂易
DP颂 DP之神 圣洁美丽 算法光芒照大地 我们怀着 崇高敬意 跪倒在DP神殿里 你的复杂 能让蒟蒻 试图入门却放弃 在你光辉 照耀下面 AC真心不容易 dp大概是最经久不衰 亘古不化的算法了吧. 而 ...
- [学习笔记] 多项式与快速傅里叶变换(FFT)基础
引入 可能有不少OIer都知道FFT这个神奇的算法, 通过一系列玄学的变化就可以在 $O(nlog(n))$ 的总时间复杂度内计算出两个向量的卷积, 而代码量却非常小. 博主一年半前曾经因COGS的一 ...
- 【学习笔记】快速傅里叶变换(FFT)
[学习笔记]快速傅里叶变换 学习之前先看懂这个 浅谈范德蒙德(Vandermonde)方阵的逆矩阵的求法以及快速傅里叶变换(FFT)中IDFT的原理--gzy hhh开个玩笑. 讲一下\(FFT\) ...
随机推荐
- CentOS 7 启用中文输入法
$HOME/.xinitrc LANG="zh_CN.UTF-8" exec startxfce4
- 剑指offer——49礼物的最大价值
题目描述 在一个m*n的棋盘的每一格都放有一个礼物,每个礼物都有一定的价值(价值大于0).你可以从棋盘的左上角开始拿格子里的礼物,并每次向左或者向下移动一格,知道到达棋盘的右下角.给定一个棋盘及其上面 ...
- jquery 弥补ie6不支持input:hover状态
<!doctype html><html> <head> <meta charset="utf-8"> <t ...
- 深入分析Synchronized原理
前言 记得开始学习Java的时候,一遇到多线程情况就使用synchronized,相对于当时的我们来说synchronized是这么的神奇而又强大,那个时候我们赋予它一个名字“同步”,也成为了我们解决 ...
- 最长递增子序列nlogn的做法
费了好大劲写完的 用线段树维护的 nlogn的做法再看了一下 大神们写的 nlogn 额差的好远我写的又多又慢 大神们写的又少又快时间 空间 代码量 哪个都赶不上大佬们的代码 //这是我写的 ...
- 笔记-ubuntu中/home下中文目录改英文
安装ubuntu后,如果选择的语言是中文,那/home下的文件夹会默认中文,在使用命令行的时候很不方便,此文记录切换成英文的方式,以便日后查看. 将目录重命名为英文 可以使用图形化界面,直接重命名 可 ...
- RHEL5/6/7中常用命令及命令之间的差异
System basics Task RHEL5 RHEL6 RHEL7 View subscription information /etc/sysconfig/rhn/systemid /etc/ ...
- Java HashSet和ArrayList的查找Contains()时间复杂度
今天在刷leetCode时,碰到了一个题是这样的. 给定一个整数数组,判断是否存在重复元素. 如果任何值在数组中出现至少两次,函数返回 true.如果数组中每个元素都不相同,则返回 false. 看到 ...
- thinkphp 数据缓存
在ThinkPHP中进行缓存操作,一般情况下并不需要直接操作缓存类,因为系统内置对缓存操作进行了封装,直接采用S方法即可,例如: 缓存初始化 // 缓存初始化 S(array('type'=>' ...
- <转载>深入 理解char * ,char ** ,char a[ ] ,char *a[] 的区别
C语言中由于指针的灵活性,导致指针能代替数组使用,或者混合使用,这些导致了许多指针和数组的迷惑,因此,刻意再次深入探究了指针和数组这玩意儿,其他类型的数组比较简单,容易混淆的是字符数组和字符指针这两个 ...