毒瘤的FFT笔记

自从机房掀起学习数学之风,我便跟风学起了数学(其实是因为不会)

众所周知FFT很有用(一开始真是不太好理解)。

首先上大佬的博客:

1.胡小兔(对不起我不如小学生)

2.路人黑的纸巾

3.GGN_2015

4.自为风月马前卒

接下来是正文(我抄的)。

FFT(Fast Fourier Transformation),中文名快速傅里叶变换,用来加速多项式乘法

前置知识:无吧?(反正我上数学课顺便看了看复数的基础知识就过来学了)

啊好吧还是有的

向量

同时具有大小和方向的量

在几何中通常用带有箭头的线段表示

圆的弧度制

等于半径长的圆弧所对的圆心角叫做1弧度的角,用符号rad表示,读作弧度。用弧度作单位来度量角的制度叫做弧度制

公式:

1=π180rad1∘=π180rad

180=πrad180∘=πrad

平行四边形定则

 

平行四边形定则:AB+AD=AC

系数表示法

A(x)表示一个n1次多项式

A(x)=ni=0aixi

例如:A(3)=2+3x+x2

利用这种方法计算多项式乘法复杂度为O(n2)

(第一个多项式中每个系数都需要与第二个多项式的每个系数相乘)

点值表示法

nn互不相同的xx带入多项式,会得到nn个不同的取值yy

则该多项式被这nn个点(x1,y1),(x2,y2),,(xn,yn))唯一确定

其中yi=n1j=0ajxji

例如:上面的例子用点值表示法可以为(0,2),(1,6),(2,12)

利用这种方法计算多项式乘法的时间复杂度仍然为O(n^2)

(选点O(n),每次计算O(n)

复数

定义

a,ba,b为实数,i2=1i2=−1,形如a+bia+bi的数叫复数,其中ii被称为虚数单位,复数域是目前已知最大的域

在复平面中,xx代表实数,yy轴(除原点外的点)代表虚数,从原点(0,0)(0,0)到(a,b)(a,b)的向量表示复数a+bia+bi

模长:从原点(0,0)(0,0)到点(a,b)(a,b)的距离,即a2+b2−−−−−−√a2+b2

幅角:假设以逆时针为正方向,从xx轴正半轴到已知向量的转角的有向角叫做幅角

C++的STL提供了复数模板!
头文件:#include <complex>
定义: complex<double> x;
运算:直接使用加减乘除。

代数定义:

(a+bi)(c+di)(a+bi)∗(c+di)
=ac+adi+bci+bdi2=ac+adi+bci+bdi2
=ac+adi+bcibd=ac+adi+bci−bd
=(acbd)+(bc+ad)i

傅里叶要用到的n个复数,不是随机找的,而是——把单位圆(圆心为原点、1为半径的圆)n等分,取这n个点(或点表示的向量)所表示的虚数,即分别以这n个点的横坐标为实部、纵坐标为虚部,所构成的虚数。

从点(1,0)(1,0)开始(显然这个点是我们要取的点之一),逆时针将这n个点从0开始编号,第kk个点对应的虚数记作ωnk(根据复数相乘时模长相乘幅角相加可以看出,ωnk是w1nk次方,所以ω1n被称为n次单位根)。

单位根的性质

为什么要使用单位根作为xx代入

答:因为离散傅里叶变换有着特殊的性质。

一个结论

把多项式A(x)A(x)的离散傅里叶变换结果作为另一个多项式B(x)B(x)的系数,取单位根的倒数即ω0n,ω1n,ω2n,...,ω(n1)nωn0,ωn−1,ωn−2,...,ωn−(n−1)作为xx代入B(x)B(x),得到的每个数再除以n,得到的是A(x)A(x)的各项系数

 

正主出场

快速傅里叶变换

然而知道了这些还不够(除非你想体验TLE的赶脚)

所以我们需要优化。

一.迭代优化(别问,问就是盗图,捂脸.jpg)

 

 

 

结论:每个位置分治后的最终位置为其二进制翻转后得到的位置
这样的话我们可以先把原序列变换好,把每个数放在最终的位置上,再一步一步向上合并
一句话就可以O(n)预处理第iii位最终的位置rev[i]

 

void get_rev(int bit){  //bit表示二进制的位数
    for(int i=0;i<(1<<bit);i++)//我么要对1~2^bit-1中的所有数做长度为bit的二进制翻转
        rev[i]=(rev[i>>1]>>1)|((i&1)<<(bit-1));
}

emm我并没有看懂一开始,然后(我又要开始盗图啦)

我们可以把一个二进制数看成两部分,它的前bit-1位是一部分,它的最后一位是一部分。一个数的二进制翻转就相当于是把它的最后一位当成首位,然后在后面接上它前bit-1为的二进制翻转(有图有真相↓)。而且在这个循环中我们能保证,在计算“i”的二进制翻转之前1~i-1中的所有数的二进制翻转都已经完成。“i”的前bit-1位的数值其实就是[i/2](向下取整)的值,也就是i>>1的值,直接调用i>>1的二进制翻转的结果就相当于调用了“i”的前bit-1位二进制翻转的结果。

 

其实i>>1的翻转与“i”的前bit-1位的翻转是有一点出入的,因为我们的二进制翻转始终以bit位为标准,所以i>>1会比“i”的前bit-1位多出一个前导零,而翻转之后就会多出一个“后缀零”,所以“i”的前bit-1位的翻转要去掉那个“后缀零”,也就是“rev[i>>1]>>1”。

 

 

 

 

 我们只要把末尾乘上2^(bit-1)变成首位,再加上rev[i>>1]>>1就是我们要的答案了。(奇数偶数翻转的差别就是靠或操作来实现的)

二.蝴蝶操作(我又盗图了

高不高大上?

 

 

好的,上述就是蝴蝶操作了(忘了它吧)这是一个很简单的优化

 他可以帮助你原地进行FFT的前一半更新后一半的操作,只需要对代码稍加改动(嗯,就一点点,真的)

优化完结撒花


 


例题:【模板】A*B Problem升级版(FFT快速傅里叶)

这边提供两种代码,一种是自己的,另一种嘛,是某金牌教练写的(我并咩有看懂)

#include<bits/stdc++.h>
using namespace std;
const int N=211314;
const double PI=acos(-1);
typedef complex<double>cp;
char sa[N],sb[N];
long long n=1,lena,lenb,res[N];
cp a[N],b[N],omg[N],inv[N];
void init(){
    for(int i=0;i<n;i++){
        omg[i]=cp(cos(2*PI*i/n),sin(2*PI*i/n));
        inv[i]=conj(omg[i]);//共轭函数 
    }    
}
void fft(cp *a, cp *omg){
    int lim = 0;
    while((1 << lim) < n) lim++;
    for(long long i = 0; i < n; i++){
        long long t = 0;
        for(long long j = 0; j < lim; j++)
            if((i >> j) & 1) t |= (1 << (lim - j - 1));
        if(i < t) swap(a[i], a[t]); 
    }
    for(long long l = 2; l <= n; l *= 2){
        int m = l / 2;
        for(cp *p = a; p != a + n; p += l)
            for(long long i = 0; i < m; i++){
                cp t = omg[n / l * i] * p[i + m];
                p[i + m] = p[i] - t;
                p[i] += t;
            }
        }
}
int main(){
    int l;cin>>l;  
    scanf("%s%s", sa, sb);
    lena = strlen(sa), lenb = strlen(sb);
    while(n < lena + lenb) n *= 2;
    for(long long i = 0; i < lena; i++)
        a[i].real(sa[lena - 1 - i] - '0');
    for(int i = 0; i < lenb; i++)
        b[i].real(sb[lenb - 1 - i] - '0');
    init();
    fft(a, omg);
    fft(b, omg);
    for(long long i = 0; i < n; i++)
        a[i] *= b[i];
    fft(a, inv);//inv是取共轭复数的符号
    for(long long i = 0; i < n; i++){
        res[i] += floor(a[i].real() / n + 0.5);
        res[i + 1] += res[i] / 10;
        res[i] %= 10;
    }
    int t=lena + lenb - 1;
    while(res[t]==0)t--; 
    for(int i =t; i >=0; i--){
        putchar('0'+res[i]);    
    } 
    putchar('\n');
    return 0;

}

 第二个(这是他在我们写高精度a+b的时候让我们做的题(我们才学了不到一个月啊!!!!))

#include <bits/stdc++.h>
using namespace std;
char s[60020];
long long a[14020];
long long b[14020];
long long c[14020];
int T = 9, B = 1;
int main() {
    scanf("%*d");
    for (int i = 0; i < T; i++) {
        B = B * 10;
    }
    scanf("%s", s);
    int la = strlen(s);
    reverse(s, s + la);
    for (int i = 0; i < la; i++) {
        s[i] -= '0';
    }
    for (int i = 0; i * T < la; i++) {
        for (int j = T - 1; j >= 0; j--) {
            a[i] = a[i] * 10 + s[i * T + j];
        }
    }
    la = (la + T - 1) / T;

    scanf("%s", s);
    int lb = strlen(s);
    reverse(s, s + lb);
    for (int i = 0; i < lb; i++) {
        s[i] -= '0';
    }
    for (int i = 0; i * T < lb; i++) {
        for (int j = T - 1; j >= 0; j--) {
            b[i] = b[i] * 10 + s[i * T + j];
        }
    }
    lb = (lb + T - 1) / T;
    int lc = la + lb;
    for (int i = 0; i < la; i++) {
        for (int j = 0; j < lb; j++) {
            c[i + j] += a[i] * b[j];
            c[i + j + 1] += c[i + j] / B;
            c[i + j] %= B;
        }
    }

    while (lc > 1 && c[lc - 1] == 0) {
        lc--;
    }

    printf("%d", (int)c[lc - 1]);
    for (int i = lc - 2; i >= 0; i--) {
        printf("%09d", (int)c[i]);
    }

    printf("\n");
    return 0;
}

 

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

这是一份被卡了两个点的代码(我不想改变我的写法(胡小兔大佬写法比较美观))

#include<bits/stdc++.h>
using namespace std;
const int N=5211314;
const double PI=acos(-1);
typedef complex<double>cp;
long long n=1,lena,lenb;
cp a[N],b[N],omg[N],inv[N];
void init(){
    for(int i=0;i<n;i++){
        omg[i]=cp(cos(2*PI*i/n),sin(2*PI*i/n));
        inv[i]=conj(omg[i]);//共轭函数 
    }    
}
void fft(cp *a, cp *omg){
    int lim = 0;
    while((1 << lim) < n) lim++;
    for(long long i = 0; i < n; i++){
        long long t = 0;
        for(long long j = 0; j < lim; j++)
            if((i >> j) & 1) t |= (1 << (lim - j - 1));
        if(i < t) swap(a[i], a[t]); // i < t 的限制使得每对点只被交换一次(否则交换两次相当于没交换)
    }
    for(long long l = 2; l <= n; l <<= 1){
        int m = l / 2;
        for(cp *p = a; p != a + n; p += l)
            for(long long i = 0; i < m; i++){
                cp t = omg[n / l * i] * p[i + m];
                p[i + m] = p[i] - t;
                p[i] += t;
            }
        }
}

int read(){
    int out = 0,flag = 1; char c = getchar();
    while (c < 48 || c > 57) {if (c == '-') flag = -1; c = getchar();}
    while (c >= 48 && c <= 57) {out = (out << 3) + (out << 1) + c - '0'; c = getchar();}
    return out * flag;
}
int main(){ 
    lena=read();lenb=read();
    for(n=1;n<=lena+lenb;n<<=1);
    for(int i=0;i<=lena;i++)a[i]=read();
    for(int i=0;i<=lenb;i++)b[i]=read();
    init();
    fft(a,omg);
    fft(b,omg);
    for(int i=0;i<=n;i++)a[i]=a[i]*b[i];
    fft(a,inv);
    for(int i=0;i<=lena+lenb;i++)printf("%d ",(int)(a[i].real()/n+0.5));
    return 0;
} 

这是一份过了的代码

#include<bits/stdc++.h>
#define N 2621450
#define pi acos(-1)
using namespace std;
typedef complex<double> E;
int n,m,l,r[N];
E a[N],b[N];
void FFT(E *a,int f){
    for(int i=0;i<n;i++)if(i<r[i])swap(a[i],a[r[i]]);
    for(int i=1;i<n;i<<=1){
        E wn(cos(pi/i),f*sin(pi/i));
        for(int p=i<<1,j=0;j<n;j+=p){
            E w(1,0);
            for(int k=0;k<i;k++,w*=wn){
                E x=a[j+k],y=w*a[j+k+i];
                a[j+k]=x+y;a[j+k+i]=x-y;
            }
        }
    }
}
inline int read(){
    int f=1,x=0;char ch;
    do{ch=getchar();if(ch=='-')f=-1;}while(ch<'0'||ch>'9');
    do{x=x*10+ch-'0';ch=getchar();}while(ch>='0'&&ch<='9');
    return f*x;
}
int main(){
    n=read();m=read();
    for(int i=0;i<=n;i++)a[i]=read();
    for(int i=0;i<=m;i++)b[i]=read();
    m+=n;for(n=1;n<=m;n<<=1)l++;
    for(int i=0;i<n;i++)r[i]=(r[i>>1]>>1)|((i&1)<<(l-1));
    FFT(a,1);FFT(b,1);
    for(int i=0;i<=n;i++)a[i]=a[i]*b[i];
    FFT(a,-1);
    for(int i=0;i<=m;i++)printf("%d ",(int)(a[i].real()/n+0.5));
}

 完结。

posted @ 2019-10-21 20:59  wilxx  阅读(249)  评论(0编辑  收藏  举报