快速傅里叶变换 (Fast Fourier Transform, FFT) 是一种 的时间复杂度内完成离散傅里叶变换 (DFT) 的算法,OI 中通常用来优化多项式乘法。
常见题目类型链接:
给定一个 次多项式
和一个
次多项式
。求出它们的卷积。
FFT 就可以在 的时间复杂度内解决该类问题。
先来学习一些 FFT 前备知识。
多项式
系数表示法
设 表示一个
次的多项式,则
利用系数直接运算多项式乘法的时间复杂度是 。(即每个系数都要枚举相乘)
点值表示法
将这个多项式 看成一个函数,就是说代入
个互不相同的
,会得到
个不同的取值
。
那么这个多项式就被这 个点
唯一确定。
当然这样计算也是 的。
当然这些都需要优化。对于第一种好像不会优化,于是我们考虑第二种表示法的优化。
向量(矢量)
有方向也有大小的量,与标量(无方向)相对。
在集合中我们用有箭头的直线来表示。
圆的弧度制
我们之前学的角度制的定义是将圆 等分。
弧度制的定义是等于半径长的圆弧对应的圆心角叫做 1 弧度的角。用符号 rad 表示,读作弧度,用 rad 作为单位来度量角的制度叫做弧度制(字面意思)
显然有:
因为设这个圆的半径为 r ,我们发现周长是 ,那么在这个周长上取长度为 r 的弧,圆心角显然是
平行四边形定则
有一个平行四边形 ,那么有
。
可以表示成向量加法。
等比数列求和
设一个等比数列中:首项为 ,公比是
。
那么前 项的和
证明可以通过错位相减来实现。
复数
定义
设实数 ,定义虚数单位
,有
,将形如
的数叫做复数。复数域是目前已知最大的域。
在复平面里(x 轴表示实数,y 轴表示虚数)。从 到
的向量能表示复数
。
模长: 的长度,即
幅角:假设以逆时针为正方向,从 正半轴旋转至向量的转角的有向角叫做幅角。
运算法则
加法
向量相加,满足平行四边形定则。即:
复数的加法是封闭的。
乘法
向量的乘法几何定义:复数相乘,模长相乘,幅角相加。
代数定义:
单位根
下文若不做特别的声明,均默认 为
的整数次幂。
在复平面上以原点为圆心,1 为半径作出的圆叫做单位圆。以圆点为起点,圆的 n 等分点为终点作出 n 个向量。设幅角为正且最小的向量对应的复数为 ,称为
次单位根。
其余的 个复数显然是
注意到有 (对应方向为 x 轴正半轴的向量)
单位根的幅角是周角的 (显然
等分)。
一些性质:
性质1:
证明:
性质2:
证明:
我们终于把预备知识讲完了,那么接下来开始正题。
快速傅里叶变换
朴素的求一个多项式的点值表达法的复杂度是 ,我们先来探究一些性质。
设多项式 的系数为
那么
将其按照下标分成两个多项式:
那么可以得到:
我们代入 得到
同理,代入 得到
发现这两个式子仅由一个常数项不同(-号)
所以我们只需要算出第一个式子,第二个式子就可以 求。
所以说我们可以递归二分去计算这个东西,遇到常数项就返回。
时间复杂度:
快速傅里叶逆变换
还没有结束...
我们要考虑怎么输出答案啊。
一般来说没有人会让你输出点值表示法的,所以我们需要用逆变换来换回来。
首先我们可以将 FFT 后的结果看做一个向量 。
我们设另一个向量 令其满足
浅显易懂的讲就是将结果看做一个多项式,求出这个多项式的 FFT 的结果。
推一波式子:
(这里公式我不知道为什么一直挂 将就看看吧qaq)
设
代入 得:
直接套用等比数列求和(错位相减法),得:
不难看出分子为 0 ,分母不为 0 。
分类讨论一下:
:
:
我们继续考虑式子
同理:
:
:
这样我们就得到点值与系数之间的表示方法:
递归实现的代码
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)
a[i]=a1[i]+w*a2[i],
a[i+(limit>>1)]=a1[i]-w*a2[i];// O(1)得到另一部分
} 蝴蝶操作
稍微优化一下小常数。
发现 被计算了两次,开一个
来存储仅需计算一次。
迭代实现
递归的效率过于低下,我们需要更快的解决这个问题。
我们观察原序列的下标和分类后的序列下标。
发现二进制翻转可以就直接解决这个问题。
这样我们就可以 的利用某种操作来求得序列,然后依照规律合并即可。
#include <algorithm>
#include <iostream>
#include <cstring>
#include <climits>
#include <cstdio>
#include <vector>
#include <cmath>
#include <queue>
#include <stack>
#include <map>
#include <set>
#define R register
#define LL long long
#define U unsigned
#define FOR(i,a,b) for(int i = a;i <= b;++i)
#define RFOR(i,a,b) for(int i = a;i >= b;--i)
#define CLR(i,a) memset(i,a,sizeof(i))
#define BR printf("--------------------\n")
#define DEBUG(x) std::cerr << #x << '=' << x << std::endl
namespace fastIO{
#define BUF_SIZE 100000
#define OUT_SIZE 100000
#define ll long long
bool IOerror=0;
inline char nc(){
static char buf[BUF_SIZE],*p1=buf+BUF_SIZE,*pend=buf+BUF_SIZE;
if (p1==pend){
p1=buf; pend=buf+fread(buf,1,BUF_SIZE,stdin);
if (pend==p1){IOerror=1;return -1;}
}
return *p1++;
}
inline bool blank(char ch){return ch==' '||ch=='\n'||ch=='\r'||ch=='\t';}
inline void read(int &x){
bool sign=0; char ch=nc(); x=0;
for (;blank(ch);ch=nc());
if (IOerror)return;
if (ch=='-')sign=1,ch=nc();
for (;ch>='0'&&ch<='9';ch=nc())x=x*10+ch-'0';
if (sign)x=-x;
}
inline void read(ll &x){
bool sign=0; char ch=nc(); x=0;
for (;blank(ch);ch=nc());
if (IOerror)return;
if (ch=='-')sign=1,ch=nc();
for (;ch>='0'&&ch<='9';ch=nc())x=x*10+ch-'0';
if (sign)x=-x;
}
#undef ll
#undef OUT_SIZE
#undef BUF_SIZE
};
using namespace fastIO;
#undef R
const int MAXN = 10000000 + 5;
const double PI = acos(-1.0);
struct complex{
double x,y;
complex(double x=0,double y=0) : x(x),y(y) {}
}a[MAXN],b[MAXN];
complex operator + (const complex &a,const complex &b){
return complex(a.x + b.x,a.y + b.y);
}
complex operator - (const complex &a,const complex &b){
return complex(a.x-b.x,a.y-b.y);
}
complex operator * (const complex &a,const complex &b){
return complex(a.x * b.x - a.y * b.y,a.x*b.y+a.y*b.x);
}
int N,M;
int f[MAXN],g[MAXN],r[MAXN];
int limit=1,len;
inline void fft(complex *A,int opt){
FOR(i,0,limit){
if(i < r[i]) std::swap(A[i],A[r[i]]);
}
for(int mid = 1;mid < limit;mid <<= 1){
complex W(cos(PI/mid),opt*sin(PI/mid));
for(int j=0,R = (mid << 1);j < limit;j += R){
complex w(1,0);
for(int k = 0;k < mid;k++,w=w*W){
complex x = A[j+k],y=w*A[j+mid+k];
A[j+k] = x+y;
A[j+mid+k] = x-y;
}
}
}
}
int main(){
read(N);read(M);
FOR(i,0,N){
int x;read(x);a[i].x = x;
}
FOR(i,0,M){
int x;read(x);b[i].x = x;
}
while(limit <= N + M){
limit <<= 1;len++;
}
FOR(i,0,limit){
r[i] = (r[i>>1]>>1)|((i&1)<<(len-1));
}
fft(a,1);
fft(b,1);
FOR(i,0,limit) a[i] = a[i]*b[i];
fft(a,-1);
FOR(i,0,N+M) printf("%d%c",(int)(a[i].x/limit+0.5),(i == N + M) ? '\n': ' ');
return 0;
} 
京公网安备 11010502036488号