FFT 相关

FFT 数组记得开两倍!

前言

update 2025.09.28 学习了 FFT 自己卷自己。

参考博客

快速傅里叶变换(一)FFT

快速傅里叶变换(二)NTT

快速傅里叶变换(三)

超详细傅里叶变换

FFT自己卷自己

FFT

简介

用于求卷积(\(a,b\) 已知):

\[\sum_{i=0}^n a_ib_{n-i}
\]

或者多项式乘法(\(A,B\) 均是多项式且已知):

\[C=A\cdot B
\]

\(A=\sum_{i=0}^{n} a_i x^i\\
B=\sum_{i=0}^{m} b_i x^i\)

可见 \(C\) 是 \(n+m\) 次多项式。

如果我们把卷积的 \(a_i,b_i\) 看成多项式的系数,卷积就变成求:

\[c_n = \sum_{i+j=n} a_i b_j
\]

求卷积或者多项式乘法的时间复杂度是 \(O(n^2)\) 的。使用 FFT 可以做到 \(O(n\log n)\)。

大体思路

设 \(C\) 的项数为 \(n\)(不是次数),若 \(A,B\) 不足 \(n\) 项就补系数 \(0\)。

显然这个过程很难优化,我们从另一个角度去想。

对于一个多项式,求其在 \(x\) 处的值的时间复杂度是 \(O(n)\) 的,我们把这个操作叫做点值(DFT)。

\(n\) 个点可以唯一确定一个 \(n\) 项多项式(即 \(n-1\) 次多项式),证明不显然但是略过我不会,大概感受一下吧。拉格朗日插值法给出了一种 \(O(n^2)\) 的求出这个多项式(即求出它的系数)的方法原理我不会,这个由点值求系数的过程叫做插值(IDFT)。

因此求多项式乘法,可以变成求任意 \(n\) 个点在两个多项式 \(A,B\) 的点值,然后由 \(C(x)=A(x)B(x)\),\(O(n)\) 求出多项式 \(C\) 在这 \(n\) 个点的点值,然后做一次插值求出 \(C\) 的系数。当然这个也是 \(O(n^2)\) 的,但是聪明的傅里叶给出了一种基于单位根的特殊性质的分治方法求 DFT 和 IDFT,成为快速傅里叶变换(FFT)。

就是先求出 \(A,B\) 的点值(DFT),进行点值相乘得到 \(C\) 的点值,然后插值(IDFT)得到 \(C\) 的系数。

单位根

写作 \(\omega_n^k\),读作 \(n\) 次单位根的 \(k\) 次方。

\(\omega_n^k\) 是在复数域上的向量,形如在复平面上分成 \(n\) 等分,其中 \(\omega_n^0=\omega_n^n=1\)。按逆时针分别为 \(\omega_n^0,\omega_n^1\dots \omega_n^{n-1}\)。

后面为了方便,我们会假设 \(n=2^k\)。

有几个重要的性质。

  • \(\omega_n^n=1\) 正确性显然
  • \(\omega_{an}^{ak}=\omega_n^k\) 在平面上想象一下,显然正确
  • \(\omega_n^{k-\frac{n}{2}}=-\omega_n^k\) 相当于把向量转 \(180^。\)。

点值

基于这些性质,我们求 \(A,B\) 在 \(\omega_n^0,\omega_n^1\dots \omega_n^{n-1}\) 处的点值。

以求 \(A(x)\) 为例,我们要求所有 \(A(\omega_n^k),0\le k<n\)。

我们把 \(A\) 按奇偶分为两部分:

\[A_0(x)=a_0+a_2 x+a_4 x^2+\dots a_{n-2} x^{\frac{n-2}{2}}\\
A_1(x)=a_1+a_3x^2+a_5x^4+\dots a_{n-1}x^{\frac{n-2}{2}}\]

因此有:

\[A(x)=A_0(x^2)+xA_1(x^2)
\]

代入 \(x=\omega_n^k\),有:

\[\begin{aligned}
A(\omega_n^k)& =A_0(\omega_n^{2k})+\omega_n^k A_1(\omega_n^{2k})\\
& = A_0(\omega_{\frac{n}{2}}^k)+\omega_n^k A_1(\omega_{\frac{n}{2}}^k)
\end{aligned}
\]

我们先求出 \(k< \frac{n}{2}\) 的 \(A_0,A_1\) 的点值。这个范围是缩小了一半的。然后把它们相加就得到了 \(A\) 在 \(k< \frac{n}{2}\) 的点值。

然后我们求剩下一半的 \(A_1,A_2\) 的点值。仍然设 \(k< \frac{n}{2}\) 发现:

\[\begin{aligned}
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^{k} A_2(\omega_n^{2k})\\
&=A_1(\omega_{\frac{n}{2}}^k)-\omega_n^{k} A_2(\omega_{\frac{n}{2}}^k)
\end{aligned}
\]

因此你发现,这俩十分地相似,因此你求出 \(k< \frac{n}{2}\) 的 \(A_1,A_2\) 的点值之后,可以直接 \(O(n)\) 求出 \(A\) 的 \(n\) 个点值了。然后就这样分治下去,点值时间复杂度为 \(O(n\log n)\)。

因为要一直对 \(n\) 除以 \(2\) 所以令 \(n=2^k\) 意义就在此。(位数不足高位补系数 \(0\))

插值

求插值的过程是类似的,改几个参数就行了。

把求点值的过程写成矩阵。

由 \(A\) 得到 \(dft(A)_i = A(\omega_{n}^i)\)。有

\[dft(A)_i = \sum_{j=0}^{n-1} \omega_{n}^{ij} a_j= \sum_{j=0}^{n-1} p_{i,j} a_j
\]

把 \(p_{i,j}\) 写成矩阵就是:

\[\begin{bmatrix}
1 & 1 & \cdots & 1\\
1 & \omega_{n}^1 & \cdots & \omega_{n}^{n-1}\\
\vdots & \vdots & \ddots & \vdots\\
1 & \omega_{n}^{n-1} & \cdots & \omega_{n}^{(n-1)(n-1)}
\end{bmatrix}
\]

只要把参数改成逆矩阵,其他不变,就可以由 \(dft(A)\) 求出 \(A\) 了。

根据单位根的性质,矩阵的逆恰好是指数取反:

\[\begin{bmatrix}
1 & 1 & \cdots & 1\\
1 & \omega_{n}^{-1} & \cdots & \omega_{n}^{-(n-1)}\\
\vdots & \vdots & \ddots & \vdots\\
1 & \omega_{n}^{-(n-1)} & \cdots & \omega_{n}^{-(n-1)(n-1)}
\end{bmatrix}
\]

改进

code

然后你会发现过不了板子……

因为这个递归的过程常数很大,假设 FFT 的常数本身就大,然后就超时了。

考虑从下往上递推分治的过程。

\[\left(a_{0}, a_{1}, a_{2}, a_{3}, a_{4}, a_{5}, a_{6}, a_{7}\right)\\
\left(a_{0}, a_{2}, a_{4}, a_{6}\right)\left(a_{1}, a_{3}, a_{5}, a_{7}\right)\\
\left(a_{0}, a_{4}\right)\left(a_{2}, a_{6}\right)\left(a_{1}, a_{5}\right)\left(a_{3}, a_{7}\right)\\
\left(a_{0}\right)\left(a_{4}\right)\left(a_{2}\right)\left(a_{6}\right)\left(a_{1}\right)\left(a_{5}\right)\left(a_{3}\right)\left(a_{7}\right)
\]

然后你惊奇地发现最下层的顺序是 \(000,100,010,110,001,101,011,111\),刚好是 \(0\sim 7\) 的二进制的 reverse。

然后就有了如下板子:

Code

struct fushu {
double x,y;
fushu (double _x=0,double _y=0):x(_x),y(_y){}
}a[N],b[N];
fushu operator + (fushu a,fushu b) { return {a.x+b.x,a.y+b.y}; }
fushu operator - (fushu a,fushu b) { return {a.x-b.x,a.y-b.y}; }
fushu operator * (fushu a,fushu b) { return {a.x*b.x-a.y*b.y,a.x*b.y+a.y*b.x}; }
int len;
const double pi=acos(-1.0);
int re[N];
void FFT(fushu *c,int type) {
rep(i,0,(1<<len)-1) {
if(i<re[i]) swap(c[i],c[re[i]]);
}
for(int k=1;k<(1<<len);k<<=1) {
fushu wn(cos(pi/k),type*sin(pi/k));
for(int r=k<<1,j=0;j<(1<<len);j+=r) {
fushu w(1,0);
for(int i=0;i<k;i++,w=w*wn) {
fushu x=c[j+i],y=w*c[j+k+i];
c[j+i]=x+y;
c[j+k+i]=x-y;
}
}
}
}
int n,m;
int main(){
sf("%d%d",&n,&m);
rep(i,0,n) sf("%lf",&a[i].x);
rep(i,0,m) sf("%lf",&b[i].x);
while((1<<len)<=n+m) len++;
rep(i,0,(1<<len)-1) {
re[i]=(re[i>>1]>>1)|((i&1)<<(len-1));
}
FFT(a,1),FFT(b,1);
rep(i,0,(1<<len)-1) a[i]=a[i]*b[i];
FFT(a,-1);
rep(i,0,n+m) pf("%d ",(int)(a[i].x/(1<<len)+0.5));
}

算法缺陷

由于复数域是用浮点数计算的,所以会存在掉精度问题。如果答案是对一个特别的指数取模,如著名的 \(998244353\),可以使用原根代替单位根计算,在剩余系里计算而不是在复数域计算。详见 NTT。

NTT

阶和原根

欧拉定理:若 \(\gcd(a,n)=1\),则 \(a^{\varphi(n)} \equiv 1(\bmod n)\)。

阶:设 \(m>1\),且 \(\gcd(a,m)=1\),根据欧拉定理一定存在正整数 \(d<m\) 使得 \(a^d \equiv 1(\bmod m)\)。把满足该式子的最小的正整数定义为 \(a\) 对模 \(m\) 的阶(指数),写作 \(\text{ord}_m(a)\)。

阶的一个结论:对于正整数 \(d\),\(a^d \equiv 1 (\bmod m)\) 的一个充要条件是 \(d \mid \text{ord}_m(a)\)。

原根:设 \(m>1\),\(\gcd(a,m)=1\),若 \(\text{ord}_m(a)=\varphi(m)\),则 \(a\) 为 \(m\) 的原根,记做 \(g_m\)。

常见质数 \(998244353\) 的原根是 \(3\)。

定理 1:\(g_m,g_m^2,\dots g_m^{\varphi(m)-1}\) 两两模 \(m\) 不同余且均与 \(m\) 互质。(其实是两条定理)

定理 2:若 \(m\) 是质数,\(g_m,g_m^2,\dots,g_{m}^{m-2}\) 模 \(m\) 的余数恰好形成 \(1 \sim m-1\) 的排列,而 \(g_m^{m-1}\) 模 \(m\) 的余数则是 \(0\)。

由此定理可以发现原根有很多类似单位根的性质。

定理 3:\(m\) 的原根有 \(\varphi(\varphi(m))\) 个。

简介

NTT 的原理是用原根代替单位根。质数 \(p\) 的原根在模 \(p\) 剩余系意义下具有我们利用的单位根的性质的相同性质。因此如果要求的多项式的系数只需要模 \(p\) 意义下的,可以用原根代替单位根。因为原根是整型,所以可以避免精度问题。

Code

const int N=4e6+7,mod=998244353,G=3,invG=332748118;
ll a[N],b[N];
int len;
int re[N];
ll ksm(ll a,ll b=mod-2) {
ll s=1;
while(b) {
if(b&1) s=s*a%mod;
a=a*a%mod;
b>>=1;
}
return s;
}
void NTT(ll *c,int type) {
rep(i,0,(1<<len)-1) {
if(i<re[i]) swap(c[i],c[re[i]]);
}
for(int k=1;k<(1<<len);k<<=1) {
ll wn=ksm(type==1?G:invG,(mod-1)/(k<<1));
for(int r=k<<1,j=0;j<(1<<len);j+=r) {
ll w=1;
for(int i=0;i<k;i++,w=w*wn%mod) {
ll x=c[j+i],y=w*c[j+k+i]%mod;
c[j+i]=(x+y)%mod;
c[j+k+i]=(x-y+mod)%mod;
}
}
}
}
int n,m;
int main(){
#ifdef LOCAL
freopen("in.txt","r",stdin);
freopen("my.out","w",stdout);
#endif
sf("%d%d",&n,&m);
rep(i,0,n) sf("%lld",&a[i]),a[i]%=mod;
rep(i,0,m) sf("%lld",&b[i]),b[i]%=mod;
while((1<<len)<=n+m) len++;
ll inv=ksm(1<<len);
rep(i,0,(1<<len)-1) {
re[i]=(re[i>>1]>>1)|((i&1)<<(len-1));
}
NTT(a,1),NTT(b,1);
rep(i,0,(1<<len)-1) a[i]=a[i]*b[i]%mod;
NTT(a,-1);
rep(i,0,n+m) pf("%lld ",a[i]*inv%mod);
}

分治 FFT

Luogu 模板

形如 \(f_i=\sum_{j=1}^i f_{i-j}g_j\) 的卷积(\(f_0\) 需要初值)。

一般的卷积我们是知道卷起来的两个多项式的系数的,但是这个卷积显然我们只知道 \(g\) 的系数,却不知道 \(f\) 的系数,那么如何卷呢?可以分治 FFT 解决。

它的思想类于 CDQ 分治。

要求 \(f_{1 \sim n}\),先求 \(f_{1 \sim mid}\),然后计算左边对右边的贡献,然后再递归计算右边。

左边对右边的贡献,即算 \(\forall i \in [1,n],\sum_{j=1}^{mid} f_{j}g_{i-j}\),只需要对 \(f,g\) 做一次 \(O(n\log n)\) 的 FTT。

因为这个卷积不是标准卷积(指 \(j\) 不是枚举到 \(i\)),因此我们可以给 \(j>mid\) 的 \(f_j\) 当做 \(0\) 来做卷积。变成求 \(\sum_{j=1}^i f_j g_{i-j}\)。

然后我们再求 \(f_{mid+1\sim r}\),因为左边已经对右边贡献过了,所以我们递归右边的时候,计算贡献不需要再带上左边。

整理一下,现在我们要计算 \(f_{[l,r]}\):

  1. 先递归算好 \(f_{[l,mid]}\)。
  2. 算 \(f_{[l,mid]}\) 对 \(f_{[mid+1,r]}\) 的贡献。\(f_i \gets \sum_{j=l}^{mid} f_j g_{i-j}\)。
  3. 递归计算 \(f_{[mid+1,r]}\)。

\(f_i \gets \sum_{j=l}^{mid} f_j g_{i-j}\),这是一个标准卷积,FFT 时间复杂度是 \(O(len \log len)\)。

像这样分治下去,直到长度为 \(1\) 时,不用卷积了,直接返回值。分治一共 \(\log\) 层,每一层每个区间都要做 \(O(len\log len)\) 的 FTT,总时间复杂度为 \(O(n\log^2 n)\)。

FFT 自己卷自己

前言

感觉它的思想有点像这题 https://vjudge.net/contest/744791#problem/E。我就说吧,不会的东西先放着,说不定之后那一次恰好就理解了它的思想呢!所以我要学多少次 SAM 才能学会?

介绍

形如

\[f_n = \sum_{i=1}^{n-1} f_i f_{n-i}
\]

参考分治 FFT 的过程分治。

现在我们要计算 \(f_{[l,r]}\):

  1. 先递归算好 \(f_{[l,mid]}\)。
  2. 算 \(f_{[l,mid]}\) 对 \(f_{[mid+1,r]}\) 的贡献。
    • 若 \(l \neq 1\),可以和分治 FFT 一样做。即 \(f_i \gets \sum_{j=l}^{mid} f_j f_{i-j}\),\(f_{i-j}\) 一定已知。
    • 若 \(l = 1\),\(f_{[1,mid]} \times f_{[1,mid]}\) 的贡献是可以算的,但是 \(f_{[1,mid]} \times f_{[mid+1,r]}\) 的贡献目前没法算。这部分贡献可以在递归 \([mid+1,r]\) 的时候再算。
  3. 递归计算 \(f_{[mid+1,r]}\)。

总结整个算法流程,目前分治区间为 \([l,r]\):

  1. 若 \(l=1\):

    • solve(l,mid)
    • 将 \(f_{[1,mid]} \times f_{[1,mid]}\) 贡献至 \(f_{[mid+1,r]}\)。
    • solve(mid+1,r)
  2. 若 \(l \neq 1\):
    • solve(l,mid)
    • 将 \(f_{[l,mid]} \times f_{[1,r-l]}\) 贡献至 \(f_{[mid+1,r]}\)。
    • 将 \(f_{[1,r-l]} \times f_{[l,mid]}\) 贡献至 \(f_{[mid+1,r]}\)。
    • solve(mid+1,r)

时间复杂度 \(O(n \log^2 n)\)。

FFT 相关的更多相关文章

  1. 多项式FFT相关模板

    自己码了一个模板...有点辛苦...常数十分大,小心使用 #include <iostream> #include <stdio.h> #include <math.h& ...

  2. 快速傅里叶变换(FFT)相关内容汇总

    (原稿:https://paste.ubuntu.com/p/yJNsn3xPt8/) 快速傅里叶变换,是求两个多项式卷积的算法,其时间复杂度为$O(n\log n)$,优于普通卷积求法,且根据有关证 ...

  3. fft相关的复习

    任意长度卷积 CZT 就是一波推导 \[ \begin{aligned} b_i &= \sum_{j=0}^{n-1} \omega^{ij}a_j \\ &= \sum_{j=0} ...

  4. 用于ARM上的FFT与IFFT源代码(C语言,不依赖特定平台)(转)

    源:用于ARM上的FFT与IFFT源代码(C语言,不依赖特定平台) 代码在2011年全国电子大赛结束后(2011年9月3日)发布,多个版本,注释详细. /*********************** ...

  5. STM32F4使用FPU+DSP库进行FFT运算的测试过程一

    测试环境:单片机:STM32F407ZGT6   IDE:Keil5.20.0.0  固件库版本:STM32F4xx_DSP_StdPeriph_Lib_V1.4.0 第一部分:使用源码文件的方式,使 ...

  6. 洛谷.3803.[模板]多项式乘法(FFT)

    题目链接:洛谷.LOJ. FFT相关:快速傅里叶变换(FFT)详解.FFT总结.从多项式乘法到快速傅里叶变换. 5.4 又看了一遍,这个也不错. 2019.3.7 叕看了一遍,推荐这个. #inclu ...

  7. DSP5509项目之用FFT识别钢琴音调(4)之麦克风输入和Line in输入

    1. 麦克风输入需要修改的内容,之前的版本是LINE IN的输入.实现功能,检测麦克风的输入,并且同时在耳机里面播放. #include <csl.h> #include <csl_ ...

  8. DSP5509项目之用FFT识别钢琴音调(1)

    1. 其实这个项目难点在于,能不能采集到高质量的钢琴音调.先看一下FFT相关程序. FFT 并不是一种新的变换,它是离散傅立叶变换(DFT)的一种快速算法.由于我们在计算 DFT 时一次复数乘法需用四 ...

  9. 基于HAL库的STM32的DSP库详解(附FFT应用)

    1 . 建立工程,生成代码时选择包含所有库.   2. 打开 option for target 选择 Target 标签,在code generatio中,将floating point hardw ...

  10. 一步一步教你实现iOS音频频谱动画(二)

    如果你想先看看最终效果再决定看不看文章 -> bilibili 示例代码下载 第一篇:一步一步教你实现iOS音频频谱动画(一) 本文是系列文章中的第二篇,上篇讲述了音频播放和频谱数据计算,本篇讲 ...

随机推荐

  1. notebook 开启 有限元学习

    简介 jupyter-notebook --ip 0.0.0.0 开启 sudo docker run -ti -p 127.0.0.1:8888:8888 -v $(pwd):/home/fenic ...

  2. AppLink+WMS,实现仓储管理一体化

    WMS像全能的库管员,可以在线还原真实仓库,让企业进行科学化.条理化.俯视化的仓库管理. 随着移动互联网和物流行业的快速发展,如何提高仓储管理的效率和准确性成为了企业关注的焦点.在这个背景下,结合Ap ...

  3. 一个java空指针异常的解决过程

    背景 上一篇讲了我们从另外一个部门迁移了一个线上系统回来,迁回来是为啥呢,因为这个好几年没新需求的系统,突然有新需求要开发,然后我就开发呗,其实就是在某个服务里加点表,然后提供个查询接口给app.这个 ...

  4. SciTech-BigDataAIML-LangChain 完整指南:使用大语言模型构建强大的应用程序 + Cursor AI Editor(用AI驱动的IDE与代码编辑器) + ComfyUI(视频音频领域的AI Workflow LLM) + Cursor

    可以先在github上研究一下: livetalking, 数字人的直播系统: metahuman-stream 已经有的成功案例:https://www.bilibili.com/video/BV1 ...

  5. SciTech-Mathmatics-等比数列n项加总公式 + 三角函数Trigonometric Identities you must remember: 需要记住的三角函数

    $\large S_n = a_1 \cdot \frac{1-q^n}{1-q}=\frac{a_1-a_n \cdot q}{1-q},\ when\ q \neq 1 \ and\ a_1 \n ...

  6. Oracle Exadata存储节点主动替换磁盘最佳实践

    我们的文章会在微信公众号IT民工的龙马人生和博客网站( www.htz.pw )同步更新 ,欢迎关注收藏,也欢迎大家转载,但是请在文章开始地方标注文章出处,谢谢! 由于博客中有大量代码,通过页面浏览效 ...

  7. 全能文件格式转换AllToAll

    文件格式转换 https://www.alltoall.net/

  8. unity 组合键 搓招 流星蝴蝶剑

    转载:https://www.zhihu.com/question/36951135/answer/69880133 bilibili  的up实现:https://www.bilibili.com/ ...

  9. DP 优化 学习笔记

    0 参考资料 DP 优化方法大杂烩 II. -- Alex_Wei 算法竞赛进阶指南 -- LYD XMOJ 倾情讲解 -- BYD 1 斜率优化 1.1 斜率优化简介 如果一类最优化问题的 dp 式 ...

  10. nacos适配达梦数据库

    点击这里直接看作者开源的成品 遇到问题可在评论区给我留言 加微信:pky86676022 有偿提供技术支持(请说明来意) 从2.2.0版本开始,nacos可通过SPI机制注入多数据源实现插件,并在引入 ...