快速傅里叶变换(fft)

一.复数

1.概念

复数就是形如 a+bi 的数,其中 a,b 是实数,且b0,i2=1

其中实数 abi 分别被称为复数的实部和虚部。

2.四则运算

1.加法 (a+bi)+(c+di)=(a+c)+(b+d)i

2.减法 (a+bi)(c+di)=(ac)+(bd)i

3.乘法 (a+bi)×(c+di)=(acbd)+(ad+bc)i

4.除法 (a+bi)÷(c+di)=(a+bi)(cdi)(c+di)(cdi)=acbdc2d2+bcadc2d2i

struct Complex {
    double x , y;

    Complex operator + ( const Complex &a ) const {
        return { x + a.x , y + a.y };
    }
    Complex operator - ( const Complex &a ) const {
        return { x - a.x , y - a.y };
    }
    Complex operator * ( const Complex &a ) const {
        return { x * a.x - y * a.y , x * a.y + y * a.x };
    }
    Complex operator / ( const Complex &a ) const {
        return { ( x * a.x - y * a.y ) / ( a.x * a.x - a.y * a.y ) , ( y * a.x - x * a.y ) / ( a.x * a.x - a.y * a.y ) };
    }
};

3.几何意义

每一个复数可以由直角坐标系上的点唯一确定,x轴称为实轴,y轴称为虚轴。

复数乘法有个重要的性质:模长相乘,幅角相加

1.模长相乘

l1=a2+b2l2=c2+d2l3=(acbd)2+(ad+bc)2=(ac)2+(bd)22abcd+(ad)2+(bc)2+2abcd=(ac)2+(bd)2+(ad)2+(bc)2=a2(c2+d2)+b2(c2+d2)=(a2+b2)(c2+d2)=(a2+b2)(c2+d2)=l1l2

2.幅角相加

证明 AOPCOB 相似即可。

首先有:

AOCO=OPOB=1l2

只需证:

APCB=1l2APCB=(a1)2+b2(acbdc)2+(ad+bcd)2=a2+b22a+1((ac)2+(bd)2+c22abcd2ac2+2bcd)+((ad)2+(bc)2+d2+2abcd2ad22bcd)=a2+b22a+1(ac)2+(bd)2+(ad)2+(bc)2+c2+d22ac22ad2=a2+b22a+1(c2+d2)(a2+b22a+1)=1c2+d2=1l2

二.复数单位根

1.概念

n 次单位根是 n 次幂为 1 的复数。

1的模长为1,所以 n 次单位根的模长也为1,即在单位圆上。

因为模长均为1,所以在单位圆上,我们只需考虑幅角。

不难想到,幅角为 2πnα 的复数,都是单位根。

n 次单位根一共有 n 个,且等分单位圆。如图便是所有的 6 次单位根。

类似的,n 次单位根记为 ωn0ωn1ωn2....ωnn1(逆时针标号)

2.性质

1.ωn0=1

2.ωnk=(ωn1)k

3.ωni×ωnj=ωni+j

4.ωnk=ωnk%n

5.ωpnpk=ωnk

6.ωnk+n2=ωnk (n 为偶数)

7.ωnk=(cos2kπn,sin2kπn)

8.i=0n1ωni=0

i=0n1ωni=ωn0(1ωnn1)1ωn1=0

三.多项式的表示

系数表示法

一个 n1 次多项式一共有 n 项,每项前面的数字称为系数。

f(x)=f0+f1x+f2x2+...+fn1xn1

是最常见的一种表示方法,但乘法需要 Θ(n2)

点值表示法

还记得周考卷的已知两点求直线解析式吗?

其实直线可以看做一个 1 次多项式,我们知道两点即可得到解析式。

那么对于 n 次多项式,只需要知道 n+1 个取值即可唯一确定。

两个点值表示多项式的乘法只需要 Θ(n)

fft理论

既然点值表示法的乘法复杂度低,那么我们就可以将两个多项式由系数表示转化为点值表示,相乘后再还原为系数表示。

将多项式由系数表示到点值表示叫做离散傅里叶变换(dft),由点值表示到系数表示叫做逆离散傅里叶变换(idft)。

四.(逆)离散傅里叶变换(DFT/IDFT)

以下的 n 均为 2 的幂。

1.离散傅里叶变换

考虑一个 n 项多项式 f(x)

f(x)=f0+f1x+f2x2+...+fn1xn1

按奇偶分成两部分:

f(x)=(f0+f2x2....+fn2xn2)+(f1+f3x3+...+fn1xn1)

又有两个 n/2 项多项式 fl,fr

fl(x)=f0+f2x+...+fn2xn/21

fl(x)=f1+f3x+...+fn1xn/21

那么有: f(x)=fl(x2)+xfr(x2)

对于 k(k<n2) 分别代入 ωnkωnk+n/2 得到。

f(ωnk)=fl((ωnk)2)+ωnkfr((ωnk)2)=fl(ωn/2k)+ωnkfr(ωn/2k)

f(ωnk+n/2)=fl((ωnk+n/2)2)+ωnk+n/2fr((ωnk+n/2)2)=fl((ωnk+n/2)2)ωnkfr((ωnk+n/2)2)=fl(ωn2k+n)ωnkfr(ωn2k+n)=fl(ωn2k)ωnkfr(ωn2k)=fl(ωn/2k)ωnkfr(ωn/2k)

然后我们会惊奇的发现,两个式子只有一个符号的区别。

现在就可以进行分治了。

2.逆离散傅里叶变换

不妨记 Dk 为多项式 fωnk 的点值表示 ,则有

nfk=i=0n1(ωnk)iDi

证明:

根据点值的意义可得:

Dk=i=0n1fi(ωnk)i

那么代入 Di

nfi=i=0n1(ωnk)iDi=i=0n1(ωnk)ij=0n1(ωni)jfj=i=0n1j=0n1(ωni)jkfj=j=0n1fji=0n1(ωnjk)i

可以看出:

  • j=k 时,后一个和式的值为 n

  • jk 时,由等比数列得:

=j=0n1fji=0n11ωn(jk)n1ωnjk=j=0n1fji=0n1111ωnjk=0

所以原式为: nfk

3.实现

void fft( Complex *F , int len , int op ) {
    if( len == 1 ) return;
    
    Complex *Fl = F , *Fr = F + len / 2;
    for( int i = 0 ; i < len ; i ++ ) tmp[ i ] = F[ i ];
    for( int i = 0 ; i < len / 2 ; i ++ )
        Fl[ i ] = tmp[ i << 1 ] , Fr[ i ] = tmp[ i << 1 | 1 ];
    
    fft( Fl , len / 2 , op );
    fft( Fr , len / 2 , op );

    Complex w = { cos( 2 * pi / len ) , op * sin( 2 * pi / len ) } , buf = { 1 , 0 };
    for( int i = 0 ; i < len / 2 ; i ++ ) {
        tmp[ i ] = Fl[ i ] + buf * Fr[ i ]; //*
        tmp[ i + len / 2 ] = Fl[ i ] - buf * Fr[ i ];
        buf = buf * w;
    }
    for( int i = 0 ; i < len ; i ++ ) F[ i ] = tmp[ i ];
}

很显然,递归版本因为常数太大 T 掉了,考虑优化常数。

注意到 () 处的 bufFr[i] 计算了两次,但复数乘法极慢,可以用变量存下来。

但还是过不了,我们需要一种常数更小的写法。

五.快速傅里叶变换

看一下离散傅里叶变换中的下标变化:

0,1,2,3,4,5,6,7

0,2,4,6,1,3,5,7

0,4,2,6,1,5,3,7

把它们的二进制写出来,即

{0(000)0(000)1(001)4(100)2(010)2(010)3(011)6(110)4(100)1(001)5(101)5(101)6(110)3(011)7(111)7(111)

经过观察发现,原数列位置的二进制反转后得到现在的位置。我不会证

然后就可以迭代实现了。

六.拆系数 fft (mtt)

7 次版本

正常的 fft 最大系数为 np2 ,会掉精度,所以考虑减小系数。

不妨设一个阈值 base , 将 f,g 划分为两部分。

[xn]A0=[xn]f(x)base,[xn]A1(x)=[xn]f(x)modbase。同理得 B0,B1

可以看出,当 basep 附近时,多项式的最大系数为 np

那么有:

{f(x)=baseA0(x)+A1(x)g(x)=baseB0(x)+B1(x)

f(x)×g(x)=(baseA0(x)+A1(x))×(baseB0(x)+B1(x))=base2A0(x)B0(x)+base(A0(x)B1(x)+A1(x)B0(x))+A1(x)B1(x)

那么可以 4 次 dft + 3 次 idft 计算。

可能有人(比如我)会问为什么不把点值加起来最后一次 idft 回去,但这样值域又会变很大,idft 时精度会炸。

#include <cmath>
#include <cstdio>
#include <vector>
#include <iostream>
using namespace std;
#define pi acos( -1 )
#define double long double

int Mod;
int Add( int x , int y ) { x += y; return x >= Mod ? x - Mod : x; }
int Sub( int x , int y ) { x -= y; return x < 0 ? x + Mod : x; }

struct Complex {
	double x , y;
	Complex(){ x = y = 0; }
	Complex( double X , double Y ) { x = X , y = Y; }
	Complex operator + ( const Complex &a ) const { return Complex( x + a.x , y + a.y ); }
	Complex operator - ( const Complex &a ) const { return Complex( x - a.x , y - a.y ); }
	Complex operator * ( const Complex &a ) const { return Complex( x * a.x - y * a.y , x * a.y + y * a.x ); }
	Complex operator / ( const double &a ) const { return Complex( x / a , y / a ); }
};

#define Poly vector< Complex >
#define Polyint vector< int >
#define len( x ) ( (int)x.size() )

const int MAXN = 4e6;
int lim , rev[ MAXN + 5 ];
void fft( Poly &f , int op ) {
    for( int i = 0 ; i < lim ; i ++ ) if( i < rev[ i ] ) swap( f[ i ] , f[ rev[ i ] ] );
    for( int len = 2 ; len <= lim ; len <<= 1 ) {
    	Complex w( cos( (double)2 * pi / len ) , op * sin( (double)2 * pi / len ) );
		for( int l = 0 ; l < lim ; l += len ) {
			Complex wk( 1 , 0 );
			for( int i = l ; i < l + len / 2 ; i ++ , wk = wk * w ) {
				Complex t = wk * f[ i + len / 2 ];
				f[ i + len / 2 ] = f[ i ] - t; f[ i ] = f[ i ] + t; 
			}
		} 
	}
	if( op == -1 ) for( int i = 0 ; i < lim ; i ++ ) f[ i ] = f[ i ] / lim;
}

Poly A0 , A1 , B0 , B1;
Polyint mtt( Polyint f , Polyint g , int Mod ) {
	int n = len( f ) + len( g ) - 1; for( lim = 1 ; lim < n ; lim <<= 1 );
	for( int i = 0 ; i < lim ; i ++ ) rev[ i ] = ( rev[ i >> 1 ] >> 1 ) | ( i & 1 ? lim >> 1 : 0 );
	
	int base = 1 , lg = 0; for( ; 1ll * base * base < Mod ; base <<= 1 , lg ++ );
	A0.resize( lim ); A1.resize( lim );
	B0.resize( lim ); B1.resize( lim );
	for( int i = 0 ; i < len( f ) ; i ++ ) A0[ i ].x = f[ i ] / base , A1[ i ].x = f[ i ] % base; 
	for( int i = 0 ; i < len( g ) ; i ++ ) B0[ i ].x = g[ i ] / base , B1[ i ].x = g[ i ] % base;
	
	fft( A0 , 1 ); fft( A1 , 1 ); fft( B0 , 1 ); fft( B1 , 1 );
	
	Poly tmp; tmp.resize( lim );
	Polyint h; h.resize( lim );
	for( int i = 0 ; i < lim ; i ++ ) tmp[ i ] = A0[ i ] * B0[ i ];
	fft( tmp , -1 );
	for( int i = 0 ; i < lim ; i ++ ) h[ i ] = Add( h[ i ] , (long long)( tmp[ i ].x + 0.49 ) % Mod * base * base % Mod % Mod );
	
	for( int i = 0 ; i < lim ; i ++ ) tmp[ i ] = A0[ i ] * B1[ i ] + A1[ i ] * B0[ i ];
	fft( tmp , -1 );
	for( int i = 0 ; i < lim ; i ++ ) h[ i ] = Add( h[ i ] , (long long)( tmp[ i ].x + 0.49 ) % Mod * base % Mod );
	
	for( int i = 0 ; i < lim ; i ++ ) tmp[ i ] = A1[ i ] * B1[ i ];
	fft( tmp , -1 );
	for( int i = 0 ; i < lim ; i ++ ) h[ i ] = Add( h[ i ] , (long long)( tmp[ i ].x + 0.49 ) % Mod ); 
	
	h.resize( n );
	return h;
}

int n , m;
Polyint f , g; Polyint h;

int main( ) {
	scanf("%d %d %d",&n,&m,&Mod);
	f.resize( n + 1 ); g.resize( m + 1 );
	for( int i = 0 , x ; i <= n ; i ++ ) scanf("%d",&x) , f[ i ] = x % Mod;
	for( int i = 0 , x ; i <= m ; i ++ ) scanf("%d",&x) , g[ i ] = x % Mod;
	h = mtt( f , g , Mod );
	for( int i = 0 ; i < len( h ) ; i ++ ) printf("%d ", h[ i ] );
	return 0;
}

4 次版本

首先可以发现, dft 时虚部都为 0

对于两个点值表达式, F(k)=A(ωnk)+iB(ωnk) , G(k)=A(ωnk)iB(ωnk)

F(k)=j=0n1ajωnkj+ij=0n1bjωnkj=j=0n1(aj+ibj)ωnkj=j=0n1(aj+ibj)(cos(2πkjn)+isin(2πkjn))

同理有:

G(nk)=A(ωnk)+B(ωnk)=j=0n1ajωnkjij=0n1bjωnkj=j=0n1(ajibj)ωnkj=j=0n1(ajibj)(cos(2πkjn)isin(2πkjn))

可以看出,F(k)G(nk) 是共轭复数。

并且,我们可以根据 F(k)G(nk) 求得 A(x)B(x)

那么将拆分出的 4 个多项式分别放到实部和虚部,可以用 2 次 dft 求得点值。

同理我们可以将两个点值序列分别放进点值的实部和虚部,idft 后实部和虚部的系数便为两个多项式的系数表示。

那么只需 2 次 idft 即可完成上述操作。

#include <cmath>
#include <cstdio>
#include <vector>
#include <iostream>
using namespace std;
#define pi acos( -1 )
#define double long double

int Mod;
int Add( int x , int y ) { x += y; return x >= Mod ? x - Mod : x; }
int Sub( int x , int y ) { x -= y; return x < 0 ? x + Mod : x; }

struct Complex {
	double x , y;
	Complex(){ x = y = 0; }
	Complex( double X , double Y ) { x = X , y = Y; }
	Complex operator + ( const Complex &a ) const { return Complex( x + a.x , y + a.y ); }
	Complex operator - ( const Complex &a ) const { return Complex( x - a.x , y - a.y ); }
	Complex operator * ( const Complex &a ) const { return Complex( x * a.x - y * a.y , x * a.y + y * a.x ); }
	Complex operator / ( const double &a ) const { return Complex( x / a , y / a ); }
	Complex operator * ( const double &a ) const { return Complex( x * a , y * a ); }
	Complex Conj() { return Complex( x , -y ); }
};

#define Poly vector< Complex >
#define Polyint vector< int >
#define len( x ) ( (int)x.size() )

const int MAXN = 4e6;
int lim , rev[ MAXN + 5 ];
void fft( Poly &f , int op ) {
    for( int i = 0 ; i < lim ; i ++ ) if( i < rev[ i ] ) swap( f[ i ] , f[ rev[ i ] ] );
    for( int len = 2 ; len <= lim ; len <<= 1 ) {
    	Complex w( cos( (double)2 * pi / len ) , op * sin( (double)2 * pi / len ) );
		for( int l = 0 ; l < lim ; l += len ) {
			Complex wk( 1 , 0 );
			for( int i = l ; i < l + len / 2 ; i ++ , wk = wk * w ) {
				Complex t = wk * f[ i + len / 2 ];
				f[ i + len / 2 ] = f[ i ] - t; f[ i ] = f[ i ] + t; 
			}
		} 
	}
	if( op == -1 ) for( int i = 0 ; i < lim ; i ++ ) f[ i ] = f[ i ] / lim;
}

void mtt( Poly f , Poly &A , Poly &B ) {
	fft( f , 1 ); A.resize( lim ); B.resize( lim );
	Poly g; g.resize( lim );
	g[ 0 ] = f[ 0 ].Conj(); for( int i = 1 ; i < lim ; i ++ ) g[ lim - i ] = f[ i ].Conj();
	for( int i = 0 ; i < lim ; i ++ ) {
		A[ i ] = ( f[ i ] + g[ i ] ) * 0.5;
		B[ i ] = ( f[ i ] - g[ i ] ) * 0.5 * Complex( 0 , -1 );
	}
}
Poly F , G , A0 , A1 , B0 , B1;
Polyint mtt( Polyint f , Polyint g , int Mod ) {
	int n = len( f ) + len( g ) - 1; for( lim = 1 ; lim < n ; lim <<= 1 );
	for( int i = 0 ; i < lim ; i ++ ) rev[ i ] = ( rev[ i >> 1 ] >> 1 ) | ( i & 1 ? lim >> 1 : 0 );
	
	int base = 1 , lg = 0; for( ; 1ll * base * base < Mod ; base <<= 1 , lg ++ );
	F.resize( lim ); G.resize( lim );
	for( int i = 0 ; i < len( f ) ; i ++ ) F[ i ].x = f[ i ] >> lg , F[ i ].y = f[ i ] & ( base - 1 ); 
	for( int i = 0 ; i < len( g ) ; i ++ ) G[ i ].x = g[ i ] >> lg , G[ i ].y = g[ i ] & ( base - 1 );
	mtt( F , A0 , A1 ); mtt( G , B0 , B1 );

	Poly tmp1 , tmp2;
	tmp1.resize( lim ); tmp2.resize( lim );
	for( int i = 0 ; i < lim ; i ++ ) {
		tmp1[ i ] = A0[ i ] * B0[ i ] + A1[ i ] * B1[ i ] * Complex( 0 , 1 );
		tmp2[ i ] = A0[ i ] * B1[ i ] + A1[ i ] * B0[ i ] * Complex( 0 , 1 );
	}
	fft( tmp1 , -1 ); fft( tmp2 , -1 );

	Polyint h; h.resize( lim );
	for( int i = 0 ; i < lim ; i ++ ) {
		h[ i ] = Add( h[ i ] , (long long)( tmp1[ i ].x + 0.49 ) % Mod * base * base % Mod % Mod );
		h[ i ] = Add( h[ i ] , (long long)( tmp2[ i ].x + tmp2[ i ].y + 0.49 ) % Mod * base % Mod );
		h[ i ] = Add( h[ i ] , (long long)( tmp1[ i ].y + 0.49 ) % Mod ); 
	}
	h.resize( n ); return h;
}

int n , m;
Polyint f , g; Polyint h;

int main( ) {
	scanf("%d %d %d",&n,&m,&Mod);
	f.resize( n + 1 ); g.resize( m + 1 );
	for( int i = 0 , x ; i <= n ; i ++ ) scanf("%d",&x) , f[ i ] = x % Mod;
	for( int i = 0 , x ; i <= m ; i ++ ) scanf("%d",&x) , g[ i ] = x % Mod;
	h = mtt( f , g , Mod );
	for( int i = 0 ; i < len( h ) ; i ++ ) printf("%d ", h[ i ] );
	return 0;
}

参考资料

洛谷日报

posted @   chihik  阅读(483)  评论(0编辑  收藏  举报
编辑推荐:
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示