attack
2018-02-12 11:10:15
设
则
例如:
利用这种方法计算多项式乘法复杂度为
(第一个多项式中每个系数都需要与第二个多项式的每个系数相乘)
将
则该多项式被这
其中
例如:上面的例子用点值表示法可以为
利用这种方法计算多项式乘法的时间复杂度仍然为
(选点
我们可以看到,两种方法的时间复杂度都为
对于第一种方法,由于每个点的系数都是固定的,想要优化比较困难
对于第二种方法,貌似也没有什么好的优化方法,不过当你看完下面的知识,或许就不这么想了
在介绍复数之前,首先介绍一些可能会用到的东西
同时具有大小和方向的量
在几何中通常用带有箭头的线段表示
等于半径长的圆弧所对的圆心角叫做1弧度的角,用符号rad表示,读作弧度。用弧度作单位来度量角的制度叫做弧度制
公式:
平行四边形定则:AB+AD=AC
设
在复平面中,
模长:从原点
幅角:假设以逆时针为正方向,从
加法:
因为在复平面中,复数可以被表示为向量,因此复数的加法与向量的加法相同,都满足平行四边形定则(就是上面那个)
乘法:
几何定义:复数相乘,模长相乘,幅角相加
代数定义:
单位根
下文中,默认
在复平面上,以原点为圆心,
根据复数乘法的运算法则,其余
注意
那么如何计算它们的值呢?这个问题可以由欧拉公式解决
单位根的幅角为周角的
在代数中,若
单位根的性质
证明:
讲了这么多,貌似跟我们的正题没啥关系啊。。
OK!各位坐稳了,前方高能!
我们前面提到过,一个
那么我们可以把单位根的
我们设多项式
那么
将其下标按照奇偶性分类
设
那么不难得到
我们将
同理,将
大家有没有发现什么规律?
没错!这两个式子只有一个常数项不同!
那么当我们在枚举第一个式子的时候,我们可以
又因为第一个式子的
所以我们将原来的问题缩小了一半!
而缩小后的问题仍然满足原问题的性质,所以我们可以递归的去搞这件事情!
直到多项式仅剩一个常数项,这时候我们直接返回就好啦
不难看出FFT是类似于线段树一样的分治算法。
因此它的时间复杂度为
不要以为FFT到这里就结束了。
我们上面的讨论是基于点值表示法的。
但是在平常的学习和研究中很少用点值表示法来表示一个多项式。
所以我们要考虑如何把点值表示法转换为系数表示法,这个过程叫做傅里叶逆变换
将
当
等式两边同乘
两式相减得
观察这个式子,不难看出它分母不为0,但是分子为0
因此,当
那当
很显然,
继续考虑刚刚的式子
当
这样我们就得到点值与系数之间的表示啦
至此,FFT的基础理论部分就结束了。
我们来小结一下FFT是怎么成功实现的
首先,人们在用系数表示法研究多项式的时候遇阻
于是开始考虑能否用点值表示法优化这个东西。
然后根据复数的两条性质(这个思维跨度比较大)得到了一种分治算法。
最后又推了一波公式,找到了点值表示法与系数表示法之间转换关系。
emmmm
其实FFT的实现思路大概就是
系数表示法—>点值表示法—>系数表示法
当然,再实现的过程中还有很多技巧
我们根据代码来理解一下
递归实现的方法比较简单。
就是按找我们上面说的过程,不断把要求的序列分成两部分,再进行合并
在c++的STL中提供了现成的complex类,但是我不建议大家用,毕竟手写也就那么几行,而且万一某个毒瘤卡STL那岂不是很GG?
#include<iostream>
#include<cstdio>
#include<cmath>
using namespace std;
const int MAXN=2*1e6+10;
inline int read()
{
char c=getchar();int x=0,f=1;
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;
}
const double Pi=acos(-1.0);
struct complex
{
double x,y;
complex (double xx=0,double yy=0){x=xx,y=yy;}
}a[MAXN],b[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 fast_fast_tle(int limit,complex *a,int type)
{
if(limit==1) return ;//只有一个常数项
complex a1[limit>>1],a2[limit>>1];
for(int i=0;i<=limit;i+=2)//根据下标的奇偶性分类
a1[i>>1]=a[i],a2[i>>1]=a[i+1];
fast_fast_tle(limit>>1,a1,type);
fast_fast_tle(limit>>1,a2,type);
complex Wn=complex(cos(2.0*Pi/limit) , type*sin(2.0*Pi/limit)),w=complex(1,0);
//Wn为单位根,w表示幂
for(int i=0;i<(limit>>1);i++,w=w*Wn)//这里的w相当于公式中的k
a[i]=a1[i]+w*a2[i],
a[i+(limit>>1)]=a1[i]-w*a2[i];//利用单位根的性质,O(1)得到另一部分
}
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 limit=1;while(limit<=N+M) limit<<=1;
fast_fast_tle(limit,a,1);
fast_fast_tle(limit,b,1);
//后面的1表示要进行的变换是什么类型
//1表示从系数变为点值
//-1表示从点值变为系数
//至于为什么这样是对的,可以参考一下c向量的推导过程,
for(int i=0;i<=limit;i++)
a[i]=a[i]*b[i];
fast_fast_tle(limit,a,-1);
for(int i=0;i<=N+M;i++) printf("%d ",(int)(a[i].x/limit+0.5));//按照我们推倒的公式,这里还要除以n
return 0;
}
这里还有一个听起来很装B的优化—蝴蝶效应
观察合并的过程,w*a2[i] 这一项计算了两次,因为理论上来说复数的乘法是比较慢的,所以我们可以把这一项记出来
for(int i=0;i<(limit>>1);i++,w=w*Wn)//这里的w相当于公式中的k
{
complex t=w*a2[i];//蝴蝶效应
a[i]=a1[i]+t,
a[i+(limit>>1)]=a1[i]-t;//利用单位根的性质,O(1)得到另一部分
}
速度什么的才不是关键呢?
关键是我们AC不了啊啊啊
表着急,AC不了不代表咱们的算法不对,只能说这种实现方法太low了
下面介绍一种更高效的方法
观察一下原序列和反转后的序列
聪明的你有没有看出什么显而易见的性质?
没错!
我们需要求的序列实际是原序列下标的二进制反转!
因此我们对序列按照下标的奇偶性分类的过程其实是没有必要的
这样我们可以
// luogu-judger-enable-o2
#include<iostream>
#include<cstdio>
#include<cmath>
using namespace std;
const int MAXN=1e7+10;
inline int read()
{
char c=getchar();int x=0,f=1;
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;
}
const double Pi=acos(-1.0);
struct complex
{
double x,y;
complex (double xx=0,double yy=0){x=xx,y=yy;}
}a[MAXN],b[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);}//不懂的看复数的运算那部分
int N,M;
int l,r[MAXN];
int limit=1;
void fast_fast_tle(complex *A,int type)
{
for(int i=0;i<limit;i++)
if(i<r[i]) swap(A[i],A[r[i]]);//求出要迭代的序列
for(int mid=1;mid<limit;mid<<=1)//待合并区间的中点
{
complex Wn( cos(Pi/mid) , type*sin(Pi/mid) ); //单位根
for(int R=mid<<1,j=0;j<limit;j+=R)//R是区间的右端点,j表示前已经到哪个位置了
{
complex w(1,0);//幂
for(int k=0;k<mid;k++,w=w*Wn)//枚举左半部分
{
complex x=A[j+k],y=w*A[j+mid+k];//蝴蝶效应
A[j+k]=x+y;
A[j+mid+k]=x-y;
}
}
}
}
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();
while(limit<=N+M) limit<<=1,l++;
for(int i=0;i<limit;i++)
r[i]= ( r[i>>1]>>1 )| ( (i&1)<<(l-1) ) ;
// 在原序列中 i 与 i/2 的关系是 : i可以看做是i/2的二进制上的每一位左移一位得来
// 那么在反转后的数组中就需要右移一位,同时特殊处理一下复数
fast_fast_tle(a,1);
fast_fast_tle(b,1);
for(int i=0;i<=limit;i++) a[i]=a[i]*b[i];
fast_fast_tle(a,-1);
for(int i=0;i<=N+M;i++)
printf("%d ",(int)(a[i].x/limit+0.5));
return 0;
}