概述
问题
给出长度为
n
n
n的多项式
f
(
x
)
=
∑
i
=
0
n
−
1
a
i
x
i
f(x)=sum_{i=0}^{n-1}a_{i}x^{i}
f(x)=∑i=0n−1aixi和长度为
m
m
m多项式
g
(
x
)
=
∑
i
=
0
m
−
1
b
i
x
i
g(x)=sum_{i=0}^{m-1}b_{i}x^{i}
g(x)=∑i=0m−1bixi。求解多项式
h
(
x
)
=
f
(
x
)
×
g
(
x
)
h(x)=f(x)times g(x)
h(x)=f(x)×g(x)。
(
1
≤
n
≤
1
0
5
)
(1leq nleq 10^{5})
(1≤n≤105)
系数表示法和点值表示法
多项式的两种表示方法,系数表示法和点值表示法。
系数表示法:我们存储
f
(
x
)
f(x)
f(x)的每一项
x
i
x^{i}
xi的系数
a
i
a_{i}
ai。
点值表示法:由大家都知道知,
N
N
N个点可以确定一个最高次为
N
−
1
N-1
N−1多项式,所以我们可以存储多项式上任意
N
N
N个点,来确定这个多项式。
点值表示法有个性质是,对于多项式 f f f的一个点值 ( x , y 1 ) (x,y_{1}) (x,y1),和多项式 g g g的一个点值 ( x , y 2 ) (x,y_{2}) (x,y2),可以得到两个多项式相乘的多项式 h h h在横坐标为 x x x处的点为 ( x , y 1 × y 2 ) (x,y_{1}times y_{2}) (x,y1×y2)。
所以在对于长度为
n
n
n的多项式
f
f
f和长度为
m
m
m的多项式
g
g
g做乘法时,我们可以算出
f
f
f的
n
+
m
−
1
n+m-1
n+m−1个点值和
g
g
g的
n
+
m
−
1
n+m-1
n+m−1个点值,且要求两个多项式点值所取的
x
x
x相同。
我们就可以通过遍历算出这
n
+
m
−
1
n+m-1
n+m−1个横坐标的点值纵坐标相乘,来得到相乘后多项式的
n
+
m
−
1
n+m-1
n+m−1个点值,也就是新多项式的点值表示法。
(注意:虽然我们存储
f
f
f只需要
n
n
n个点值,但是为了计算出长度为
n
+
m
−
1
n+m-1
n+m−1的
h
h
h,我们仍要先得到
f
f
f的(n+m-1)个点值)
因此,假如我们可以通过 o ( n l o g n ) o(nlogn) o(nlogn)的办法,将系数表示法转化为点值表示法,然后将两个多项式点值 o ( n ) o(n) o(n)相乘,最后通过 o ( n l o g n ) o(nlogn) o(nlogn)的办法将点值表示法转化回系数表示法,就可以解决这个问题了。
后面来介绍如何将系数表示发和点值表示法进行转换。
离散傅里叶变换(DFT)
对于没有周期的函数,我们可以认为其周期为
∞
infty
∞,用无穷个周期函数去拟合。
同理,对于一个长度为
N
N
N的离散多项式
x
(
n
)
(
n
=
0
,
1
,
2..
N
−
1
)
x(n) (n=0,1,2..N-1)
x(n)(n=0,1,2..N−1),可以通过
N
N
N个周期函数来拟合。(
N
N
N个周期函数只能拟合
N
N
N个点,不能用来拟合连续多项式函数)
由此我们得到离散傅里叶级数展开式如下。
X
(
k
)
=
∑
n
=
0
N
−
1
x
(
n
)
e
−
2
π
i
k
n
N
X(k)=sum_{n=0}^{N-1}x(n)e^{-2pi ikfrac{n}{N}}
X(k)=n=0∑N−1x(n)e−2πikNn
其中
e
−
2
π
i
k
n
N
=
c
o
s
(
−
2
π
k
n
N
)
+
i
s
i
n
(
−
2
π
k
n
N
)
e^{-2pi ikfrac{n}{N}}=cos(-2pi kfrac{n}{N})+isin(-2pi kfrac{n}{N})
e−2πikNn=cos(−2πkNn)+isin(−2πkNn)是关于
n
n
n的周期为
N
N
N的函数。(由欧拉公式
e
i
x
=
c
o
s
x
+
i
s
i
n
x
e^{ix}=cos x+isin x
eix=cosx+isinx得)
离散傅里叶逆变换(IDFT)
对于变换后得到的
X
(
n
)
X(n)
X(n),我们可以通过离散傅里叶逆变换公式得到原函数
x
(
n
)
x(n)
x(n)。与
D
F
T
DFT
DFT公式差不多,在原来的基础上
i
i
i变成了
−
i
-i
−i,前面乘上一个
1
N
frac{1}{N}
N1。公式如下。
x
(
n
)
=
1
N
∑
k
=
0
N
−
1
X
(
k
)
e
2
π
i
k
n
N
x(n)=frac{1}{N}sum_{k=0}^{N-1}X(k)e^{2pi ikfrac{n}{N}}
x(n)=N1k=0∑N−1X(k)e2πikNn
快速傅里叶变换(FFT)
由前面讲过的,我们要解决的核心问题就是,算出多项式的
N
N
N个点值。
大致做法是,我们求出函数在离散傅里叶变换后的N个点值,由点值乘出新多项式后,再将其逆变换回系数表示法。
我们先来观察式子
D
F
T
DFT
DFT的式子。
我们为了简化式子,我们把
e
2
π
i
k
N
e^{frac{2pi ik}{N}}
eN2πik用
W
N
W_{N}
WN代替。(
W
N
W_{N}
WN是一个关于
k
k
k的函数)
得到式子如下。
X
(
k
)
=
∑
n
=
0
N
−
1
x
(
n
)
W
N
n
(
k
)
X(k)=sum_{n=0}^{N-1}x(n)W_{N}^{n}(k)
X(k)=n=0∑N−1x(n)WNn(k)
然后观察
W
N
W_{N}
WN如下。
W
N
(
k
)
=
e
2
π
i
N
=
c
o
s
(
2
π
k
N
)
+
i
s
i
n
(
2
π
k
N
)
W_{N}(k)=e^{frac{2pi i}{N}}=cos(frac{2pi k}{N})+isin(frac{2pi k}{N})
WN(k)=eN2πi=cos(N2πk)+isin(N2πk)
发现
W
N
W_{N}
WN是一个周期为
N
N
N的函数。
由此,我们发现变换后的函数 X ( k ) X(k) X(k)是由 N N N个周期为 N N N的函数的幂次,乘上一个系数,相加而成。且这 N N N个函数的实部和虚部都是三角函数( c o s cos cos和 s i n sin sin)。
我们很轻易地发现了
W
N
(
k
)
=
W
N
(
k
+
N
)
W_{N}(k)=W_{N}(k+N)
WN(k)=WN(k+N),但是这对我们来说不重要。更重要的性质源于三角函数的另一个性质——对称性。
我们把
[
0
,
2
π
]
[0,2pi]
[0,2π]分成两部分
[
0
,
π
)
[0,pi)
[0,π),和
(
π
,
2
π
]
(pi,2pi]
(π,2π],可以发现我们算出左边
t
t
t 处的值,就可以得到右边
π
+
t
pi +t
π+t处的值。
说白了就是
s
i
n
(
t
)
=
−
s
i
n
(
π
+
t
)
sin(t)=-sin(pi +t)
sin(t)=−sin(π+t)和
c
o
s
(
t
)
=
−
c
o
s
(
π
+
t
)
cos(t)=-cos(pi +t)
cos(t)=−cos(π+t)。
对应到上面就得到了一个结论,我们只要算出了 W N ( t ) W_{N}(t) WN(t),就可以得到 W N ( N 2 + t ) W_{N}(frac{N}{2}+t) WN(2N+t)。即 W N ( t ) = − W N ( N 2 + t ) W_{N}(t)=-W_{N}(frac{N}{2}+t) WN(t)=−WN(2N+t)。
上面只是简单的性质推导,正式的傅里叶变换在下面。
总而言之就是要分治,这个式子可以和分治有机结合。(我也不知道他怎么想出来的,公式推导在《数字信号处理》第四版的P122)
我们把枚举的奇数项和偶数项分离开。
X
(
k
)
=
∑
n
为
偶
数
x
(
n
)
W
N
n
(
k
)
+
∑
n
为
奇
数
x
(
n
)
W
N
n
(
k
)
=
∑
r
=
0
N
2
−
1
x
(
2
r
)
W
N
2
r
(
k
)
+
∑
r
=
0
N
2
−
1
x
(
2
r
+
1
)
W
N
2
r
+
1
(
k
)
=
∑
r
=
0
N
2
−
1
x
1
(
r
)
W
N
2
r
(
k
)
+
W
N
(
k
)
∑
r
=
0
N
2
−
1
x
2
(
r
)
W
N
2
r
(
k
)
=
∑
r
=
0
N
2
−
1
x
1
(
r
)
W
N
2
r
(
k
)
+
W
N
(
k
)
∑
r
=
0
N
2
−
1
x
2
(
r
)
W
N
2
r
(
k
)
=
X
1
(
k
)
+
W
N
(
k
)
X
2
(
k
)
X(k)=sum_{n为偶数}x(n)W_{N}^{n}(k)+sum_{n为奇数}x(n)W_{N}^{n}(k) \=sum_{r=0}^{frac{N}{2}-1}x(2r)W_{N}^{2r}(k)+sum_{r=0}^{frac{N}{2}-1}x(2r+1)W_{N}^{2r+1}(k) \=sum_{r=0}^{frac{N}{2}-1}x_{1}(r)W_{N}^{2r}(k)+W_{N}(k)sum_{r=0}^{frac{N}{2}-1}x_{2}(r)W_{N}^{2r}(k) \=sum_{r=0}^{frac{N}{2}-1}x_{1}(r)W_{frac{N}{2}}^{r}(k)+W_{N}(k)sum_{r=0}^{frac{N}{2}-1}x_{2}(r)W_{frac{N}{2}}^{r}(k) \=X_{1}(k)+W_{N}(k)X_{2}(k)
X(k)=n为偶数∑x(n)WNn(k)+n为奇数∑x(n)WNn(k)=r=0∑2N−1x(2r)WN2r(k)+r=0∑2N−1x(2r+1)WN2r+1(k)=r=0∑2N−1x1(r)WN2r(k)+WN(k)r=0∑2N−1x2(r)WN2r(k)=r=0∑2N−1x1(r)W2Nr(k)+WN(k)r=0∑2N−1x2(r)W2Nr(k)=X1(k)+WN(k)X2(k)
其中
X
1
X_{1}
X1是
x
1
x_{1}
x1的傅里叶变换,
X
2
X_{2}
X2是
x
2
x_{2}
x2的傅里叶变换。
其中有一步
W
N
2
r
(
k
)
=
W
N
2
r
W_{N}^{2r}(k)=W_{frac{N}{2}}^{r}
WN2r(k)=W2Nr前面忘记证了,想必读者应该一下子就能证出来了。
到此位置,所有准备工作都做完了,容易发现只要分治做好两个小多项式的点值,大的自然就出来了。所以可以分治做。
最后式子如下。
X
(
k
)
=
X
1
(
k
)
+
W
N
k
X
2
(
k
)
,
k
=
0
,
1
,
…
,
N
2
−
1
X
(
k
+
N
2
)
=
X
1
(
k
)
−
W
N
k
X
2
(
k
)
,
k
=
0
,
1
,
…
,
N
2
−
1
X(k)=X_{1}(k)+W^{k}_{N}X_{2}(k),k=0,1,…,frac{N}{2}-1 \X(k+frac{N}{2})=X_{1}(k)-W^{k}_{N}X_{2}(k),k=0,1,…,frac{N}{2}-1
X(k)=X1(k)+WNkX2(k),k=0,1,…,2N−1X(k+2N)=X1(k)−WNkX2(k),k=0,1,…,2N−1
但是有一个细节问题,计算
W
N
(
k
)
W_{N}(k)
WN(k)的复杂度是
o
(
l
o
g
n
)
o(logn)
o(logn)。而在分治的每一层我们需要求
N
N
N次
W
W
W函数,总共分治
l
o
g
n
logn
logn层,最终导致我们的复杂度变成了
o
(
n
l
o
g
n
l
o
g
n
)
o(nlognlogn)
o(nlognlogn)。
那么解决办法是前面提的
W
N
(
t
)
=
−
W
N
(
N
2
+
t
)
W_{N}(t)=-W_{N}(frac{N}{2}+t)
WN(t)=−WN(2N+t),我们每层只需要计算一半的
W
W
W,也就是
N
2
frac{N}{2}
2N个
W
W
W,于是在计算
W
W
W上的总开销就变成了
o
(
n
2
l
o
g
n
l
o
g
n
)
o(frac{n}{2}lognlogn)
o(2nlognlogn)。有锅巴用。
然后又发现一个可以优化的地方。既然
W
N
(
k
)
=
−
W
N
(
N
2
+
k
)
W_{N}(k)=-W_{N}(frac{N}{2}+k)
WN(k)=−WN(2N+k)来减少
W
W
W的计算次数(主要是减少
s
i
n
sin
sin和
c
o
s
cos
cos的计算次数),主要是来源于三角函数知道
[
0
,
π
]
[0,pi]
[0,π]可以推演出
[
π
,
2
π
]
[pi,2pi]
[π,2π]。这样只需要在每层算
N
2
frac{N}{2}
2N次
c
o
s
cos
cos和
s
i
n
sin
sin,所以我们再往下细化一层,使得每层只需要计算
N
4
frac{N}{4}
4N次
c
o
s
cos
cos和
s
i
n
sin
sin。
那么同理,三角函数知道
[
0
,
π
2
]
[0,frac{pi}{2}]
[0,2π]就可以推算出
[
π
2
,
2
π
]
[frac{pi}{2},2pi]
[2π,2π]。
所以,我们开始推
W
N
(
k
+
N
4
)
W_{N}(k+frac{N}{4})
WN(k+4N)。
W
N
(
k
+
N
4
)
=
c
o
s
(
2
π
k
N
+
π
2
)
+
i
s
i
n
(
2
π
k
N
+
π
2
)
=
−
s
i
n
(
2
π
k
N
)
+
i
c
o
s
(
2
π
k
N
)
W_{N}(k+frac{N}{4})=cos(frac{2pi k}{N}+frac{pi}{2})+isin(frac{2pi k}{N}+frac{pi}{2})=-sin(frac{2pi k}{N})+icos(frac{2pi k}{N})
WN(k+4N)=cos(N2πk+2π)+isin(N2πk+2π)=−sin(N2πk)+icos(N2πk)
然后,我们发现,我们对于某个
k
k
k计算出的
s
i
n
sin
sin和
c
o
s
cos
cos函数,可以沿用至
k
+
N
4
k+frac{N}{4}
k+4N,
k
+
N
2
k+frac{N}{2}
k+2N,
k
+
3
N
4
k+frac{3N}{4}
k+43N。
所以,在每一层,我们只需要计算
N
4
frac{N}{4}
4N次
W
W
W。
然后复杂度被优化到了
o
(
n
4
l
o
g
n
l
o
g
n
)
o(frac{n}{4}lognlogn)
o(4nlognlogn)。仍然没有任何卵用。
其实我们只需要算
W
N
(
1
)
W_{N}(1)
WN(1),有
W
N
(
k
+
1
)
=
W
N
(
k
)
×
W
N
(
1
)
W_{N}(k+1)=W_{N}(k)times W_{N}(1)
WN(k+1)=WN(k)×WN(1),这才是真的有用。证明可以考虑这个复数的几何意义,就是把位于圆心的单位圆划等分成了
N
N
N个部分,取出第一个部分就相当于是一个旋转矩阵,幂次就是旋转的次数。
于是,我们只要在开始用三角函数算出
W
N
(
1
)
W_{N}(1)
WN(1),后面一直乘上它就行了。
然后就做完了。
关于转换回系数,也就是逆变换,发现式子和这个差不多,所以做法也基本一样。记得最后要除以 N N N。这里就不重复介绍了。
为了方便起见,一般先把多项式长度拉到2的幂次,然后再进行变换,方便递归到底层为1。反过来说,要使用FFT板子时,注意长度应为2的幂次。
虽然是分治,但为了常数考虑,一般都用非递归版的写法。
递归版
递归版常数很大,强烈建议找个常数小的非递归版代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll mod=1e9+7;
const double pi = acos(-1);
const int MAXN = 3000005;
struct cp
{
double s,v;
cp(double ss=0,double vv=0){
s=ss;v=vv;
}
};
cp operator * (cp a,cp b)
{
return cp(a.s*b.s-a.v*b.v,a.s*b.v+a.v*b.s);
}
cp operator + (cp a,cp b)
{
return cp(a.s+b.s,a.v+b.v);
}
cp operator - (cp a,cp b)
{
return cp(a.s-b.s,a.v-b.v);
}
cp operator / (cp a,int b)
{
return cp(a.s/b,a.v/b);
}
//预处理出2的幂次的cos和sin
double coss[MAXN],sinn[MAXN];
void fft(cp *a,int len,int opt)
{
if(len==1)return ;
static cp b[MAXN];
int mid=len>>1,mid_2=mid>>1;
for(int i=0;i<len;i++)b[(i>>1)|(i&1?mid:0)]=a[i];
for(int i=0;i<len;i++)a[i]=b[i];
fft(a,mid,opt);fft(a+mid,mid,opt);
cp uni(coss[mid],opt*sinn[mid]),w(1,0);
for(int k=0;k<len;k++)
{
if(k<mid)b[k]=a[k]+w*a[k+mid];
else b[k]=a[k-mid]+w*a[k];
w=w*uni;
}
for(int i=0;i<len;i++)a[i]=b[i];
}
void mul(cp *a,cp *b,int len)
{
fft(a,len,1);
fft(b,len,1);
for(int i=0;i<len;i++)a[i]=a[i]*b[i];
fft(a,len,-1);
for(int i=0;i<len;i++)a[i]=a[i]/len;
}
string sa,sb,sans;
cp a[MAXN],b[MAXN];
int ans[MAXN];
int main()
{
ios::sync_with_stdio(0);
cin.tie(0);cout.tie(0);
cin>>sa>>sb;
int n=sa.size(),m=sb.size();
int len=1;
while(len<n+m-1)coss[len]=cos(pi/len),sinn[len]=sin(pi/len),len<<=1;
for(int i=0;i<len;i++)
{
a[i]=i<n?cp(sa[n-i-1]-'0',0):cp(0,0);
b[i]=i<m?cp(sb[m-i-1]-'0',0):cp(0,0);
}
mul(a,b,len);
for(int i=0;i<len;i++)
{
ans[i]+=round(a[i].s);
ans[i+1]+=ans[i]/10;
ans[i]%=10;
}
int flag=0;
for(int i=len-1;i>=0;i--)
{
if(ans[i]==0&&flag==0)continue;
else
{
flag=1;
sans+=(char)(ans[i]+'0');
}
}
cout<<sans<<endl;
return 0;
}
非递归版
最后
以上就是美满烧鹅为你收集整理的快速傅里叶变换FFT简明教程的全部内容,希望文章能够帮你解决快速傅里叶变换FFT简明教程所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复