引言
快速傅里叶变换
不少
应该都曾听过 FFT (Fuck Fuck TLE) 这样一个神(gui)奇(chu)算法
不得不说数论蒟蒻在看它证明的时候脑袋里只有三个想法
“我是谁?” “我在哪?” “我在干嘛?”
但认真学完又不得不赞叹数论的奇妙啊
各种鬼畜证明后代码只用简洁的10行就让多项式乘法达到
复杂度
所以我们下面就来看看FFT的优(gui)美(chu)证明吧
大量数论定理及证明即将来袭,请在dalao陪同下食用
多项式的系数表达
多项式的系数表达应该是在日常运算中较常用的一种表示法
即对于一个次数界为
的多项式
其可以表示为这样的形式
那么多项式
的系数所组成的向量
是这个多项式的系数表达
基于系数表达的多项式,对于一个给定的值
显然我们可以利用秦九韶算法
计算
多项式的点值表达
点值表示可能对大多接触数论较少的人是个不熟悉的名词
一个次数界为
的多项式
的点值表达为一个由
个点值对所组成的集合:
满足所有的
各不相同
- 插值多项式唯一性定理
定理:对于任意
个点值对所组成的集合
,若其中所有
不同
则存在唯一的次数界为
的多项式
,满足
证明:
可以考虑借助增广矩阵作个简单证明
中间的那一大坨就是范德蒙德矩阵,记为
它的行列式计算公式为
因为
两两不同,所以一定有
,即证明该矩阵可逆
因此给定点值表达
,则能确定唯一的系数表达
,使得
多项式乘法
已知多项式 ,要求多项式 ,这就是多项式乘法
假如
基于系数表达
那么
的每一项为
,复杂度为
那么基于点值表示呢
对于一个给定的值
,有
,复杂度为
这是明显的差距啊,也就是说计算多项式乘法显然点值表示更优
多项式的求值&&插值
将多项式的系数表示转化为点值表示即为求值
对于一个次数界为
的多项式
计算一个点的值为
,总共
个点,复杂度为
相对的,将多项式的点值表示转化为系数表示即为插值
假如我们将插值表示成增广矩阵形式再对范德蒙德矩阵求逆来得到系数表达,显然复杂度
假如借助拉格朗日插值公式
复杂度也依然是
从DFT && IDFT 到 FFT
看到这里,其实你已经懂得了 (离散傅里叶变换) 与 (逆离散傅立叶变换)
对于
的系数表达
定义结果
则
就是系数向量
的离散傅里叶变换,记为
则恰好相反,即
现在计算DFT与IDFT复杂度都是
,不但与直接系数相乘复杂度一样,还常数贼大
不过介绍这么多肯定不会是没用的,接下来我们的FFT就要登场了
为了解决两种表示法转换的复杂度问题
通过精心挑选求值点,可以让两种表示法的转换复杂度达到
所以我们概括出计算多项式乘法的策略
1. 求值:利用FFT在 时间内转换多项式为点值表达
2. 逐点相乘:利用基于点值表达的多项式O(n)计算多项式乘积
3. 差值:利用FFT在 时间内转换多项式为系数表达
单位复根
(注意理解此处需要一定的复数学习基础)
首先引入单位复根的定义
- 对于满足 的复数 ,我们称其为 次单位复根
根据其定义及复数运算法则不难得出 次单位复根有 个 这样的结论
若我们将其用向量在复平面上表示出来的话,会是像这样
及在复平面上以原点为圆心,一单位长度为半径作圆
以原点为起点,圆的
等分点为终点的向量,即为一个
次单位根
那么如何计算这些单位根的值呢
这里就要用到史称最优美的数学公式——欧拉公式
(其中
为虚数单位,即
)
我们尝试将
带入
我们称此时的
为主
次单位根
即上图中 幅角为正且最小的向量对应的复数
对于其他的单位根,我们记作
不难发现这些单位根都是主单位根的整数次幂,其中
单位复根的引理
- 消去引理
引理:对任何整数
有
证明:直接带入上述定义即可
- 折半引理
引理:对于任何大于0的偶数n,都有
个
次单位复根的平方的集合,等于
个
次单位复根的集合
证明:读起来很绕,但我们结合公式就好理解了
以复平面上的向量来理解就是方向相对的两组向量,其平方相等
这条引理就是FFT能折半解决问题的基础
快速傅里叶变换FFT
讲了这么久的前置姿势终于进入正题了
前面提到的 “精心挑选的求值点” 也就是
个
次单位根
下面为了讲解方便,设
为2的整次幂
实际操作中若不足,则在高此项不断用系数为0的项补齐
对于多项式
我们将其每一项按奇偶项划分,设
那么显然有
我们需要求所有
尝试带入
得
再尝试带入
我们惊讶的发现这两个式子恰好只差一个符号
于是我们可以基于递归写出求值FFT
void FFT(complex* a,int len)//a是基于系数表达的向量a
{
if(len==1) return;
complex* a0=new complex[len>>1];
complex* a1=new complex[len>>1];
for(int i=0;i<len;i+=2)
a0[i>>1]=a[i],a1[i>>1]=a[i+1];
FFT(a0,len>>1); FFT(a1,len>>1);
complex wn(cos(2*Pi/len),sin(2*Pi/len));//主n次单位根
complex w(1,0);//辐角为2pi得单位根
for(int i=0;i<(len>>1);++i)
{
a[i]=a0[i]+w*a1[i];//根据上述推导公式
a[i+(len>>1)]=a0[i]-w*a1[i];
w=w*wn;//得到下一个单位根
}
delete[] a0;
delete[] a1;
}
插值FFT
上面我们通过FFT解决了求值
基于点值计算完多项式乘法后,再将其转换为系数表达就大功告成了
首先我们可以将求DFT过程结合范德蒙德矩阵表示出来
我们用
表示中间的范德蒙德矩阵,那么求值过程表示为
那么它的逆运算——插值,表示为
(
为
的逆矩阵)
所以其实我们只要求出
即可
现在我们再引入一个玄学定理
- 定理: 对于 , 的 处元素为
根据该引理我们得出
这与前面求值的公式
仅仅次幂由正变负且多了一个
项而已
所以我们在前面FFT代码上稍作修改即可同时求插值
void FFT(complex* a,int len,int opt)//opt==1求值,opt==-1插值
{
if(len==1) return;
complex* a0=new complex[len>>1];
complex* a1=new complex[len>>1];
for(int i=0;i<len;i+=2)
a0[i>>1]=a[i],a1[i>>1]=a[i+1];
FFT(a0,len>>1,opt); FFT(a1,len>>1,opt);
complex wn(cos(2*Pi/len),opt*sin(2*Pi/len));//仅仅主次单位根不同
complex w(1,0);
for(int i=0;i<(len>>1);++i)
{
a[i]=a0[i]+w*a1[i];
a[i+(len>>1)]=a0[i]-w*a1[i];
w=w*wn;
}
delete[] a0;
delete[] a1;
}
到这里FFT就可以以
的复杂度实现多项式乘法了
那么整个多项式乘法的FFT递归实现如下
#include<iostream>
#include<cmath>
#include<algorithm>
#include<map>
#include<cstring>
#include<cstdio>
using namespace std;
typedef long long lt;
typedef double dd;
int read()
{
int f=1,x=0;
char ss=getchar();
while(ss<'0'||ss>'9'){if(ss=='-')f=-1;ss=getchar();}
while(ss>='0'&&ss<='9'){x=x*10+ss-'0';ss=getchar();}
return f*x;
}
const dd Pi=acos(-1.0);
const int maxn=4000010;
int n,m;
struct complex{
dd x,y;
complex(dd _x=0,dd _y=0){ x=_x; y=_y;}
}A[maxn],B[maxn];
int lim=1;
//复数运算
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);}
complex operator *(complex a,complex b){ return complex( a.x*b.x-a.y*b.y, a.x*b.y+a.y*b.x);}
void FFT(complex* a,int len,int opt)
{
if(len==1) return;
complex* a0=new complex[len>>1];
complex* a1=new complex[len>>1];
for(int i=0;i<len;i+=2)
a0[i>>1]=a[i],a1[i>>1]=a[i+1];
FFT(a0,len>>1,opt); FFT(a1,len>>1,opt);
complex wn(cos(2*Pi/len),opt*sin(2*Pi/len));
complex w(1,0);
for(int i=0;i<(len>>1);++i)
{
a[i]=a0[i]+w*a1[i];
a[i+(len>>1)]=a0[i]-w*a1[i];
w=w*wn;
}
delete[] a0;
delete[] a1;
}
int main()
{
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();
while(lim<=n+m) lim<<=1;
FFT(A,lim,1); FFT(B,lim,1);
for(int i=0;i<=lim;++i) A[i]=A[i]*B[i];
FFT(A,lim,-1);
for(int i=0;i<=n+m;++i)
printf("%d ",(int)(A[i].x/lim+0.5));//根据推到的公式还要除以n,加0.5是为了保证精度
return 0;
}
但是你以为到这里就结束了? 还没!
你会发现这份代码在luogu提交还是T一个点,是因为算法不对吗?不是
递归本身就自带大常数,而且每层还要额外申请空间,没有MLE还真是奇迹
FFT得优美性质远不止于此
给FFT来点优化吧——迭代与蝴蝶操作
首先我们可以注意到, 在上面的实现代码中计算了两次
复数运算是自带大常数的,我们可以将它只计算一次并将结果放在一个临时变量中
for(int i=0;i<(len>>1);++i)
{
int t=w*a1[i];
a[i]=a0[i]+t;
a[i+(len>>1)]=a0[i]-t;
w=w*wn;
}
好了,优化到此结束 呸,怎么可能
讲这里只是为了引入蝴蝶操作
-蝴蝶操作
我们定义
为旋转因子
那么每一次先将
与旋转因子的乘积存储在一个变量t里
并在
增加、减去t的操作称为一次蝴蝶操作
接下来,为了避免递归带来的大常数我们自然想到迭代
尝试画出递归调用的递归树
如果将初始向量按照叶子的位置预先排序好的话, 就可以自底向上一步一步合并结果
首先成对取出元素,
对于每对元素进行 1 次蝴蝶操作计算出它们的
并用它们的
替换原来的2个元素,
这样
中就会存储有
个二元
继续成对取出元素,
对于每对元素进行 2 次蝴蝶操作计算出它们的
并用它们的
替换原来的4个元素,
这样
中就会存储有
个四元
…
如此反复,直到计算出 2 个长度为 的 , 最后使用 n/2 次蝴蝶操作即可计算出整个向量的
假设一开始 已按递归树最低层顺序排序,上述操作不难实现,只需要三层循环
for(int i=1;i<lim;i<<=1)//枚举当前已计算的DFT长度
{
complex wn(cos(Pi/i),opt*sin(Pi/i));
for(int j=0;j<lim;j+=(i<<1))//枚举每个DFT的开始位置
{
complex w(1,0);
for(int k=0;k<i;++k)//枚举每个DFT内的偏移量
{
complex nx=a[j+k],ny=w*a[i+j+k];//蝴蝶操作
a[j+k]=nx+ny;
a[i+j+k]=nx-ny;
w=w*wn;
}
}
}
最后一个要解决的问题就是对
排序了
我们观察排序前后的变化
原来的序号 0 1 2 3 4 5 6 7
现在的序号 0 4 2 6 1 5 3 7
原来的二进制表示 000 001 010 011 100 101 110 111
现在的二进制表示 000 100 010 110 100 101 011 111
发现它们的二进制表示正好是反序,这就是蝴蝶定理
我们只要一开始
处理好其对应位置即可
for(int i=0;i<lim;++i)
R[i]= (R[i>>1]>>1) | ( (i&1) << (L-1) ) ;
最后迭代FFT代码呈上
#include<iostream>
#include<cmath>
#include<algorithm>
#include<map>
#include<cstring>
#include<cstdio>
using namespace std;
typedef long long lt;
typedef double dd;
int read()
{
int f=1,x=0;
char ss=getchar();
while(ss<'0'||ss>'9'){if(ss=='-')f=-1;ss=getchar();}
while(ss>='0'&&ss<='9'){x=x*10+ss-'0';ss=getchar();}
return f*x;
}
const dd Pi=acos(-1.0);
const int maxn=4000010;
int n,m;
struct complex{
dd x,y;
complex(dd _x=0,dd _y=0){ x=_x; y=_y;}
}A[maxn],B[maxn];
int lim=1,L,R[maxn];
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);}
complex operator *(complex a,complex b){ return complex( a.x*b.x-a.y*b.y, a.x*b.y+a.y*b.x);}
void FFT(complex* a,int opt)
{
for(int i=0;i<lim;++i)
if(i<R[i]) swap(a[i],a[R[i]]);
for(int i=1;i<lim;i<<=1)
{
complex wn(cos(Pi/i),opt*sin(Pi/i));
for(int j=0;j<lim;j+=(i<<1))
{
complex w(1,0);
for(int k=0;k<i;++k)
{
complex nx=a[j+k],ny=w*a[i+j+k];
a[j+k]=nx+ny;
a[i+j+k]=nx-ny;
w=w*wn;
}
}
}
}
int main()
{
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();
while(lim<=n+m) lim<<=1,L++;
for(int i=0;i<lim;++i)
R[i]=(R[i>>1]>>1)|((i&1)<<(L-1));
FFT(A,1); FFT(B,1);
for(int i=0;i<=lim;++i) A[i]=A[i]*B[i];
FFT(A,-1);
for(int i=0;i<=n+m;++i)
printf("%d ",(int)(A[i].x/lim+0.5));//根据推倒的公式除以n
return 0;
}
最后的闲(tu)话(cao)
作为一个数论蒟蒻还真是第一次接触这么难的东西
为了学FFT自己花了三天恶补了一堆选修
到最后其实还是有很定理不能自证,只能记结论直接用,等哪天突然开悟了再一点点补上吧
若还有什么解释错误的地方希望dalao们指出QAQ