2023.2.9【数学】快速傅里叶变换
2023.2.9【模板】快速傅里叶变换(FFT)
好多天没写博客了qwq
题目描述
给定一个 n 次多项式 F(x),和一个 m 次多项式 G(x)。
请求出 F(x) 和 G(x) 的卷积。
朴素(正常)思路
枚举计算的每一位,交叉相乘加起来计算答案,时间复杂度O(
原地爆炸
这个时候就需要用到NOIP不考的FFT了
前置芝士
有关复数详见lxw (tqqqqqqqqqqqqqqqqqql%%%%%%%%%%%%%)巨佬的博客
复数 - Ricky2007 - 博客园 (cnblogs.com)
*泰勒展开
泰勒中值定理:若
泰勒展开经常用于将一个难以计算的函数逼近(或等效)于一个多项式,为了消掉
,我们经常将函数在0处展开,即
*欧拉公式
证明:将三个柿子泰勒展开可以得到:
我们(神奇地)发现:
这难道不和
为了将
特殊地,代入
这不就是那个被很多人当作壁纸的公式吗,我们来思考它的实际作用
在一个笛卡尔坐标系中,一个从源点指向
我们将Y坐标指定为复数域,X坐标指定为实数域,发现x + yi可以表示一个点
(
这样以来,再使用欧拉公式转化,就得到了:
一个单项就可以表达一个坐标,省去许多计算麻烦
*离散傅里叶级数
对于序列
其中
可以看作在极坐标内,一个单位角度是
(B站神犇up@3Blue1Brown的示意图)
*离散傅里叶变换 DFT
对于序列
这个过程叫做离散傅里叶变换,反之,我们称对于一个构造好的序列,求原序列的过程叫做的逆傅里叶变换(IFT)
*定理:一个序列 的傅里叶变换后的新序列 其 次DF值恰为原序列 的 倍
证明:设
代入
提出求和符号:
Key1.
Key2.
为什么
设其为
通过*****,我们就可以构造出一种计算多项式乘法的新方法:
1.将F(x)和G(x)进行傅里叶变换
2.将两者点乘(
3.将所得序列进行
怎样加快计算过程?
考虑多项式
提出奇数
偶数
可推得合并式
代入:
可以发现:
其中通过
合并过程图解:例:
小技巧:怎样快速将序列拆成奇项和偶项?
考虑到我们要将偶项也拆成其中的奇和偶、再拆为奇和偶...我们可以发现,每次拆项都是按照数字二进制的第K位排序,最后的顺序就是将序列按照最低位为第一关键字,次低位为第二关键字...来排序(或者说是拆分数组),一个数的排名就是它二进制拆分的倒序,于是我们在程序开始预处理每个数字的二进制翻转:
(其中
在
Code
#include<bits/stdc++.h>
using namespace std;
const int N = 3e6 + 5;
const double PI = acos(-1.0);
int n,m,rev[N],tt = 1,tw = 0;
struct Complex{
double r,c;
}a[N],b[N];
Complex operator +(Complex x,Complex y)
{
Complex z;
z.r = x.r + y.r;
z.c = x.c + y.c;
return z;
}
Complex operator -(Complex x,Complex y)
{
Complex z;
z.r = x.r - y.r;
z.c = x.c - y.c;
return z;
}
Complex operator *(Complex x,Complex y)
{
Complex z;
z.r = x.r * y.r - x.c * y.c;
z.c = x.c * y.r + x.r * y.c;
return z;
}
inline int read()
{
int s = 0,w = 1;
char k = getchar();
while(k > '9' || k < '0')
{
if(k == '-') w = -w;
k = getchar();
}
while(k <= '9' && k >= '0')
{
s = s * 10 + k - '0';
k = getchar();
}
return s * w;
}
inline void FFT(Complex *x,int len,int type)
{
for(int i = 0;i < tt;i++) if(i < rev[i]) swap(x[i],x[rev[i]]);
for(int mid = 1;mid < tt;mid <<= 1)
{
Complex omega;omega.r = cos(PI / mid);omega.c = type * sin(PI / mid);
for(int j = 0,R = mid << 1;j < tt;j += R)
{
Complex now;now.c = 0;now.r = 1;
for(int k = j;k < j + mid;k++,now = now * omega)
{
Complex X = x[k],Y = x[k + mid];
x[k] = X + now * Y;
x[k + mid] = X - now * Y;
}
}
}
}
int main()
{
n = read();m = read();
for(int i = 0;i <= n;i++) a[i].r = read();
for(int i = 0;i <= m;i++) b[i].r = read();
while(tt <= n + m) tt <<= 1,tw++;
rev[0] = 0;
for(int i = 1;i < tt;i++) rev[i] = (rev[i >> 1] >> 1) | ((i & 1) << (tw - 1));
FFT(a,n,1);FFT(b,m,1);
for(int i = 0;i <= tt;i++) a[i] = a[i] * b[i];
FFT(a,n,-1);
for(int i = 0;i <= n + m;i++)
printf("%d ",(int)(a[i].r / tt + 0.5));
return 0;
}
买一送一环节
我们知道,FFT中暴力计算魔改
考虑到在
考虑到答案中的每个数不会很大,我们将答案模一个素数,就可以利用它的原根
(在
于是我们每一步将
(这就是为什么需要
Code
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 3e6 + 5,MOD = 998244353,g = 3,gi = 332748118;
inline ll ksm(ll base,ll pts)
{
ll ret = 1;
for(;pts > 0;pts >>= 1,base = base * base % MOD)
if(pts & 1)
ret = ret * base % MOD;
return ret;
}
int rev[N];
ll a[N],b[N],n,m,tt = 1,tw = 0;
inline void NTT(ll *x,ll type)
{
for(int i = 0;i < tt;i++) if(i < rev[i]) swap(x[i],x[rev[i]]);
for(int mid = 1,t = 0;mid < tt;mid <<= 1,t++)
{
ll dom = ksm((type == 1) ? g : gi,(MOD - 1) >> (t + 1));
for(int j = 0,R = mid << 1;j < tt;j += R)
{
ll w = 1;
for(int k = j;k < j + mid;k++,w = w * dom % MOD)
{
ll X = x[k],Y = x[k + mid] * w % MOD;
x[k] = (X + Y) % MOD;
x[k + mid] = (X - Y + MOD) % MOD;
}
}
}
}
int main()
{
cin>>n>>m;
for(int i = 0;i <= n;i++) cin>>a[i];
for(int i = 0;i <= m;i++) cin>>b[i];
while(tt <= n + m) tt <<= 1,tw++;
for(int i = 0;i < tt;i++) rev[i] = (rev[i >> 1] >> 1) | ((i & 1) << (tw - 1));
NTT(a,1);NTT(b,1);
for(int i = 0;i <= tt;i++) a[i] = a[i] * b[i] % MOD;
NTT(a,-1);
ll inv = ksm(tt,MOD - 2);
for(int i = 0;i <= n + m;i++) cout<<a[i] * inv % MOD<<" ";
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!