扯
去北京学习的时候才系统的学习了一下卷积,当时整理了这个笔记的大部分。后来就一直放着忘了写完。直到今天都腊月二十八了,才想起来还有个FFT的笔记没整完呢。整理完这个我就假装今年的任务全都over了吧。
更改了一些以前不大正确的地方,又添加了一些推导,证明实在不会。
有一些公式,但个人觉得还是比较好理解。可能还会有错误,希望大佬友情指出。
最后,祝各位看官新年快乐.
回家过寒假去咯(虽然就\(4\)天\(qwq\))
多项式
一个次数界为\(n\)的多项式\(A(x) = \sum_{i = 0}^{n - 1}a_ix^i\)
次数界为\(n\)说明这个多项式最高次项的指数小于\(n\)
系数表示法
我们可以只保留多项式每项的系数来描述这个多项式。即\(a_i\)表示指数为\(i\)的项的系数
运算
多项式之间的加运算,只要将对应项的系数相加即可。
即若\(C(x) = A(x) + B(x)\)那么有\(C(x) = \sum_{i = 0}^{n - 1}{(a_i+b_i)x^i}\)
多项式之间的乘法。
若\(C(x) = A(x) + B(x)\)那么\(C(x) = \sum_{i = 0}^{n - 1}{\sum_{j = 0}^ia_jb_{i - j}x^i}\)
其中\(C(x) = \sum_{i = 0}^{n - 1}{\sum_{j = 0}^ia_jb_{i - j}x^i}\)叫做卷积
可以看出暴力计算卷积的复杂度为\(O(n^2)\)
点值表示法
我们可以选\(n\)个值代入多项式从而得出\(n\)个二元组\((x_i,y_i)\)。可以证明,通过这\(n\)个点对可以确定这个多项式。
还原这个多项式用高斯消元即可。
点值表示法的优点是进行多项式的加法和乘法只要将\(y_i\)相加或者相乘即可。
求值与插值
考虑如何可以比较快速的计算卷积。因为点值表示计算乘法比较方便。所以我们可以把系数表示法转换为点值表示法。然后进行乘法。然后在转换回来。就可以计算卷积了。
把系数表示法转化为点值表示法称为求值,把点值表示法转换为系数表示法称为插值。
拉格朗日插值公式
\(A(x) = \sum_{i = 0}^{n-1}y_i\frac{\prod_{j\ne i}{(x - x_j)}}{\prod_{j \ne i}{(x_i - x_j)}}\)
利用拉格朗日插值公式可以在给定\(n\)个点值表示法的情况下\(O(n^2)\)时间内计算出原二项式在某个位置的值
离散傅里叶变换(DFT)
离散傅里叶变换实现了两种表示法之间的转换,复杂度是\(O(n^2)\)
复数
复数是形如\(a+bi\)的数,其中\(a,b\)为实数。\(a\)称为实部,\(b\)称为虚部。\(i\)为虚数单位,定义\(i^2 = -1\)
复数是数的范围在实数上的扩展
将复数\(a+bi\)表示在数轴上,是一个\((a,b)\)的向量.
复数的运算
复数相加将\(a\)和\(b\)分别相加即可。
复数相乘,其实将i看作未知量相乘就行了。即\((a+bi)*(x+yi) = (ax - by) + (ay + bx)i\)
单位圆
在数轴上以原点为圆心半径为\(1\)的圆。
单位根
\(n\)次单位根满足\(\omega^n = 1\)。\(\omega 就是n次单位根\)
表现在单位圆上就是单位圆的\(n\)等分点
我们用\(\omega_n\)表示主\(n\)次单位根也就是从\((1,0)\)开始数第一个n次单位根
那么可以得到其他的\(n\)次单位根分别为\(\omega_n^0,\omega_n^2,\omega_n^3,.....\omega_n^{n - 1}\)
性质:
\(\omega_n^n = 1\)当n为偶数时,\(\omega_n^{n/2} = -1\)
\(\sum_{i=0}^{n-1}\omega_n^i=0\)
欧拉公式
\(e^{ix} = cos(x)+i * sin(x)\)
\(\omega_n=e^{2\pi i/n} = cos(\frac{2\pi}{n})+i*sin(\frac{2\pi}{n})\)
用来求主\(n\)次单位根
转换
有了上面这些铺垫,就可以进行转换了。
将\(n\)个\(n\)次单位根带入原来的多项式,就可以得到\(n\)个点值表示法。
那么怎么将点值表示法转换为系数表示法呢?
这就是用\(n\)次单位根的原因
我们将得到\(n\)个\(y\)作为另一个多项式\(B(x)\)的系数
然后取单位根倒数带入就能得到原来的多项式\(A(x)\)啦
这样就成功的实现了点值表示法与系数表示法之间的转换
快速傅里叶变换(FFT)
用\(DFT\)计算卷积的复杂度是\(O(n^2)\)的。利用快速傅里叶变换可以优化到\(O(nlogn)\)
分治
考虑一个次数界为\(n\)的多项式\(A(x)\)求他的\(DFT\)
这里默认\(n\)为偶数,如果\(n\)不是偶数,那么只要加上一个系数为\(0\)的项即可将他填充为偶数
然后我们根据下标的奇偶性将他分为两部分。
\]
\]
然后就能得到\(A(x)=A_{(even)}(x^2) + xA_{(odd)}(x^2)\)
然后发现\(A_{(even)}(x)\)和\(A_{(odd)}(x)\)是可以用同样的方法递归计算的。所以就可以进行分治然后递归运算。
那么我们需要带入这\(n\)个单位根,应该怎么计算呢。
下面进行推导
假如现在要把\(\omega_{n}^i\)代入
依据上面的式子
\]
\]
然后考虑\(A(\omega_n^{i + \frac{n}{2}})\)
\]
\]
\]
然后就可以递归的愉快的计算出将这\(n\)个单位根代入所得到的值
优化
递归实现\(FFT\)非常慢。所以要用非递归的方法来实现
我们将递归实现的过程写下来
0 1 2 3 4 5
0 2 4|1 3 5
0 4|2|1 5|3
0|4|2|1|5|3
然后看出每个数字递归到最后的为是为当前位置二进制表示法翻转之后的数字。
然后就可以先把每个数字都放到最后的位置上面,然后再合并就行了。
代码
递归
/*
* @Author: wxyww
* @Date: 2019-02-01 21:09:51
* @Last Modified time: 2019-02-01 21:47:34
*/
#include<cstdio>
#include<iostream>
#include<cstdlib>
#include<cmath>
#include<ctime>
#include<cmath>
#include<bitset>
using namespace std;
typedef long long ll;
const int N = 3000100;
const double pi = acos(-1.0);
ll read() {
ll x=0,f=1;char c=getchar();
while(c<'0'||c>'9') {
if(c=='-') f=-1;
c=getchar();
}
while(c>='0'&&c<='9') {
x=x*10+c-'0';
c=getchar();
}
return x*f;
}
struct complex {
double x,y;
complex() {x = y = 0;}
complex(double xx,double yy) {
x = xx;y = yy;
}
}A[N],B[N];
complex operator * (complex a,complex b) {
return complex(a.x * b.x - a.y * b.y,a.x * b.y + a.y * b.x);//复数运算
}
complex operator + (complex a,complex b) {
return complex(a.x + b.x,a.y + b.y);
}
complex operator - (complex a,complex b) {
return complex(a.x - b.x,a.y - b.y);
}
void FFT(complex *a,int n,int ty) {
if(n == 1) return;
complex a1[n >> 1],a2[n >> 1];
for(int i = 0;i <= n;i += 2) {
a1[i >> 1] = a[i];a2[i >> 1] = a[i + 1];//按奇偶分开
}
FFT(a1,n >> 1,ty);FFT(a2,n >> 1,ty);//递归
complex w1 = complex(cos(2.0 * pi / n),ty * sin(2.0 * pi / n));//主n次单位根
complex w = complex(1.0,0.0);//当前的n次单位根
int k = n >> 1;
for(int i = 0;i < k;++i) {//根据式子计算
complex t = w * a2[i];
a[i + k] = a1[i] - t;
a[i] = a1[i] + t;
w = w * w1;
}
}
int main() {
int n = read(),m = read();
for(int i = 0;i <= n;++i) A[i].x = read();
for(int i = 0;i <= m;++i) B[i].x = read();
int tot = 1;
while(tot <= n + m) tot <<= 1;
FFT(A,tot,1);
FFT(B,tot,1);
for(int i = 0;i <= tot;++i) A[i] = A[i] * B[i];
FFT(A,tot,-1);
for(int i = 0;i <= n + m;++i)
printf("%d ",(int)(A[i].x / tot + 0.5));
return 0;
}
非递归
/*
* @Author: wxyww
* @Date: 2019-02-01 21:50:51
* @Last Modified time: 2019-02-01 22:03:10
*/
#include<cstdio>
#include<iostream>
#include<cstdlib>
#include<cmath>
#include<ctime>
#include<cmath>
#include<bitset>
using namespace std;
typedef long long ll;
const int N = 3000100;
const double pi = acos(-1.0);
ll read() {
ll x=0,f=1;char c=getchar();
while(c<'0'||c>'9') {
if(c=='-') f=-1;
c=getchar();
}
while(c>='0'&&c<='9') {
x=x*10+c-'0';
c=getchar();
}
return x*f;
}
struct complex {
double x,y;
complex() {x = y = 0;}
complex(double xx,double yy) {
x = xx;y = yy;
}
}A[N],B[N];
complex operator * (complex a,complex b) {
return complex(a.x * b.x - a.y * b.y,a.x * b.y + a.y * b.x);//复数运算
}
complex operator + (complex a,complex b) {
return complex(a.x + b.x,a.y + b.y);
}
complex operator - (complex a,complex b) {
return complex(a.x - b.x,a.y - b.y);
}
void FFT(complex *a,int n,int ty) {
for(int i = 0,j = 0;i < n;++i) {//找到最终对应的位置
if(i < j) swap(a[i],a[j]);
for(int k = n >> 1;(j ^= k) < k;k >>= 1);
}
for(int m = 2;m <= n;m <<= 1) {
complex w1 = complex(cos(2*pi/m),ty * sin(2 * pi / m));
for(int i = 0;i < n;i += m) {
complex w = complex(1,0);
for(int k = 0;k < (m >> 1);++k) {
complex t = w * a[i + k + (m >> 1)];
complex u = a[i + k];
a[i + k] = u + t;
a[i + k + (m >> 1)] = u - t;
w = w * w1;
}
}
}
}
int main() {
int n = read(),m = read();
for(int i = 0;i <= n;++i) A[i].x = read();
for(int i = 0;i <= m;++i) B[i].x = read();
int tot = 1;
while(tot <= n + m) tot <<= 1;
FFT(A,tot,1);
FFT(B,tot,1);
for(int i = 0;i <= tot;++i) A[i] = A[i] * B[i];
FFT(A,tot,-1);
for(int i = 0;i <= n + m;++i)
printf("%d ",(int)(A[i].x / tot + 0.5));
return 0;
}
时间差异
最后来一份递归代码与非递归代码运行时间的比较