FFT

最近刚学了FFT,给我这个蒟蒻的脑细胞干废了,写篇笔记浅浅记录一下

先讲一下卷积,卷积就是求\(\int_{-\infty}^{\infty} f(\tau)g(x-\tau)d\tau\),这玩意的用处有一个是多项式乘法\(\sum_{i+j=x}^{n+m} a_ib_j(i\leq n,j\leq m)\),别的就比如说高斯模糊什么的

FFT,快速傅里叶变换,是一种快速求卷积的方式

意义

两个数的竖式计算乘法,本质上就是在求他们的卷积,然后根据十进制进位,而FFT可以在\(O(nlogn)\)的时间内求卷积

不用卷积解释的话,那你可以理解为将两个函数转成点值表示法,\(O(n)\)相乘,然后转回乘积

FFT可以将一个函数转成点值表示法

点值表示法

正常我们表达都是\(f(x)=\sum_{i=0}^{n}a_ix^i\),就和两个点只能划一条线一样,一个\(n\)次多项式由\(n+1\)个点唯一确定,都说到这个了,那我挖个拉格朗日插值法的坑回头讲,就是\(f(x)=(x_0,f(x_0)),(x_1,f(x_1))...(x_n,f(x_n))\)

点值表示法意义下的乘法是\(O(n)\)
对于

\[f(x)=(x_0,f(x_0)),(x_1,f(x_1))...(x_n,f(x_n))
\]

\[g(x)=(x_0,g(x_0)),(x_1,g(x_1))...(x_n,g(x_n))
\]

乘积的点值表示法是

\[f\cdot g=(x_0,f(x_0)\cdot g(x_0)),(x_1,f(x_1)\cdot g(x_1))...(x_n,f(x_n)\cdot g(x_n))
\]

复数

复数肯定都会,就不讲了

蝴蝶变换

在复数平面上的单位圆上的点都是可以取的,因为易见它们的若干次方可以为1,读者自证不难,这里不证

当满足这个条件时,对于我们的计算,时空复杂度优化,码量以及IFFT大有裨益

假设\(n=8\),我们取上面等分的\(8\)个点,逆时针从1开始,标到7

记编号为\(k\)的点代表的复数值为\(\omega_n^k\),易得\((\omega_n^1)^k=\omega_n^k\)

众所周知,\(e^{i\theta}=cos\theta+isin\theta\)并且\(e^{i\pi}+1=0\)

所以易见

\[\omega_n^k=cos\frac{k}{n}2\pi+isin\frac{k}{n}2\pi
\]

还有几条

\[\omega_n^k=\omega_{2n}^{2k}
\]

\[\omega_n^{k+\frac{n}{2}}=-\omega_n^k
\]

\[\omega_n^0=\omega_n^n=1
\]

DFT是代入这些值的变换,是\(O(n^2)\)

但DFT可以分治来搞,这就是FFT

DFT很好写,就不贴代码了,把这些值直接带入多项式计算就行了

FFT

\[A(x)=\sum_{i=0}^{n-1}a_ix^i=a_0+a_1x+...+a_{n-1}x^{n-1}
\]

\(A(x)\)下标的奇偶性把\(A(x)\)分成两半,右边再提一个\(x\)

\[A(x)=(a_0+a_2x^2+...+a_{n-2}x^{n-2})+(a_1x+a_3x^3+...+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})
\]

两边非常相似,于是再设两个多项式\(A_1(x)\)\(A_2(x)\),令

\[A_1(x)=a_0+a_2x+...+a_{n-2}x^{\frac{n}{2}-1}
\]

\[A_2(x)=a_1+a_3x+...+a_{n-1}x^{\frac{n}{2}-1}
\]

比较明显得出

\[A(x)=A_1(x^2)+xA_2(x^2)
\]

\(k \leq \frac{n}{2}\),把\(\omega_n^k\)带入,接下来几步变换要多想想单位根的性质

\[A(\omega_n^k)=A_1(\omega_n^{2k})+\omega_n^kA_2(\omega_n^{2k})
\]

\[=A_1(\omega_\frac{n}{2}^k)+\omega_n^kA_2(\omega_\frac{n}{2}^k)
\]

对于\(A(\omega_n^{k+\frac{n}{2}})\)

\[A(\omega_n^{k+\frac{n}{2}})=A_1(\omega_n^{2k+n})+\omega_n^{k+\frac{n}{2}}A_2(\omega_n^{2k+n})
\]

\[=A_1(\omega_n^{2k})-\omega_n^kA_2(\omega_n^{2k})
\]

\[=A_1(\omega_\frac{n}{2}^k)-\omega_n^kA_2(\omega_\frac{n}{2}^k)
\]

\(A(\omega_n^k)\)\(A(\omega_n^{k+\frac{n}{2}})\)后面只有符号不同

就是只要已知\(A_1(\omega_\frac{n}{2}^k)\)\(A_2(\omega_\frac{n}{2}^k)\),就可以求\(A(\omega_n^k)\)\(A(\omega_n^{k+\frac{n}{2}})\)

现在我们就可以递归分治来搞FFT了

每一次回溯时只扫当前前面一半的序列,即可得出后面一半序列的答案

\(n==1\)时就一个常数项,直接\(return\)

\(O(nlognloglogn)\)

优化-迭代版FFT

  • 每个位置分治后的最终位置为其二进制翻转后得到的位置

读者自证不难,这里不证

这样的话我们可以先把原序列变换好,把每个数放在最终的位置上,再一步一步向上合并

一句话就是可以\(O(n)\)预处理第\(i\)位最终的位置\(rev[i]\)

\(O(nlogn)\)

IFFT

  • 一个多项式在分治的过程中乘上单位根的共轭复数,分治完的每一项除以\(n\)即为原多项式的每一项系数

证明

\((y_0,y_1,y_2,...,y_{n-1})\)\(A(x)=a_0+a_1x+...+a_{n-1}x^{n-1}\)做完FFT的样子,设\(B(x)=y_0+y_1x+...+y_{n-1}x^{n-1}\),把它们的倒数\(\omega_n^0,\omega_n^{-1},...,\omega_n^{1-n}\)带入做FFT,得到\((z_0,z_1,...,z_{n-1})\)

\[z_k=\sum_{i=0}^{n-1}y_i(\omega_n^{-k})^i
\]

\[=\sum_{i=0}^{n-1}(\sum_{j=0}^{n-1}a_j(\omega_n^i)^j)(\omega_n^{-k})^i
\]

\[=\sum_{j=0}^{n-1}a_j(\sum_{i=0}^{n-1}(\omega_n^i)^{j-k})
\]

\(j=k\)时,易知

\[\sum_{i=0}^{n-1}(\omega_n^i)^{j-k}=n
\]

否则,通过等比数列求和可知

\[\sum_{i=0}^{n-1}(\omega_n^i)^{j-k}=\frac{(\omega_n^{j-k})^n-1}{\omega_n^{j-k}-1}=\frac{(\omega_n^n)^{j-k}-1}{\omega_n^{j-k}-1}=\frac{1-1}{\omega_n^{j-k}-1}=0
\]

所以

\[z_k=na_k
\]

\[a_k=\frac{z_k}{n}
\]

证毕Q.E.D

代码

学会了FFT之后就可以自己手搓板子啦

注意:FFT的\(n\)只能是\(2^k\)

void fft(cp *a,int inv){//a是要改变的数组,inv是区分FFT和IFFT的
        for(int i=0;i<n;i++) rev[i]=(rev[i>>1]>>1)|((i&1)<<(k-1));//二进制翻转
	for(int i=0;i<n;i++) if(i<rev[i]) swap(a[i],a[rev[i]]);//防止交换两次,就是没交换
	for(int mid=1;mid<n;mid<<=1){//二分长度
		cp temp(cos(pi/mid),inv*sin(pi/mid));//这个就是单位根,遍历每一个omega的
		for(int i=0;i<n;i+=(mid<<1)){
			cp omega(1,0);//初始元
			for(int j=0;j<mid;j++,omega*=temp){
				cp x=a[i+j],y=omega*a[i+j+mid];
				a[i+j]=x+y,a[i+j+mid]=x-y;//这个就是蝴蝶变换什么的
			}
		}
	}
	if(inv==-1) for(int i=0;i<n;i++) a[i]/=n;//IFFT
}

来实践一下

实践

A*B Problem

两个整数\(a\)\(b\)可以看做两个多项式\(x=10\)带入的结果,可以把每一位看成系数,\(a=a_na_{n-1}...a_1a_0\)可以看成多项式\(a=a_nx^n+a_{n-1}x^{n-1}...+a_1x+a_0\)\(x=10\)时的数,看到乘法就是这两个多项式的乘积带回\(x=10\),所以,开卷!!!

正常的运算卷积速度,或者说乘法,是\(O(n^2)\)的,即使DFT和IDFT也是\(O(n^2)\)

Karatsuba乘法的时间复杂度是\(O(n^{log3})≈O(n^{1.585})\),但Karatsuba是可以压位的,实现的好甚至可以吊打不压位的fft

关于NTT,实际上就是利用一些特殊的质数(例如\(998244353\),这些质数的特征都是可以可以表示 \(q\times 2^k + 1\)的形式)进行膜意义下的运算代替浮点复数运算来保证精度,原理是质数原根和复数单位根在DFT运算中具有相同的性质

这时候就可以用FFT啦!主要是因为我不会NTT,回头会了再发一篇,诶不对,我好像又挖了一个坑

在2019年之前,FFT的时间复杂度是\(O(nlognloglogn)\),虽然\(loglogn\)这项系数极小,但是直到2019年才去除掉,变成\(O(n logn)\)

#include<bits/stdc++.h>
using namespace std;
#define int long long
#define cp complex<double>
const double pi=acos(-1);
const int N=2097152+114514;
int len1,len2,n=1,k=0;
string a1,b1;
cp a[N],b[N]; 
int ans[N],rev[N];
void fft(cp *a,int inv){
	for(int i=0;i<n;i++) if(i<rev[i]) swap(a[i],a[rev[i]]);
	for(int mid=1;mid<n;mid<<=1){
		cp temp(cos(pi/mid),inv*sin(pi/mid));
		for(int i=0;i<n;i+=(mid<<1)){
			cp omega(1,0);
			for(int j=0;j<mid;j++,omega*=temp){
				cp x=a[i+j],y=omega*a[i+j+mid];
				a[i+j]=x+y,a[i+j+mid]=x-y;
			}
		}
	}
	if(inv==-1) for(int i=0;i<n;i++) a[i]/=n;
}
main(){
	cin>>a1>>b1;
	len1=a1.length(),len2=b1.length();
	for(int i=0;i<len1;i++) a[i]=(double)a1[len1-i-1]-48;
	for(int i=0;i<len2;i++) b[i]=(double)b1[len2-i-1]-48;
	while(n<len1+len2-1) n<<=1,k++;
	for(int i=0;i<n;i++) rev[i]=(rev[i>>1]>>1)|((i&1)<<(k-1));
	fft(a,1),fft(b,1);
	for(int i=0;i<n;i++) a[i]*=b[i];
	fft(a,-1);
	for(int i=0;i<n;i++) ans[i]+=(int)(a[i].real()+0.5),ans[i+1]+=ans[i]/10,ans[i]%=10;
	while(!ans[n]&&n) n--;
	for(int i=n;i>=0;i--) cout<<ans[i];
	return 0;
}

原文链接: https://www.cnblogs.com/liaiyang/p/17017847.html

欢迎关注

微信关注下方公众号,第一时间获取干货硬货;公众号内回复【pdf】免费获取数百本计算机经典书籍

    FFT

原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/309059

非原创文章文中已经注明原地址,如有侵权,联系删除

关注公众号【高性能架构探索】,第一时间获取最新文章

转载文章受原作者版权保护。转载请注明原作者出处!

(0)
上一篇 2023年2月16日 上午10:49
下一篇 2023年2月16日 上午10:50

相关推荐