【Learning】多项式乘法与快速傅里叶变换(FFT)
简介:
FFT主要运用于快速卷积,其中一个例子就是如何将两个多项式相乘,或者高精度乘高精度的操作。
显然暴搞是$O(n^2)$的复杂度,然而FFT可以将其将为$O(n lg n)$。
这看起来十分玄学,因为怎么看它们的相乘操作都逃不过$O(n^2)$,FFT是如何再减少复杂度的呢?
讲到FFT就不可避免地出现公式,但实际上它们都是比较容易理解的。
全局思路
设两个次数界均为$n$的多项式$\begin{aligned}A(x)&=a_0x^0+a_1x^1+a_2x^2+...+a_{n-1}x^{n-1}\\B(x)&=b_0x^0+b_1x^1+b_2x^2+...+b_{n-1}x^{n-1}\end{aligned}$
那么我们要求$C=A*B$。
我们把$A$、$B$和$C$的函数图像画出来,考虑每一个取值点:$C(x_1)=A(x_1)*B(x_1)$,$C(x_2)=A(x_2)*B(x_2)$,$C(x_3)=A(x_3)*B(x_3)$......
如果我们能对$A$和$B$求出它们在$x_1,x_2,...x_{2n-1}$处的值$A(x_1),A(x_2),...,A(x_{2n-1})$与$B(x_1),B(x_2),...,B(x_{2n-1})$。
那么将它们一一对应地相乘,就可以得到$C(x_1),C(x_2),...,C(x_{2n-1})$。其中,$C(x)=A(x)*B(x)$。
接着,我们可以利用$C$的这$2n-1$个点,对$C$进行插值,求出$C$的解析式。由于两个次数界为$n$的多项式相乘后是一个次数界为$2n-1$的多项式,因此我们需要$2n-1$个点,才能对$C$进行准确插值。
问题是,上面三步的时间复杂度分别为$O(n^2)$、$O(n)$和$O(n^2)$,还是没有什么改进。它们的名字分别是:DFT,点值乘法,逆DFT。
改进以后,它们分别是FFT,点值乘法,逆FFT。时间复杂度分别为$O(nlgn)$,$O(n)$,$O(nlgn)$。
那就来改进吧(DFT $O(n^2)$ ---> FFT $O(nlgn)$)
本质老是没有飞跃,多半是废了,有一个因素是取值点$x_1,x_2,...,x_{2n-1}$停留在实数范围内,没有太多特殊性质。
但如果用复数呢?
定义$n$次单位复数根为满足$\omega^n=1$的复数$\omega$。$n$次单位复数根恰好有$n$个:$\omega_n^0,\omega_n^1,...,\omega_n^{n-1}$,它们的$n$次方都为1。
其中,$\omega_n^0=e^{2\pi i/n}$, $\omega_n^x=(\omega_n^0)^x$, $\omega_n^{n/2}=(e^{2\pi i/n})^{n/2}=e^{\pi i}=-1$。
(以下用$n$来代替之前提到的$2n-1$;用$A$表示一个次数界为$n$的多项式,上文提到的"$A$"和"$B$"的操作都是同理的)
我们把这$n$个$n$次单位复数根作为取值点,求出$A(\omega_n^0),A(\omega_n^1),...,A(\omega_n^{n-1})$。
这一步叫做离散傅里叶变换(DFT)。对于$k=0,1,...,n-1$,它要求$y_k=A(\omega_n^k)=\sum\limits_{j=0}^{n-1}a_j\omega_n^{kj}$
然而若不利用单位复数根的性质,复杂度仍然是$O(n^2)$的。
单位复数根的性质
这$n$个复数有神秘性质,主要用到三个:
1. $\omega_n^{k+n/2}=\omega_n^k*\omega_n^{n/2}=\omega_n^k*-1=-\omega_n^k$,
什么意思呢:比如$n=8$时,$n$个单位负数根,$\omega_n^0$和$\omega_n^4$互为相反数,$\omega_n^1$和$\omega_n^5$互为相反数.....也就是$[0,n/2-1)$与$[n/2-1,n)$对应的单位复数根互为相反数。
2.消去引理:$\omega_{an}^{ak}=(e^{2\pi i/an})^{ak}=(e^{2\pi i/n})^k=\omega_n^k$,类似于分数约分。
3.折半引理:$$\begin{aligned}(\omega_n^k)^2&=\omega_n^{2k}=\omega_{n/2}^k\\(\omega_n^{k+n/2})^2=\omega_n^{2k+n}=\omega_n^{2k}*\omega_n^n&=\omega_n^{2k}=\omega_{n/2}^k\end{aligned}$$
什么意思呢?如果把$n$个单位根分成两组$\omega_n^0...\omega_n^{n/2-1}$ 和 $\omega_n^{n/2}...\omega_n^{n-1}$,两两对应位置的单位根的平方是相同的。
如$n==8$时:
$(\omega_8^0)^2=(\omega_8^4)^2=\omega_{4}^0\\(\omega_8^1)^2=(\omega_8^5)^2=\omega_{4}^1\\(\omega_8^2)^2=(\omega_8^6)^2=\omega_{4}^2\\(\omega_8^3)^2=(\omega_8^7)^2=\omega_{4}^3$
也就是$n$个$n$次单位复数根的平方的集合,等于$n/2$个$n/2$次单位复数根的集合。
多项式的拆分
我们回来看一下$A$可以如何拆分:记$A$的系数为$a_0,a_1,...,a_{n-1}$。
如果我们设$\begin{aligned}A_0(x)&=a_0x^0+a_2x^1+a_4x^2+...+a_{n-2}x^{n/2}\\A_1(x)&=a_1x^0+a_3x^1+a_5x^2+...+a_{n-1}x^{n/2}\end{aligned}$,也就是将$A$的系数奇偶分组,成为两个次数界为$n/2$的多项式。
那么有$$A(x)=A_0(x^2)+x*A_1(x^2)$$
我们求的是$A(\omega_n^0),A(\omega_n^1),...,A(\omega_n^{n-1})$,那么转换一下就变成求
$$\begin{aligned}
A(\omega_n^0)&=A_0((\omega_n^0)^2)+\omega_n^0*A_1((\omega_n^0)^2)\\
A(\omega_n^1)&=A_0((\omega_n^1)^2)+\omega_n^1*A_1((\omega_n^1)^2)\\
&...\\
A(\omega_n^{n-1})&=A_0((\omega_n^{n-1})^2)+\omega_n^{n-1}*A_1((\omega_n^{n-1})^2)\\
\end{aligned}$$
求解$A_0$和$A_1$在$n$个单位复数根,我们用递归实现。
我们发现代入$A_0$和$A_1$的参数是一个单位复数根的平方,这意味着代入$A_0$和$A_1$的单位复数根并没有$n$个。根据折半引理,代入$A_0$和$A_1$的总共只有$n/2$个不同的数:$\omega_{n/2}^0,\omega_{n/2}^1,...,\omega_{n/2}^{n/2-1}$,因为$(\omega_n^k)^2=(\omega_n^{k+n/2})^2$。
我们像上面把单位复数根分为$[0,n/2)$和$[n/2,n)$两组,观察$A(\omega_n^k)$和$A(\omega_n^{k+n/2})$,也就是相对的两个单位复数根的代入:
\begin{aligned}
A(\omega_n^k)&=A_0(\omega_{n/2}^k)+\omega_n^k*A_1(\omega_{n/2}^k)\\
A(\omega_n^{k+n/2})&=A_0(\omega_{n/2}^k)+\omega_n^{k+n/2}*A_1(\omega_{n/2}^k)\\
&=A_0(\omega_{n/2}^k)-\omega_n^k*A_1(\omega_{n/2}^k)
\end{aligned}
它们长得好像!
这下可好,我们只需要递归求解$A_0(\omega_n^0...\omega_n^{n/2-1})$和$A_1(\omega_n^0...\omega_n^{n/2-1})$,就可以求出$A(\omega_n^0...\omega_n^{n-1})$了。
时间复杂度下降的原因就在于,用$n/2$次的递归得到的数据,可以求出右半边的数值。
点值乘法 (呵呵 $O(n)$ 不优化了吧这个)
对于$A$和$B$都进行DFT后,我们对$n$个点值直接相乘,得到$C$的$n$个点值。
IFFT (IDFT $O(n^2)$ ---> IFFT $O(nlgn)$)
如果我们知道$C$的$n$个点值,如何知道$C$的解析式呢?
我们看一下DFT的矩阵形式:$y=V_na$,分别与下式对应:
$$\begin{bmatrix}
y_0\\y_1\\y_2\\y_3\\.\\.\\y_{n-1}
\end{bmatrix}
=
\begin{bmatrix}
1&1&1&1&...&1\\
1&\omega_n&\omega_n^2&\omega_n^3&...&\omega_n^{n-1}\\
1&\omega_n^2&\omega_n^4&\omega_n^6&...&\omega_n^{2(n-1)}\\
1&\omega_n^3&\omega_n^6&\omega_n^9&...&\omega_n^{3(n-1)}\\
...&...&...&...&...&...\\
1&\omega_n^{n-1}&\omega_n^{2(n-1)}&\omega_n^{3(n-1)}&...&\omega_n^{(n-1)(n-1)}
\end{bmatrix}
*
\begin{bmatrix}
a_0\\a_1\\a_2\\a_3\\.\\.\\a_{n-1}
\end{bmatrix}$$
我们所求的是$a$,而$a=yV_n^{-1}$,求出$V_n$的逆矩阵即万事大吉了。
定理:对于$V_n$,$(k,j)$处的元素为$\omega_n^{kj}$。
而对于$V_n^{-1}$,$(k,j)$处的元素为$\omega_n^{-kj}/n$。
如果想简单证明的话,将$V_n^{-1}$写出来算一算就好。(可以参见算导)
那么$a_j=\frac{1}{n}\sum\limits_{k=0}^{n-1}y_k\omega_n^{-kj}$。
看回上面DFT的算式表达,我们发现它们长得几乎一样:IFFT的表达,仅仅是多了一个$\frac{1}{n}$,以及单位复数根的指数取负数。
这就非常棒了:IFFT的程序其实和FFT一样,只不过单位复数根替换一下,算完以后,每一个数值都除去$n$即可,具体参见代码解释。
END
FFT的应用,主要是将问题转化成如DFT式子的形式,用FFT来进行加速或计算的操作。
附上递归版代码和非递归版代码:
#include <cstdio>
#include <vector>
#include <cmath>
#define max(a,b) ((a)>(b)?(a):(b))
using namespace std;
const int N=;
const double Pi=3.14159265358979323846;
struct Comp{//手写了一个复数类
double a,b;
Comp(){a=b=0.0;}
Comp(double x,double y){a=x;b=y;}
friend Comp operator + (Comp x,Comp y){
return Comp(x.a+y.a,x.b+y.b);
}
friend Comp operator - (Comp x,Comp y){
return Comp(x.a-y.a,x.b-y.b);
}
friend Comp operator * (Comp x,Comp y){
return Comp(x.a*y.a-x.b*y.b,x.b*y.a+x.a*y.b);
}
};
typedef vector<Comp> vc;
int A,B,type,len;
vc a,b,c;
vc fft(vc u,int flag){//flag标识是否为逆FFT
int n=u.size();
if(n==) return u;//规模为1时,只有一个常数项的多项式的FFT就为这个常数,可以直接返回了
Comp w_n=Comp(cos(*Pi/n),sin(*Pi/n)),w=Comp(,);//算出单位复数根的底w_n;w是用来迭代的,减少计算次数
if(flag) w_n.b*=-1.0;//逆FFT与FFT的不同
vc a0,a1,v;
a0.clear(); a1.clear(); v.clear();
for(int i=;i<n;i++){//系数按奇偶分组
if(i&) a1.push_back(u[i]);
else a0.push_back(u[i]);
v.push_back(Comp(,));
}
//递归求解A0和A1
a0=fft(a0,flag);
a1=fft(a1,flag);
//用一半的数据,综合算出全部的结果,w在此处不断乘上w_n,就保证它是w_n的k次方
for(int k=;k<=n/-;k++){
v[k]=a0[k]+w*a1[k];
v[k+n/]=a0[k]-w*a1[k];
w=w*w_n;
}
return v;
}
int main(){
//原题:求两个多项式相乘后的系数(系数都为整数)
scanf("%d%d%d",&A,&B,&type);
A++; B++;
for(int i=,x;i<A;i++) scanf("%d",&x),a.push_back(Comp(x,));
for(int i=,x;i<B;i++) scanf("%d",&x),b.push_back(Comp(x,));
len=;//算出高位补齐len(上文提到的至少需要2n-1个点),并把两个多项式的次数都扩展到len
//代码里的len指的是上文提到的n
while(len<(max(A,B)*)) len<<=;
for(int i=A;i<len;i++) a.push_back(Comp(,));
for(int i=B;i<len;i++) b.push_back(Comp(,));
//求两个多项式在n个单位复数根的值O(nlgn)
a=fft(a,);
b=fft(b,);
//点值乘法 O(n)
for(int i=;i<len;i++) c.push_back(a[i]*b[i]);
//对点值乘法的结果进行逆FFT O(nlgn)
c=fft(c,);
for(int i=;i<A+B-;i++) printf("%d ",(int)(c[i].a/len+0.5));//除去len,四舍五入(这题是整数)
return ;
}
递归版FFT
#include <cstdio>
#include <iostream>
#include <cmath>
#define max(a,b) ((a)>(b)?(a):(b))
using namespace std;
const int N=;
const double Pi=3.14159265358979323846;
struct Comp{
double a,b;
Comp(){a=b=0.0;}
Comp(double x,double y){a=x;b=y;}
friend Comp operator + (Comp x,Comp y){return Comp(x.a+y.a,x.b+y.b);}
friend Comp operator - (Comp x,Comp y){return Comp(x.a-y.a,x.b-y.b);}
friend Comp operator * (Comp x,Comp y){return Comp(x.a*y.a-x.b*y.b,x.b*y.a+x.a*y.b);}
}a[N*],b[N*];
int A,B,type,n;
inline int rev(int x){
int ret=;
for(int i=;i<n;i<<=,x>>=)
ret=(ret<<|(x&));
return ret;
}
void fft(Comp *a,int f){
int lg=log2(n),len;
Comp w,w_n,u,v;
for(int i=,t;i<n;i++)
if(i<(t=rev(i))) swap(a[i],a[t]);
for(int i=;i<=lg;i++){
len=<<i;
w_n=Comp(cos(*Pi/len),sin(*Pi/len)*f);
for(int j=;j<n;j+=len){
w=Comp(,);
for(int k=;k<=len/-;k++){
u=a[j+k]; v=w*a[j+k+len/];
a[j+k]=u+v; a[j+k+len/]=u-v;
w=w*w_n;
}
}
}
}
int main(){
scanf("%d%d%d",&A,&B,&type);
for(int i=,x;i<A;i++) scanf("%lf",&a[i].a);
for(int i=,x;i<B;i++) scanf("%lf",&b[i].a);
for(n=;n<A+B;n<<=);
fft(a,);
fft(b,);
for(int i=;i<n;i++) a[i]=a[i]*b[i];
fft(a,-);
for(int i=;i<A+B-;i++)
printf("%d\n",(int)(a[i].a/n+0.5));
return ;
}
非递归版FFT(常数小)
【Learning】多项式乘法与快速傅里叶变换(FFT)的更多相关文章
- Algorithm: 多项式乘法 Polynomial Multiplication: 快速傅里叶变换 FFT / 快速数论变换 NTT
Intro: 本篇博客将会从朴素乘法讲起,经过分治乘法,到达FFT和NTT 旨在能够让读者(也让自己)充分理解其思想 模板题入口:洛谷 P3803 [模板]多项式乘法(FFT) 朴素乘法 约定:两个多 ...
- [学习笔记] 多项式与快速傅里叶变换(FFT)基础
引入 可能有不少OIer都知道FFT这个神奇的算法, 通过一系列玄学的变化就可以在 $O(nlog(n))$ 的总时间复杂度内计算出两个向量的卷积, 而代码量却非常小. 博主一年半前曾经因COGS的一 ...
- 多项式 之 快速傅里叶变换(FFT)/数论变换(NTT)/常用套路【入门】
原文链接https://www.cnblogs.com/zhouzhendong/p/Fast-Fourier-Transform.html 多项式 之 快速傅里叶变换(FFT)/数论变换(NTT)/ ...
- 快速傅里叶变换FFT
多项式乘法 #include <cstdio> #include <cmath> #include <algorithm> #include <cstdlib ...
- 快速傅里叶变换FFT& 数论变换NTT
相关知识 时间域上的函数f(t)经过傅里叶变换(Fourier Transform)变成频率域上的F(w),也就是用一些不同频率正弦曲线的加 权叠加得到时间域上的信号. \[ F(\omega)=\m ...
- 快速傅里叶变换(FFT)
扯 去北京学习的时候才系统的学习了一下卷积,当时整理了这个笔记的大部分.后来就一直放着忘了写完.直到今天都腊月二十八了,才想起来还有个FFT的笔记没整完呢.整理完这个我就假装今年的任务全都over了吧 ...
- 快速傅里叶变换(FFT)_转载
FFTFFT·Fast Fourier TransformationFast Fourier Transformation快速傅立叶变换 P3803 [模板]多项式乘法(FFT) 参考上文 首 ...
- 【学习笔记】快速傅里叶变换(FFT)
[学习笔记]快速傅里叶变换 学习之前先看懂这个 浅谈范德蒙德(Vandermonde)方阵的逆矩阵的求法以及快速傅里叶变换(FFT)中IDFT的原理--gzy hhh开个玩笑. 讲一下\(FFT\) ...
- 快速傅里叶变换(FFT)学习笔记
定义 多项式 系数表示法 设\(A(x)\)表示一个\(n-1\)次多项式,则所有项的系数组成的\(n\)维向量\((a_0,a_1,a_2,\dots,a_{n-1})\)唯一确定了这个多项式. 即 ...
随机推荐
- copy和mutableCopy都是浅拷贝!!!------你被骗了很多年
所有系统容器类的copy或mutableCopy方法,都是浅拷贝!!! (ps:什么是容器?比如NSArray,NSMutableArray,NSDictionary,NSMutableDiction ...
- Python闭包及其作用域
Python闭包及其作用域 关于Python作用域的知识在python作用域有相应的笔记,这个笔记是关于Python闭包及其作用域的详细的笔记 如果在一个内部函数里,对一个外部作用域(但不是全局作用域 ...
- spring 注入使用注解(不用xml)
(一):导入spring4的jar包 (二):在xml中配置扫描的包 <context:component-scan base-package="entity">< ...
- 201521123015 《Java程序设计》第5周学习总结
1. 本周学习总结 1.1 尝试使用思维导图总结有关多态与接口的知识点. 2. 书面作业 1.代码阅读:Child压缩包内源代码 1.1 com.parent包中Child.java文件能否编译通过? ...
- 201521123104《Java程序设计》第1周学习总结
1. 本章学习总结 1.初步了解了什么是Java,以及该如何编写简单的Java程序: 2.认识到相比于C和C++来说,Java更复杂一些,所以学习要花费更大的精力: 3.在编写Java程序过程中,认识 ...
- 201521123009 《Java程序设计》第12周学习总结
1. 本周学习总结 2. 书面作业 Q1:将Student对象(属性:int id, String name,int age,double grade)写入文件student.data.从文件读出显示 ...
- 201521123029《Java程序设计》第十周学习总结
1. 本周学习总结 1.1 以你喜欢的方式(思维导图或其他)归纳总结异常与多线程相关内容. 2. 书面作业 本次PTA作业题集异常.多线程 1.finally 题目4-2 1.1 截图你的提交结果(出 ...
- Java课程设计——学生基本信息管理
1.团队名称.团队成员介绍 团队名称:学生基本信息管理设计小组 团队成员:花雨芸(组长)--负责管理界面的编写 丁蓉(组员)--负责登陆的设计编写 2.项目git地址 https://git.osch ...
- PHP面向对象简单总结
类和对象对象:一切东西都可以看做对象,对象是类的实例化.类:类是对象的抽象,用来描述众多对象共有的特征. 定义类 class成员变量 和 成员方法访问修饰符 public共有的 private 私有的 ...
- 源码跟读,Spring是如何解析和加载xml中配置的beans
Spring版本基于: 跟踪代码源码基于: https://github.com/deng-cc/KeepLearning commit id:c009ce47bd19e1faf9e07f12086c ...