FFT学习及简单应用(一点点详细)

什么是FFT

既然打开了这篇博客,大家肯定都已经对FFT(Fast Fourier Transformation)有一点点了解了吧
FFT即为快速傅里叶变换,可以快速求卷积(当然不止这一些应用,但是我不会

系数表示法与点值表示法

我们通常表示一个\(n-1\)次多项式是利用系数表示法like this:\(f(x)=a_0+a_1x+a_2x^2+...+a_{n-1}x^{n-1}\)
点值表示法即为将多项式用坐标系上的若干个点表示
我们对这个多项式代入不同的值{\(x_1,x_2,...,x_n\)}
我们就可以得到\(n\)个点\((x_1,f(x_1)),(x_2,f(x_2)),...,(x_n,f(x_n))\)
实际上只要保证代入的\(n\)个数互不相同,那么这\(n\)个点就对应了唯一的\(f(x)\)(类似\(n\)元一次方程组?)

DFT与IDFT

DFT

我们若将一个多项式强行转为点值表示法则时间复杂度为\(O(n^2)\)
自然有人表示:太慢啦,就不能快点吗!
于是我们开始思考,如果我们对于\((x_1,f(x_1)),(x_2,f(x_2)),...,(x_n,f(x_n))\)取一系列特殊的有关系的值,是不是能加速呢?
当然可以,要不然就不会有这篇博客了
在DFT中,我们可以将复平面中的单位园n等分,然后每一等分的顶点\(\omega _n^i\)作为\(x_i\)\(\omega _n^i\)就是单位根
(如图为\(8\)等分)

其中\(\omega _n^i=\cos (\frac{{2\pi i}}{n}) + i\sin (\frac{{2\pi i}}{n})\)
当然DFT只是让\((x_1,f(x_1)),(x_2,f(x_2)),...,(x_n,f(x_n))\)变得特殊了一些,并没有优化复杂度的效果(要不然哪来的FFT)

IDFT

IDFT(Inverse Discrete Fourier Transform)顾名思义就是DFT的逆变换(当然我不会,这个东西,可以会,但没必要)

关于单位根

首先,显然\(\omega _{2n}^{2i}=\omega _n^i\)(结合图像可得)\((n=2^k)\)
\(\omega _n^{i+\frac{n}{2}}=-\omega _n^i\)为什么呢,因为这两个单位根在坐标中终点关于原点对称
最后一个没有多大用的东西,但是后面会用到\(\omega _n^0=\omega _n^n=0\),这个显然吧,因为这个顶点是重合的,且虚部为0

FFT

递归解法

FFT(Fast Fourier Transformation)快速傅里叶变换,有了上面这些东西,我们就可以考虑对原多项式进行变形
\(f(x)=a_0+a_1x+a_2x^2+...+a_{n-1}x^{n-1}\)
\(=(a_0+a_2x^2+...+a_{n-2}x^{n-2})+x(a_1+a_3x^2+...+a_{n-1}x^{n-2})\)
是不是发现这两个东西很像啊
我们设\(f_1(x)=(a_0+a_2x+...+a_{n-2}x^{\frac{n}{2}-1}),f_2(x)=(a_1+a_3x+a_{n-1}x^{\frac{n}{2}-1})\)
所以\(f(x)=f_1(x^2)+xf_2(x^2)\)
我们发现,\(f_1(x),f_2(x)\)\(f(x)\)的形式一样诶!
也就是说我们现在这个FFT已经可以用递归解决了!
然而,递归自带sb大常数,时间上并不能通过
于是,厉害的人们又发现了新的操作,蝴蝶变换

蝴蝶变换

首先我们令\(i<\frac{n}{2}\)
\(f(\omega _n^i)=f_1({(\omega _n^i)}^2)+\omega _n^if_2({(\omega _n^i)}^2)\)
\(=f_1(\omega _n^{2i})+\omega _n^if_2(\omega _n^{2i})\)
\(=f_1(\omega _{\frac{n}{2}}^i)+\omega _n^if_2(\omega _{\frac{n}{2}}^i)\)
\(f(\omega _n^{i+\frac{n}{2}})=f_1({(\omega _n^i)}^2\omega _n^n)+\omega _n^{i+\frac{n}{2}}f_2({(\omega _n^i)}^2\omega _n^n)\)
\(=f_1(\omega _n^{2i})-\omega _n^if_2(\omega _n^{2i})\)
\(=f_1(\omega _{\frac{n}{2}}^i)-\omega _n^if_2(\omega _{\frac{n}{2}}^i)\)
是不是很神奇,是不是!
就差了一个符号,是不是很蝴蝶啊(亲爱的,你慢慢飞,小心...)
有了蝴蝶变换,我们发现我们只要知道\(f_1(\omega _{\frac{n}{2}}^i),f_2(\omega _{\frac{n}{2}}^i)\)就可以求出\(n\)等分下两个不同单位根带入后的点值了,是不是很厉害!

rev数组

但是我们又发现了一个新的问题,每次我们合并的时候,都是把要求的序列分为奇数位和偶数位做
可是我们原数组并不符合这个条件,直接做的话肯定死
我们可以研究一下每个数字和它的二进制表示
以8个数为例:

但是我们合并的顺序应该长成这样:
(读者可以自己模拟一下,段长每次为\(2^k\),然后合并)
我们发现,其实如果把每个数的二进制位反过来一下,就变成顺序的了!

把二进制反过来其实也不复杂,就一行代码

rev[i]=(rev[i>>1]>>1)|((i&1)<<(bit-1))

其实这段代码的意义大概是把除了最低位之外的位用i>>1翻转,然后再将最低位移到最高位
也就是说,做FFT的时候只要将系数重新排列,然后每段进行合并更新就好啦(这段看一下代码就会很清楚)

IFFT

IFFT(Inverse Discrete Fourier Transform)快速傅立叶逆变换
实际上我们的傅立叶变换可以写成矩阵的形式:
\(\left[ {\begin{array}{*{20}{c}} {{a_0}}\\ {{a_1}}\\ {{a_2}}\\ {...}\\ {{a_{n - 1}}} \end{array}} \right]\left[ {\begin{array}{*{20}{c}} 1&1&1&{...}&1\\ 1&{\omega _n^1}&{\omega _n^2}&{...}&{\omega _n^{n - 1}}\\ 1&{\omega _n^2}&{\omega _n^4}&{...}&{\omega _n^{2n - 2}}\\ {...}&{...}&{...}&{...}&{...}\\ 1&{\omega _n^{n - 1}}&{\omega _n^{2n - 2}}&{...}&{\omega _n^{(n - 1) \times (n - 1)}} \end{array}} \right] = \left[ {\begin{array}{*{20}{c}} {f(\omega _n^0)}\\ {f(\omega _n^1)}\\ {f(\omega _n^2)}\\ {...}\\ {f(\omega _n^{n - 1})} \end{array}} \right]\)
当然,看到这个形式,我们第一个想法就是对
\(\left[ {\begin{array}{*{20}{c}} 1&1&1&{...}&1\\ 1&{\omega _n^1}&{\omega _n^2}&{...}&{\omega _n^{n - 1}}\\ 1&{\omega _n^2}&{\omega _n^4}&{...}&{\omega _n^{2n - 2}}\\ {...}&{...}&{...}&{...}&{...}\\ 1&{\omega _n^{n - 1}}&{\omega _n^{2n - 2}}&{...}&{\omega _n^{(n - 1) \times (n - 1)}} \end{array}} \right]\)
这个矩阵求逆,但是这样的话,复杂度就吃不消了,我们好不容易把\(O(n^2)\)的DFT降为\(O(nlogn)\)的FFT,结果逆不回去,这不是非常尴尬吗
但是我们发现,我们对这个矩阵求一下逆,可以得到这个形式是固定的
\(\left[ {\begin{array}{*{20}{c}} {\frac{1}{n}}&{\frac{1}{n}}&{\frac{1}{n}}&{...}&{\frac{1}{n}}\\ {\frac{1}{n}}&{\frac{1}{n}\omega _n^{ - 1}}&{\frac{1}{n}\omega _n^{ - 2}}&{...}&{\frac{1}{n}\omega _n^{1 - n}}\\ {\frac{1}{n}}&{\frac{1}{n}\omega _n^{ - 2}}&{\frac{1}{n}\omega _n^{ - 4}}&{...}&{\frac{1}{n}\omega _n^{2 - 2n}}\\ {...}&{...}&{...}&{...}&{...}\\ {\frac{1}{n}}&{\frac{1}{n}\omega _n^{1 - n}}&{\frac{1}{n}\omega _n^{2 - 2n}}&{...}&{\frac{1}{n}\omega _n^{ - (n - 1) \times (n - 1)}} \end{array}} \right]\)
然后我们可以把\(\frac{1}{n}\)提出来,那么我们就可以先利用快速傅立叶变换,做出系数的\(n\)倍,然后再除掉
坑终于填完啦

FFT_Code

这是一个高精乘的模板

#include<cstdio>
#include<cstring>
#include<cmath>
#include<algorithm>
using namespace std;
char s[60050],s1[60050];
struct r{
    double real,image;
}a[131090],b[131090];
double pi=acos(-1);
r operator +(r a,r b){return (r){a.real+b.real,b.image+a.image};}
r operator -(r a,r b){return (r){a.real-b.real,a.image-b.image};}
r operator *(r a,r b){return (r){a.real*b.real-b.image*a.image,a.real*b.image+a.image*b.real};}
int ans[131090],rev[131090],bit,n,l;
void fft(r *a,int n,int op){
    for (int i=0;i<n;i++)if (i<rev[i])std::swap(a[rev[i]],a[i]);
    for (int i=1;i<n;i<<=1){
        r wn=(r){cos(pi/i),op*sin(pi/i)};//将单位圆分成i*2个部分
        for (int j=0;j<n;j+=i<<1){
            r wnk=(r){1,0};
            for (int k=j;k<i+j;k++,wnk=wnk*wn){
                r x=a[k],y=wnk*a[i+k];
                a[k]=x+y;
                a[k+i]=x-y;//这里很蝴蝶
            }
        }
    }
    if (op==-1)for (int i=0;i<n;i++)a[i].real/=n;
}
int main(){
    scanf("%d",&l);
    scanf("%s",s);
    scanf("%s",s1);
    int n=2;
    for (bit=1;(1<<bit)<(l<<1)-1;bit++)n<<=1;
    for (int i=0;i<l;i++)a[i]=(r){(double)(s[l-i-1]-'0'),0};
    for (int i=0;i<l;i++)b[i]=(r){(double)(s1[l-i-1]-'0'),0};
    for (int i=0;i<n;i++)rev[i]=(rev[i>>1]>>1)|((i&1)<<(bit-1));//处理rev数组
    fft(a,n,1);
    fft(b,n,1);
    for (int i=0;i<n;i++)a[i]=a[i]*b[i];
    fft(a,n,-1); //IFFT
    for (int i=0;i<n;i++){
        ans[i]+=(a[i].real+0.5);
        ans[i+1]=ans[i]/10;
        ans[i]%=10;
    }
    int i;
    for (i=n;i>=0&&!ans[i];i--);
    if (i<0)putchar('0');
    else for (;i>=0;i--)printf("%d",ans[i]);
}

猜你喜欢

转载自www.cnblogs.com/Cool-Angel/p/10451693.html