我是靠谱客的博主 美满烧鹅,最近开发中收集的这篇文章主要介绍快速傅里叶变换FFT简明教程,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

问题

给出长度为 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=0n1aixi和长度为 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=0m1bixi。求解多项式 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}) (1n105)

系数表示法和点值表示法

多项式的两种表示方法,系数表示法和点值表示法。
系数表示法:我们存储 f ( x ) f(x) f(x)的每一项 x i x^{i} xi的系数 a i a_{i} ai
点值表示法:由大家都知道知, N N N个点可以确定一个最高次为 N − 1 N-1 N1多项式,所以我们可以存储多项式上任意 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+m1个点值和 g g g n + m − 1 n+m-1 n+m1个点值,且要求两个多项式点值所取的 x x x相同。
我们就可以通过遍历算出这 n + m − 1 n+m-1 n+m1个横坐标的点值纵坐标相乘,来得到相乘后多项式的 n + m − 1 n+m-1 n+m1个点值,也就是新多项式的点值表示法。
(注意:虽然我们存储 f f f只需要 n n n个点值,但是为了计算出长度为 n + m − 1 n+m-1 n+m1 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..N1),可以通过 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=0N1x(n)e2π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}) e2π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=0N1X(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=0N1x(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)=nx(n)WNn(k)+nx(n)WNn(k)=r=02N1x(2r)WN2r(k)+r=02N1x(2r+1)WN2r+1(k)=r=02N1x1(r)WN2r(k)+WN(k)r=02N1x2(r)WN2r(k)=r=02N1x1(r)W2Nr(k)+WN(k)r=02N1x2(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,,2N1X(k+2N)=X1(k)WNkX2(k),k=0,1,,2N1

但是有一个细节问题,计算 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简明教程所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(58)

评论列表共有 0 条评论

立即
投稿
返回
顶部