FFT/NTT/FWT学习笔记

最近舟游疯狂出货,心情很好~

FFT


快速傅里叶变换(FFT)

具体的推导见这篇:胡小兔 - 小学生都能看懂的FFT!!! (写的很好,不过本小学生第一次没看懂0.0)

总结下关键内容

~ Part 1 ~ 多项式表示转点值表示:

将原多项式$A(x)$的次数补成$2$的幂次,然后进行递归拆分

设当前一层的多项式为$B(x)$,项数为$n$,即:$B(x)=b_0+b_{1}x+b_{2}x^{2}+...+b_{n-1}x^{n-1}$

先将此多项式的按照奇偶项系数拆成两个式子

\[B_0(x)=b_0+b_2x+b_4x^2+...+b_{n-2}x^{\frac{n}{2}-1}\]

\[B_1(x)=b_1+b_3x+b_5x^2+...+b_{n-1}x^{\frac{n}{2}-1}\]

那么可以将$B(x)$重新表示:$B(x)=B_0(x^2)+xB_1(x^2)$

我们希望将复平面上单位圆的点代入,得到$B(\omega^{1}_{n})$,...,$B(\omega^{n}_{n})$的值

若$0\leq i<\frac{n}{2}$,则有【这里使用了复数运算律:因为$\omega$在单位圆上,所以$\omega^i_n\cdot\omega^j_n=\omega^{i+j}_n$;因为$n$为$2$的倍数,所以$\omega^i_n=\omega^{\frac{i}{2}}_{\frac{n}{2}}$】

\begin{align*}B(\omega^i_n)&=B_0((\omega^i_n)^2)+\omega^i_nB_1((\omega^i_n)^2)\\&=B_0(\omega^{2i}_n)+\omega^i_nB_1(\omega^{2i}_n)\\&=B_0(\omega^i_{\frac{n}{2}})+\omega^i_nB_0(\omega^i_{\frac{n}{2}})\end{align*}

而对于单位圆上的另一半,则有【这里追加一个复数运算律:因为$n$为$2$的倍数,所以$\omega^{i+\frac{n}{2}}_n=-\omega^i_n$】

\begin{align*}B(\omega^{i+\frac{n}{2}}_n)&=B_0((\omega^{i+\frac{n}{2}}_n)^2)+\omega^{i+\frac{n}{2}}_nB_1((\omega^{i+\frac{n}{2}}_n)^2)\\&=B_0(\omega^{2i+n}_n)-\omega^i_nB_1(\omega^{2i+n}_n)\\&=B_0(\omega^i_{\frac{n}{2}})-\omega^i_nB_0(\omega^i_{\frac{n}{2}})\end{align*}

所以我们将这一层计算$B(\omega^0_n)$,...,$B(\omega^{n-1}_{n-1})$的问题变为计算下一层$B_0(\omega^0_{\frac{n}{2}})$,...,$B_0(\omega^{\frac{n}{2}-1}_{\frac{n}{2}})$,$B_1(\omega^0_{\frac{n}{2}})$,...,$B_1(\omega^{\frac{n}{2}-1}_{\frac{n}{2}})$

这样,总的复杂度是$O(N logN)$

~ Part 2 ~ 点值转多项式系数

设原多项式为$A(x)=a_0+a_1x+a_2x^2+...+a_{n-1}x^{n-1}$,其中补成$2$的幂次项后项数为$n$

则分别代入$\omega^0_n$,$\omega^1_n$,...,$\omega^{n-1}_n$,得到$y_i=A(\omega^i_n)$,$i=0,1,...,n-1$,即上面得到的$n$个点值

令$B(x)=y_0+y_1x+y_2x^2+...+y_{n-1}x^{n-1}$

再分别代入$\omega^0_n$,$\omega^{-1}_n$,...,$\omega^{-(n-1)}_n$,运算(这里略去)得到$a_i=\frac{B(\omega^{-i}_n)}{n}$,$i=0,1,...,n-1$

按这个思路写出的代码如下(实现了多项式系数转点值,再转回多项式系数)

大佬用的数组指针确实方便

#include <cstdio>
#include <complex>
#include <cmath>
#include <cstring>
#include <algorithm>
using namespace std;

typedef complex<double> cp;
const int N=100005;
const double pi=acos(-1.0);

inline cp omega(int x,int n,int rev)
{
    if(rev)
        x=-x;
    return cp(cos(x*2*pi/(double)n),sin(x*2*pi/(double)n));
}

cp tmp[N];

void fft(cp *a,int n,int rev)
{
    if(n==1)
        return;
    
    for(int i=0;i<n;i++)
        tmp[i]=a[i];
    for(int i=0;i<n/2;i++)
    {
        a[i]=tmp[i*2];
        a[n/2+i]=tmp[i*2+1];
    }
    
    fft(a,n/2,rev);
    fft(a+n/2,n/2,rev);
    
    for(int i=0;i<n/2;i++)
    {
        cp x=omega(i,n,rev);
        tmp[i]=a[i]+x*a[n/2+i];
        tmp[n/2+i]=a[i]-x*a[n/2+i];
    }
    
    for(int i=0;i<n;i++)
        a[i]=tmp[i];
}

int n;
cp a[N];

int main()
{
//    freopen("input.txt","r",stdin);
    scanf("%d",&n);
    for(int i=0;i<n;i++)
    {
        int x;
        scanf("%d",&x);
        a[i]=cp((double)x,0);
    }
    
    int sz=1;
    while(sz<n)
        sz<<=1;
    n=sz;
    
    fft(a,n,0);
    fft(a,n,1);
    
    for(int i=0;i<n;i++)
        printf("%.2lf ",a[i].real()/(double)n);
    printf("\n");
    return 0;
}
View Code

~ Part 3 ~  两个简单的优化

非递归:

将递归的第一部分,即交换$a[i]$以奇偶分组,直接完成

这里的交换位置是两两一对的——若$n=8$,则$4$($100$)最终将与$1$($001$)互换

即两个交换位置的二进制是前后翻转的

去除$tmp[i]$数组:

对于递归的第二部分for循环中的每个$i$,只会用到$tmp[i]$、$tmp[n/2+i]$、$a[i]$、$a[n/2+i]$

所以对于当前$i$的改变是无后效性的,可以考虑使用临时变量而不是$tmp[i]$数组

于是,可以写出没有递归与$tmp[i]$数组的$fft()$函数

void fft(cp *a,int n,int rev)
{
    for(int i=0;i<n;i++)
    {
        int nxt=0;
        for(int j=1;j<=n;j<<=1)
            if(i&j)
                nxt+=n/2/j;
        
        if(nxt>i)
            swap(a[i],a[nxt]);
    }
    
    for(int i=2;i<=n;i<<=1)
        for(int j=0;j<n;j+=i)
            for(int k=0;k<i/2;k++)
            {
                cp x=omega(k,i,rev);
                cp b0=a[j+k],b1=a[i/2+j+k];
                a[j+k]=b0+x*b1;
                a[i/2+j+k]=b0-x*b1;
            }
}
View Code

用FFT计算卷积

(待续)

猜你喜欢

转载自www.cnblogs.com/LiuRunky/p/FFT_NTT_FWT.html