快速傅里叶变换(fft)
一.复数
1.概念
复数就是形如 的数,其中 是实数,且。
其中实数 和 分别被称为复数的实部和虚部。
2.四则运算
1.加法
2.减法
3.乘法
4.除法
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.几何意义
每一个复数可以由直角坐标系上的点唯一确定,轴称为实轴,轴称为虚轴。
复数乘法有个重要的性质:模长相乘,幅角相加。
1.模长相乘
2.幅角相加
证明 与 相似即可。
首先有:
只需证:
二.复数单位根
1.概念
次单位根是 次幂为 的复数。
的模长为,所以 次单位根的模长也为,即在单位圆上。
因为模长均为,所以在单位圆上,我们只需考虑幅角。
不难想到,幅角为 的复数,都是单位根。
次单位根一共有 个,且等分单位圆。如图便是所有的 次单位根。
类似的, 次单位根记为 ,,....(逆时针标号)
2.性质
1.
2.
3.
4.
5.
6. ( 为偶数)
7.
8.
三.多项式的表示
系数表示法
一个 次多项式一共有 项,每项前面的数字称为系数。
是最常见的一种表示方法,但乘法需要 。
点值表示法
还记得周考卷的已知两点求直线解析式吗?
其实直线可以看做一个 次多项式,我们知道两点即可得到解析式。
那么对于 次多项式,只需要知道 个取值即可唯一确定。
两个点值表示多项式的乘法只需要 。
fft理论
既然点值表示法的乘法复杂度低,那么我们就可以将两个多项式由系数表示转化为点值表示,相乘后再还原为系数表示。
将多项式由系数表示到点值表示叫做离散傅里叶变换(),由点值表示到系数表示叫做逆离散傅里叶变换()。
四.(逆)离散傅里叶变换(DFT/IDFT)
以下的 均为 的幂。
1.离散傅里叶变换
考虑一个 项多项式 。
按奇偶分成两部分:
又有两个 项多项式 。
那么有:
对于 分别代入 , 得到。
然后我们会惊奇的发现,两个式子只有一个符号的区别。
现在就可以进行分治了。
2.逆离散傅里叶变换
不妨记 为多项式 在 的点值表示 ,则有
证明:
根据点值的意义可得:
那么代入
可以看出:
-
当 时,后一个和式的值为
-
当 时,由等比数列得:
所以原式为:
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 ];
}
很显然,递归版本因为常数太大 掉了,考虑优化常数。
注意到 处的 计算了两次,但复数乘法极慢,可以用变量存下来。
但还是过不了,我们需要一种常数更小的写法。
五.快速傅里叶变换
看一下离散傅里叶变换中的下标变化:
把它们的二进制写出来,即
经过观察发现,原数列位置的二进制反转后得到现在的位置。我不会证
然后就可以迭代实现了。
六.拆系数 fft (mtt)
7 次版本
正常的 fft 最大系数为 ,会掉精度,所以考虑减小系数。
不妨设一个阈值 , 将 划分为两部分。
令 。同理得 。
可以看出,当 取 附近时,多项式的最大系数为 。
那么有:
那么可以 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 次版本
首先可以发现, 时虚部都为 。
对于两个点值表达式, ,
同理有:
可以看出, 和 是共轭复数。
并且,我们可以根据 和 求得 和 。
那么将拆分出的 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;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现