[总结] 坑逼FFT的入门

简单的预备知识

系数表达

用多项式每一项的系数表示多项式

点值表达

n 个横坐标各不相同的点 ( x , y ) 来表示一个次数界为 n 的多项式

求值

系数表达 => 点值表达
O ( n 2 ) 霍纳法则

插值

点值表达 => 系数表达
O ( n 2 ) 拉格朗日公式

系数表达式求多项式加,乘法

A = { a 0 , a 1 , , a n 1 }
B = { a 0 , a 1 , , a n 1 }

C = A + B = { a 0 + a 0 , a 1 + a 1 , a n 1 + a n 1 }
C = A B , c x = i = 0 x a i a x i

点值表达式求多项式加,乘法

A = { ( x 0 , y 0 ) , ( x 1 , y 1 ) , , ( x n 1 , y n 1 ) }
B = { ( x 0 , y 0 ) , ( x 1 , y 1 ) , , ( x n 1 , y n 1 ) }

C = A + B = { ( x 0 , y 0 + y 0 ) , ( x 1 , y 1 + y 1 ) , , ( x n 1 , y n 1 + y n 1 ) }
C = A B = { ( x 0 , y 0 y 0 ) , ( x 1 , y 1 y 1 ) , , ( x n 1 , y n 1 y n 1 ) }


显然的,用系数表达式算多项式乘法简直是龟速 O ( n 2 )
而使用点值表达式算多项式乘法则快很多 ( O ( n ) )

可以看出主要若使用点值表达式,主要复杂度在求值和插值(均为 O ( n 2 ) )
显然如果求值插值不能优化的话其复杂度甚至是不如系数乘法的(常数大)
所以我们可以利用一些特殊的方式来加速

引入单位复数根

n 次单位复数根是满足 w n = 1 的复数 w , n 次单位复数根恰有 n
对于 k = 0 , 1 , , n 1 ,这些根是 e 2 π i k n .( e π i = 1 , e 2 π i = 1 )
用复数的指数形式的定义 e u i = c o s ( u ) + i s i n ( u )
可以建立一个以实数为 x 轴,以虚数为 y 轴的坐标系
然后这些单位复数根在坐标轴上正好是半径为1的圆上的点
我们将 e 2 π i 1 n 称为主 n 次单位根,利用它,其他的单位复数根都是其幂次
然后这些单位复数根的乘法就相当于角度的转换
单位复数根之间满足乘法群的性质 ( mod n )

消去引理: w d n d k = w n k
* 证明: w d n d k = ( e 2 π i 1 d n ) d k = ( e 2 π i 1 n ) k = w n k

推论: w 2 n n = 1

折半引理: n 为偶数,则 n 次单位复数根的平方的集合就是 n 2 次单位复数根的集合,特别的,每个 n 2 次单位复数根出现两次
* 证明: ( w n k ) 2 = w n 2 k , k [ 0 , n 1 ] [消去引理]

正式开始

对于 n 次多项式,我们希望取得其在 w n 0 , w n 1 , , w n n 1 处的值
相当于求这样一个矩阵:

[ A 0 A 1 . . . A n 1 ]   = [ w n 0 w n 0 w n 0 w n 0 w n 1 w n n 1 . . . . . . . . w n 0 w n n 1 w n ( n 1 ) ( n 1 ) ] [ a 0 a 1 . . . a n 1 ]

其中 A i 为取到 w n i 时的点值, a i 为第 i 位的系数
A ( i ) = j = 0 n 1 w n i j a j

现定义:
A [ 0 ] ( x ) = a 0 + a 2 x + + a n 2 x n 2 2
A [ 1 ] ( x ) = a 1 + a 3 x + + a n 1 x n 2 2
易得:
A ( x ) = A [ 0 ] ( x 2 ) + A [ 1 ] ( x 2 )

因为当 x = w n k x = w n k + n 2 时,两数平方相等
所以说我们的取值集合就缩小了一半
这样一直递归下去
每层有 2 d 个块,每个块有 n 2 d 个取值,所以一共只需计算 n 次,共 l o g ( n ) 层,总时间复杂度为 O ( n l o g ( n ) )
非常的 n i c e
这个过程也叫做 D F T

得到点值表达式以后我们就只需要 O ( n ) 的点值乘法,这里不再赘述

那么接下来的任务就是插值了,也就是逆 D F T
易得这个矩阵:

[ C 0 C 1 . . . C n 1 ]   = [ w n 0 w n 0 w n 0 w n 0 w n 1 w n n 1 . . . . . . . . w n 0 w n n 1 w n ( n 1 ) ( n 1 ) ] [ c 0 c 1 . . . c n 1 ]

其中 C i 为取到 w n i 时的点值, c i 为第 i 位的系数
所以 c = C V 1
V 1 V = 单位矩阵
所以 V 1 中位于第 i 行第 j 列(均从0开始标号)的元素为 w n i j n
所以
c i = 1 n j = 0 n 1 C j ( w n i ) j

是不是和原来的公式很像?
A ( i ) = j = 0 n 1 a j w n i j

只不过 w 的系数变成了负的
多了一个 1 n 而已
发现也可以 D F T 解决
这样就做完啦!

但是具体代码中还是有丶东西
看注释吧!

代码如下:

#include <bits/stdc++.h>
using namespace std;
const int N =100010;
const double pi=acos(-1);
struct Complex {
    double x;
    double y;
    Complex() {};
    Complex(double _x,double _y) { x=_x;y=_y; }
    Complex operator + (const Complex o) { return Complex(x+o.x,y+o.y); }
    Complex operator - (const Complex o) { return Complex(x-o.x,y-o.y); }
    Complex operator * (const Complex o) { return Complex(x*o.x-y*o.y,x*o.y+y*o.x); }
}a[N<<2],b[N<<2];
int r[N<<2];
int n,m,nn;
void fft(Complex *now,int f) {
    for(int i=0;i<n;++i)//按块将其分到一起
        if(i<r[i]) swap(now[i],now[r[i]]);
    for(int i=1;i<n;i<<=1) {//枚举合并的块的大小
        Complex wn(cos(pi/i),f*sin(pi/i));//单位元,若进行逆DFT变换,则反向旋转
        for(int j=0;j<n;j+=(i<<1)) {//枚举头
            Complex w(1,0);
            for(int k=0;k<i;++k,w=w*wn) {//合并,蝴蝶变换
                Complex x=now[j+k],y=w*now[j+k+i];
                now[j+k]=x+y;
                now[j+k+i]=x-y;
            }
        }
    }
    //神奇的是蝴蝶变换完了以后他又恢复正常顺序了,我也不知道为什么
    if(f==-1)
        for(int i=0;i<n;++i)
            now[i].x/=n;
}
int main() {
    scanf("%d%d",&n,&m);
    for(int i=0;i<=n;++i) scanf("%lf",&a[i].x);
    for(int i=0;i<=m;++i) scanf("%lf",&b[i].x);
    m+=n;
    for(n=1;n<=m;n<<=1) nn++;
    for(int i=0;i<n;++i) {r[i]=(r[i>>1]>>1)|((i&1)<<(nn-1));}
    fft(a,1);fft(b,1);
    for(int i=0;i<=n;++i) { a[i]=a[i]*b[i]; }
    fft(a,-1);//逆DFT
    for(int i=0;i<=m;++i)
        printf("%d ",(int)(a[i].x+0.5))//防止精度出锅;
    return 0;
}

猜你喜欢

转载自blog.csdn.net/Force_CHL/article/details/80656696