FFT(快速傅里叶变换)


多项式

多项式系数表示

即:

如果要我们求两个多项式的乘积后每一项的系数

例如:

计算 $f_1(x)\times f_2(x)$ 每一项的系数。

在数学课上我们学的是将每一项两两相乘即:$3\times 1+3\times 6x+3\times 8x^2+…+2x^2\times 8x^2+2x^2\times 9x^3$

算出:

这使得在项非常多的时候计算非常麻烦,复杂度 $O(n^2)$。

还有什么别的办法得出 $f(x)$ 么?

多项式点值表示

我们知道,如果要确认一个 $x$ 次函数需要有 $x+1$ 个点就可以确认。前提是我们选的这些点两两不同。

矩阵表达为(范德蒙德矩阵):

但是这样的复杂仍然是 $O(n^2)$(找到 $x+1$ 个点,每个点计算它的 $y$ 值做后的复杂度仍然很大)

难道没有办法优化了么?

奇偶性

在学习而二次函数的时候,我们知道函数关于 $y$ 轴对称,所以如果我们知道了 $(x_0,y_0)$,那么我们同样知道了 $(x_1,y_0)$在函数上,($x_0+x_1=0$)。

例如:$f(x)=x^2$

当 $x=2$ 时,$f(x)=4$,即点($2,4$)在函数上,此时我们同样知道($-2,4$)在函数上。

同样:$fx)=x^3$

当 $x=3$ 时,$f(x)=8$,即点($2,8$)在函数上,此时我们同样知道($-2,-8$)在函数上。

正是因为我们知道它们含有奇偶性,那么就可以一对一对的得知点的坐标。

这样就不用枚举 $x+1$ 个点了,只要找到其中的一半就可以了。

我们根据它的奇偶性将原多项式整理一下:

设:

$f_1(x)=a_0+a_2x^2+a_4x^4+…$

$f_2(x)=a_1+a_3x^2+a_5x^4+…$

那么我们再根据我们的奇偶性进行带入:

可以发现只有一项不同,所以只要我们计算了 $f_1(x^2)$,$f_2(x^2)$,就可以得知 $f(x)$,$f(-x)$ 的值。

形象的描述一下:

! # ! # ! # ! # ! # ! # ! # ! #

! ! ! ! ! ! ! ! <---> # # # # # # # # 

! # ! # ! # ! # <---> ! # ! # ! # ! # 

! ! ! ! <-> # # # # <---> ! ! ! ! <-> # # # #

直到全部分开。

这时我们的复杂度降到了 $O(n \log n)$(和线段树的树形结构一样)。

但是仔细一看会发现 $f_1$ 和 $f_2$ 运算时 $x$ 取不到负,这显然是个缺陷的。那么怎么完善呢,就是让 $x^2=-1$ 成为可能。

这是就要把坐标系换成复数的了。

复数运算

单位根

单位根是什么呢?

通俗来讲就是将一个在坐标系上的一个圆心在原点,半径为 $1$。将整个原的周长等分成 $n$ 份,那么有 $n$ 个断点,断点的坐标即为一个单位。$\omega^k_n$ 代表分为 $n$ 等份的单位圆的第 $k$ 个断点的坐标。

公式:$\omega^k_n=\cos(\frac{2k\pi}{n})+i\sin(\frac{2k\pi}{n})$

幂运算

化简

共轭

相反数

结合单位根和多项式

将 $\omega^k_n$和 $\omega^{k+\frac{n}{2}}_n$ 分别带入。

得到:

即:

这个矩阵是 $DFT$(离散傅里叶变换)

点值转系数

现在我们要处理系数,所以将矩阵变换一下:

逆矩阵求出来后:

设 $c$ 向量为值对于单位根的变换。

即:

设:

所以当 $k$ 不为 $0$ 时,$S=0$,二挡 $k=0$ 时,推导式分母为零,所以不能使用推导式。但是我们可以直接带入原式:

所以,当 $j$ 不为 $k$ 时,结果为零:

当 $k=0$ 时,结果为 $n$:

这样就找到了点值和系数的关系。

到此可以写的递推的代码了,但是会TLE,所以还需要优化。

二进制反转

在处理多项式递推时,我们用的方法是奇偶分离:

! # ! # ! # ! # ! # ! # ! # ! #

! ! ! ! ! ! ! ! <—-> # # # # # # # #

! # ! # ! # ! # <—-> ! # ! # ! # ! #

! ! ! ! <-> # # # # <—-> ! ! ! ! <-> # # # #

但是,有另一个技巧是,分离最后的顺序为二进数反转:

原数:       0,  1,  2,  3,  4,  5,  6,  7

二进制:    000,001,010,011,100,101,110,111

分离后:      0,  4,  2,  6,  1,  5,  3,  7

二进制:    000,100,010,110,001,101,011,111

所以可以直接排好序然后直接从底层分好块遍历。

处理二进制反转(蝴蝶变换)

用已经处理好的二进制数右移一位,判断移走的哪位是否为 $1$,直接补在最左边。

for(int i=0;i<m_a;i++)e_d[i]=(e_d[i>>1]>>1)|((i&1)<<(l_g-1));
//反转的二进制 

有一个可以让 for 优化 $1$ 毫秒的办法,但由于性价比太小导致我们并不注意,但如果这个 for 调用了几百次甚至几千次,那么将它优化后,就有可能从 TLE变成 AC

在遍历 $f_1,f_2$ 时,如果直接套公式的话:


a_a[j+k_k]=a_a[j+k_k]+k*a_a[j+m_i+k_k];
a_a[j+m_i+k_k]=a_a[j+k_k]-k*a_a[j+m_i+k_k];

其中,k*a_a[j+m_i+k_k] 被调用了 $2$ 两变,我们可以将它存入新变量中,直接调取变量即可,这样少运算了一次.

po x_x=a_a[j+k_k];
po y_y=k*a_a[j+m_i+k_k];
a_a[j+k_k]=x_x+y_y;
a_a[j+m_i+k_k]=x_x-y_y;

实测,快了 $0.12s$,平常不算什么,但对于卡常的时候来说就是天赐良药。

公式变为:

也方便了代码的书写。

P3803 【模板】多项式乘法(FFT)

代码

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cmath>
#include<cstring>
#include<string>
#include<cctype>
using namespace std;
long long r_r(){//快读 
    long long x=0,f=1;
    char c=getchar();
    while(!isdigit(c)){
        if(c=='-')f=-1;
        c=getchar();
    }
    while(isdigit(c)){
        x=(x<<1)+(x<<3)+(c^48);
        c=getchar();
    }
    return x*f;
}
const long long o_o=3e6+10;
long long k_m(long long a,long long b){//快速幂 
    long long r_s=1;
    while(b){
        if(b&1)r_s*=a;
        a*=a;
        b>>=1;
    }
    return r_s;
}
struct po{
    double x,y;
    
    //重载基本运算 
    po operator +(const po &n)const{
        return po{x+n.x,y+n.y};
    }
    po operator -(const po &n)const{
        return po{x-n.x,y-n.y};
    }
    po operator *(const po &n)const{
        return po{x*n.x-y*n.y,x*n.y+y*n.x};
        //复数乘法 
    }
    
}a_i[o_o],b_i[o_o];
const double PI=acos(-1.0);//pi 的值 
int n=r_r(),m=r_r();
int e_d[o_o];//最后的数的位置 
int l_g;//需要遍历多少层 
int m_a;//目标最大块长 
void f_i(po *a_a,int f){
    for(int i=0;i<m_a;i++)//根据排好的顺序调整数列 
        if(i<e_d[i])swap(a_a[i],a_a[e_d[i]]);
    for(int m_i=1;m_i<m_a;m_i<<=1){//树形结构(分治) 
        po w={cos(PI/m_i),f*sin(PI/m_i)};//赋初值 
        for(int r=m_i<<1,j=0;j<m_a;j+=r){//枚举当前操作的每个块的范围 
            po k=po{1,0};//幂(初值) 
            for(int k_k=0;k_k<m_i;k_k++,k=k*w){//幂的累计 
                po x_x=a_a[j+k_k];//计算 f1 
                po y_y=k*a_a[j+m_i+k_k];//计算 f2 
                //节约小时间,积累成大时间:“蝴蝶效应” 
                
                a_a[j+k_k]=x_x+y_y;//计算 f 的 k 次 
                a_a[j+m_i+k_k]=x_x-y_y;//计算 f 的 k+(n/2) 次 
            }
        }
    }
}
int main(){
    for(int i=0;i<=n;i++)a_i[i].x=r_r();
    for(int i=0;i<=m;i++)b_i[i].x=r_r();//读入 
    l_g=log2(n+m)+1;
    m_a=k_m(2,l_g);//计算边界 
    for(int i=0;i<m_a;i++)e_d[i]=(e_d[i>>1]>>1)|((i&1)<<(l_g-1));//反转的二进制 
    f_i(a_i,1);
    f_i(b_i,1);//系数变点值 
    for(int i=0;i<=m_a;i++)a_i[i]=a_i[i]*b_i[i];//计算结果 
    f_i(a_i,-1);//点值变系数 
    for(int i=0;i<=n+m;i++)printf("%d ",(int)(a_i[i].x/m_a+0.5));//输出 
    return 0;
}

文章作者: 王大神——A001
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 王大神——A001 !
  目录