FFT & NTT - 快速傅里叶变换 & 快速数论变换

更好的阅读体验戳此进入

(建议您从上方链接进入我的个人网站查看此 Blog,在 Luogu 中图片会被墙掉,部分 Markdown 也会失效)

写在前面

该博客仅为记录学习中的笔记及个人理解,不保证正确性,同时欢迎各位纠正。

图片没有放在图床上,全都是丢在自己的网站上,带宽较低可能加载较慢。

目的

FFT (Fast Fourier Transform) 是为了为快速求出两个多项式的卷积,也就是 C(x)=A(x)B(x) ,或者表达为

c(i)=j=0ia(j)×b(ij)

( a(i),b(i),c(i) 为多项式系数 A(x),B(x),C(x) 为多项式 )

前置知识

原根

详细定义可参考 知乎OI-WIKI,简而言之就是,对于模 m 意义下的原根 a,有 a1,a2,,aφ(m)modm 的值各不相同,φ(m) 表示 欧拉函数

单位根

对于 ϵn=1(ϵ1),称 ϵn 次单位根,其可以为模意义下的或复数意义下的。

模意义下的(原根)

对于模 m 意义下的, m 为质数,令原根为 g 则有 gcd(g,m)=1,此时若满足 nm1 则有 ϵ=gm1n

更通俗一点的描述,也就是对于所有 dm2ϵdmodm 各不相同。

证明:

(gm1n)n1(modm)gm11(modm)

费马小定理 可知显然成立

复数意义下的

对于复数意义下的,则可将一单位圆 n 等分,并取该 n 个点表示的复数,从 x 轴,也就是从 (1,0) 开始取,逆时针从 0 开始对这些复数进行编号,对于第 k 个复数记作 ϵnk,则对于幅角非零且最小的复数 ϵn1,由复数相乘时模长相乘幅角相加可知一定有 (ϵn1)k=ϵnk,则称 ϵn1n 次单位根。

很多地方可能用 ω 来表示单位根,也就是本文中的 ϵ,仅为表示方式的区别而已。

单位根性质

对于复数意义下的 n 次单位根有如下式子

(ϵk)2=(ϵk+n2)2

证明

(ϵk+n2)2=(ϵk)2×ϵ2=(ϵk)2

单位根求法

复数意义下

由单位根的定义显然可知对于 n 次单位根的 k 次方,即 ωnk,考虑用三角函数表达,有 ωnk=cos(2π÷n×k)+sin(2π÷n×k)i

模意义下的(原根)

因为 NTT 模数的原根一般都很小,只有极少数的质数的原根能达到 20,所以可以直接按照定义,考虑遍历所有 dm2,如果不存在 d1(modm) 则其为原根。

同时还存在一种效率更高的方式,考虑将 m1 质因数分解,对于其每一个质因子 pi ,及一个数 ϵ,判断是否满足 ϵm1pi1(modm) ,满足则 ϵ 为原根。

等比数列求和公式

详细证明

Sn=a11qn1q

正文

单位根反演

对于 n 次单位根 ϵ,显然有如下的, a1=ϵ0=1q=ϵv,的等比数列的求和为

i=0n1ϵvi=1ϵnv1ϵv(ϵv1)=0

又有 n 次单位根性质可知 ϵv=1 时,上式 =1

且又有如下式子

ϵv=1nv

综上则有如下式子

1ni=0n1ϵvi={0nv1nv

此即为单位根反演

推式子

将单位根反演代入原式,令 v=p+qi,则 nv

且令

d(x)={0p+qimodn1p+qimodn

显然有如下式子

c(i)=j=0ia(j)×b(ij)=pqa(p)×b(q)×d(x)=p=0n1q=0n1a(p)×b(q)×1nk=0n1ϵk×(p+qi)=p=0n1a(p)q=0n1b(q)×1nk=0n1ϵkpϵkqϵki=p=0n1a(p)ϵkpq=0n1b(q)ϵkq×1nk=0n1ϵki
n×c(i)k=0n1ϵki=p=0n1a(p)ϵkpq=0n1b(q)ϵkq

 

观察最后两个式子,可以发现如下两个式子

c(i)=p=0n1a(p)ϵkpq=0n1b(q)ϵkq×1nk=0n1ϵki
n×c(i)k=0n1ϵki=p=0n1a(p)ϵkpq=0n1b(q)ϵkq

 

考虑令该多项式上一点为 (ϵp,f(p)),多项式第 p 项系数为 g(p),有

f(p)=i=0n1ϵpig(i)g(p)=1ni=0n1ϵpif(i)

证明

//TODO

则可知求 f(p) 的过程即为DFT,求 g(p) 的过程即为IDFT。

由定义显然有

DFT(C,i)=DFT(A,i)×DFT(B,i)

A,B,C 均代表该多项式 )

又有

IDFT(DFT(C))=C

证明

//TODO

则此时多项式 C 可求,但时间复杂度仍然是 O(n2)

继续推式子

对于

f(p)=i=0n1ϵpig(i)

可以考虑 f(p)f(p+2k) 的关系,则此时我们可以假设 n=2k+1

且令

d1(x)={0i0(mod2)1i1(mod2)d2(x)={1i0(mod2)0i1(mod2)

由单位根的性质可以得到以下式子

f(p+2k)=i=0n1(ϵp+2k)ig(i)=i=0n1(ϵp)ig(i)d1(i)+i=0n1(ϵp)iug(i)d2(i)

其中u为一个二次单位根,因为显然当且仅当 i 为奇数时,下式不为 0

i=0n1(ϵp)iug(i)d(i)

i 为奇数时,可以有如下推导

i=0n1(ϵp+2k)ig(i)=i=0n1(ϵp)iϵ2kg(i)

此时显然有

(ϵ2k)2=ϵ2k+1

且我们已知 ϵ2k+1 次单位根,所以显然有

ϵ2k+1=1

所以 ϵ2k 为二次单位根,这里为了方便,我们用 u 代替。

此时可以考虑令

f(p)=i=0n1(ϵp)ig(i)d1(i)f(p+2k)=i=0n1(ϵp)ig(i)d2(i)

所以将幂次除以二后,显然有(此时 ϵ 应为 n2 次单位根)

f(p)=i=0n21(ϵp)ig(i×2)f(p+2k)=i=0n21(ϵp)ig(i×2+1)

再将式子转化为

f(p)=i=0n(ϵp)ig1(i)f(p+2k)=i=0n(ϵp)ig2(i)

此时式子形式便可按相同方法继续递归,直到 n=2 时进行回溯。

Code

优化

显然递归版本的写法虽然更容易理解,但每层都需要开额外的数组,消耗空间很大,时间也较大,虽然可以通过 洛谷模板,但是在后面的题里可能会被卡常,于是便有了如下的优化,即 Cooley - Tukey 算法。

首先观察如下递归过程( 图片来源

FFT_1

通过观察我们即可发现(这真是人类能想出来的吗)对于每一个数的位置,显然是进行了一次二进制的反转,如 1 的位置从 001 变成了 100,那么我们便可以利用这个性质对位置进行反转。

这里提供两种写法

O(nlogn)

类似于模拟的写法,首先判断二进制数的位数,即 size,然后对于每个数按位判断,并将其转移到 tmp 的对应位置,最后通过swap交换位置, i<tmp 的判断是为了使其只会交换一次。

O(n)

这种方法我就不严格地证明了(主要我也不会),就从找规律的角度来研究一下这个线性递推的式子。

举个例子,假设我们有一个二进制数 0101110 我们想要对其进行 Reverse,因为我们要进行递推,所以需要分解为子问题,可以考虑将其右移一位,即变为 0010111,然后 Reverse,变为 1110100,在对比我们想要求得的 0111010,发现前者去掉最后一位,后者忽略第一位是完全相同的,那么将前者右移一位后在考虑最前面一位是 1 还是 0 即可。

对于 Reverse 后合并的过程显然我们可以通过从倒数第二层开始,模拟递归形式的操作,这部分较为显然便不再赘述。

值得注意的一个点是当我们更新数组时,由于非递归写法,可能会对需要用到的变量进行覆盖,所以这时我们显然可以将原数组复制一份,这样的空间时可以接受的,当然更好的做法就是将会被覆盖的那个变量存起来再进行操作,如下。

最后贴上优化后的完整代码

NTT

前面我们已知 FFT 是在复数意义下利用单位复根的性质进行优化,而 NTT 则是在模意义下的,对于模意义下的单位根替代品则为原根,至于证明这里不再赘述,可以在 此处 查看。

而对于如洛谷模板题的这种答案系数较小的,我们可以考虑用 NTT 代替 FFT 以大量减少时间空间消耗,我们只需要找到一个比最大的答案( 9×9×106 )更大且是 NTT质数(或者叫费马质数),如最常见的 998244353 即可,其原根为 3,如何求原根可以在前置知识中找到。

实现过程中只需要根据结论,用原根代替单位根,如将 ωngMOD1size 代替,将原本的共轭复数作为逆元,变为直接用费马小定理,通过快速幂求逆元,这里需要注意在最后需要把原本的 A(i)÷len 变为 A(i)×inv(len)modMOD

Code:

合并DFT优化

这个单独再写一个 Blog 吧,戳此进入

写在后面

写完之后发现似乎依然没有很清晰的弄明白,然后发现有几个Blog写的更清晰易懂

一小时学会快速傅里叶变换(Fast Fourier Transform)

小学生都能看懂的FFT!!!

至于几个TODO等以后再慢慢填坑吧

UPD

update-2022_08_10 初稿

update-2022_08_17 改了一下 latex 在 cnblog 里渲染异常的问题( luogu 里还是炸了,以后再改)

update-2022_08_17 修复 latex 在 luogu 里渲染异常的问题

update-2022_08_22 修复 latex 在 cnblog 里仍然存在的渲染异常问题

update-2022_08_22 添加了递归版程序中的 code

update-2022_08_22 进行一些小优化

update-2022_08_22 添加了非循环写法的讲解与 code

update-2022_08_22 添加了 NTT 的讲解与 code

update-2022_08_22 完善了对模意义下单位根的求法

update-2022_08_23 更改标题

update-2022_08_23 添加几个链接

update-2022_08_25 更新标题和链接