实在不知道标题叫啥了,总不能就 DFT 三个字母吧。是最近把 D F T D F T 的过程自己推了一遍后的浅显理解,以及更快的 F F T F F T 板子。因为转置原理对于实现的优化实在是太多了,所以我也简单学了一下,不过真的啥都不学不懂。
DFT 在干啥
首先我们要知道 D F T D F T 是什么,简单来说,它是一组复数 ⟨ a 0.. n − 1 ⟩ ⟨ a 0.. n − 1 ⟩ 到另一组复数 ⟨ b 0.. n − 1 ⟩ ⟨ b 0.. n − 1 ⟩ 的线性变换。式子长这样,其中 i i 是虚数单位:
b j = n − 1 ∑ k = 0 a k e − i 2 π j k n b j = ∑ k = 0 n − 1 a k e − i 2 π j k n
这个 e − i 2 π j k n e − i 2 π j k n 其实就是 n n 次单位根的 j k j k 次方,ω j k n ω n j k 。可以写出它对应的矩阵:
⎡ ⎢
⎢
⎢
⎢
⎢
⎢ ⎣ ω 0 × 0 n ω 0 × 1 n ω 0 × 2 n ⋯ ω 0 × ( n − 1 ) n ω 1 × 0 n ω 1 × 1 n ω 1 × 2 n ⋯ ω 1 × ( n − 1 ) n ⋮ ⋮ ⋮ ⋱ ⋮ ω ( n − 1 ) × 0 n ω ( n − 1 ) × 1 n ω ( n − 1 ) × 2 n ⋯ ω ( n − 1 ) × ( n − 1 ) n ⎤ ⎥
⎥
⎥
⎥
⎥
⎥ ⎦ [ ω n 0 × 0 ω n 0 × 1 ω n 0 × 2 ⋯ ω n 0 × ( n − 1 ) ω n 1 × 0 ω n 1 × 1 ω n 1 × 2 ⋯ ω n 1 × ( n − 1 ) ⋮ ⋮ ⋮ ⋱ ⋮ ω n ( n − 1 ) × 0 ω n ( n − 1 ) × 1 ω n ( n − 1 ) × 2 ⋯ ω n ( n − 1 ) × ( n − 1 ) ]
也叫范德蒙德矩阵。这个矩阵可以在后面的转置原理里用到,这里暂且按下不表。
接下来就来看看它在干什么吧。考虑两个序列 ⟨ f 0.. n − 1 ⟩ , ⟨ g 0.. n − 1 ⟩ ⟨ f 0.. n − 1 ⟩ , ⟨ g 0.. n − 1 ⟩ 进行 D F T D F T 后进行点乘,本文约定序列 ⟨ f 0.. n − 1 ⟩ ⟨ f 0.. n − 1 ⟩ 进行 D F T D F T 后得到的序列为 ⟨ ^ f 0.. n − 1 ⟩ ⟨ f ^ 0.. n − 1 ⟩ :
^ f k × ^ g k = ( n − 1 ∑ i = 0 f i ω i k n ) ( n − 1 ∑ j = 0 g j ω j k n ) = n − 1 ∑ i = 0 n − 1 ∑ j = 0 f i g j ω ( i + j ) k n = n − 1 ∑ i = 0 n − 1 ∑ j = 0 f i g j ω ( i + j ) k mod n n = n − 1 ∑ i = 0 n − 1 ∑ j = 0 f i g j n − 1 ∑ p = 0 [ ( i + j ) mod n = p ] ω p k n = n − 1 ∑ p = 0 ω p k n n − 1 ∑ i = 0 n − 1 ∑ j = 0 f i g j [ ( i + j ) mod n = p ] f ^ k × g ^ k = ( ∑ i = 0 n − 1 f i ω n i k ) ( ∑ j = 0 n − 1 g j ω n j k ) = ∑ i = 0 n − 1 ∑ j = 0 n − 1 f i g j ω n ( i + j ) k = ∑ i = 0 n − 1 ∑ j = 0 n − 1 f i g j ω n ( i + j ) k mod n = ∑ i = 0 n − 1 ∑ j = 0 n − 1 f i g j ∑ p = 0 n − 1 [ ( i + j ) mod n = p ] ω n p k = ∑ p = 0 n − 1 ω n p k ∑ i = 0 n − 1 ∑ j = 0 n − 1 f i g j [ ( i + j ) mod n = p ]
发现也是 D F T D F T 的形式,考虑设 ^ h k = ^ f k × ^ g k h ^ k = f ^ k × g ^ k ,则可以发现:
h k = n − 1 ∑ i = 0 n − 1 ∑ j = 0 f i g j [ ( i + j ) mod n = k ] h k = ∑ i = 0 n − 1 ∑ j = 0 n − 1 f i g j [ ( i + j ) mod n = k ]
也就是 f i , g j f i , g j 在模 n n 意义下的循环卷积。换句话说,我们把 ^ f , ^ g f ^ , g ^ 点乘就得到了 f , g f , g 循环卷积后的序列 D F T D F T 后的结果。如果我们能找到一个逆 D F T D F T ,或者说 I D F T I D F T 的过程,就可以实现循环卷积的计算了。考虑从 h k h k 的表达式入手:
h k = n − 1 ∑ i = 0 n − 1 ∑ j = 0 f i g j [ ( i + j ) mod n = k ] = 1 n n − 1 ∑ i = 0 n − 1 ∑ j = 0 f i g j n − 1 ∑ p = 0 ω ( i + j − k ) p n = 1 n n − 1 ∑ p = 0 ω − k p n ( n − 1 ∑ i = 0 f i ω i p n ) ( n − 1 ∑ j = 0 g j ω j p n ) = 1 n n − 1 ∑ p = 0 ω − k p n ^ f p × ^ g p = 1 n n − 1 ∑ p = 0 ^ h p ω − k p n = 1 n n − 1 ∑ p = 0 ^ h p ω k ( n − p ) n = 1 n ^ h 0 ω k × 0 n + n − 1 ∑ p = 1 ^ h n − p ω k p n h k = ∑ i = 0 n − 1 ∑ j = 0 n − 1 f i g j [ ( i + j ) mod n = k ] = 1 n ∑ i = 0 n − 1 ∑ j = 0 n − 1 f i g j ∑ p = 0 n − 1 ω n ( i + j − k ) p = 1 n ∑ p = 0 n − 1 ω n − k p ( ∑ i = 0 n − 1 f i ω n i p ) ( ∑ j = 0 n − 1 g j ω n j p ) = 1 n ∑ p = 0 n − 1 ω n − k p f ^ p × g ^ p = 1 n ∑ p = 0 n − 1 h ^ p ω n − k p = 1 n ∑ p = 0 n − 1 h ^ p ω n k ( n − p ) = 1 n h ^ 0 ω n k × 0 + ∑ p = 1 n − 1 h ^ n − p ω n k p
可以发现,I D F T I D F T 的过程既可以通过把 D F T D F T 的单位根取反得到,也可以通过把序列 1 ∼ n − 1 1 ∼ n − 1 处的值反转得到再进行 D F T D F T 得到。
DFT 该咋做
常见的实现是 F F T F F T ,它的限制是要求循环卷积的模数是 2 2 的次幂,通过把这个模数取到大于能得到的最高次幂的方法,我们可以让循环卷积变为普通加法卷积。如果需要实现任意模数的循环卷积,我们需要再推一推式子,即 B l u e s t e i n ′ s a l g o r i t h m B l u e s t e i n ′ s a l g o r i t h m 。我们分别来看。
首先介绍一下 F F T F F T 。它的基本思路是分治,因为 n n 是 2 2 的次幂,所以我们可以每次分成两半来算。考虑设:
f 0 , i = f 2 i , f 1 , i = f 2 i + 1 f 0 , i = f 2 i , f 1 , i = f 2 i + 1
^ f i = n − 1 ∑ j = 0 f j ω i j n = n − 1 ∑ j = 0 ( [ j mod 2 = 0 ] f j ω i j n + [ j mod 2 = 1 ] f j ω i j n ) = n − 1 ∑ j = 0 ( [ j mod 2 = 0 ] f j ω i j / 2 n / 2 + ω i n [ j mod 2 = 1 ] f j ω i ( j − 1 ) / 2 n / 2 ) = ^ f 0 , i + ω i n ^ f 1 , i f ^ i = ∑ j = 0 n − 1 f j ω n i j = ∑ j = 0 n − 1 ( [ j mod 2 = 0 ] f j ω n i j + [ j mod 2 = 1 ] f j ω n i j ) = ∑ j = 0 n − 1 ( [ j mod 2 = 0 ] f j ω n / 2 i j / 2 + ω n i [ j mod 2 = 1 ] f j ω n / 2 i ( j − 1 ) / 2 ) = f ^ 0 , i + ω n i f ^ 1 , i
从而找到一个分治结构。此外,还可以发现 ^ f i f ^ i 与 ^ f i + n / 2 f ^ i + n / 2 式子有一定的相似性:
^ f i = ^ f 0 , i + ω i n ^ f 1 , i , ^ f i + n / 2 = ^ f 0 , i − ω i n ^ f 1 , i ( i < n / 2 ) f ^ i = f ^ 0 , i + ω n i f ^ 1 , i , f ^ i + n / 2 = f ^ 0 , i − ω n i f ^ 1 , i ( i < n / 2 )
其实有 ^ f 0 / 1 , i + n / 2 = ^ f 0 / 1 , i ( i < n / 2 ) f ^ 0 / 1 , i + n / 2 = f ^ 0 / 1 , i ( i < n / 2 ) 。从而可以一次计算两个的值,且每次分治长度会减半,从而我们得到一个:
T ( n ) = 2 T ( n / 2 ) + O ( n ) , T ( n ) = O ( n log n ) T ( n ) = 2 T ( n / 2 ) + O ( n ) , T ( n ) = O ( n log n )
的做法。
递归常数比较大,怎么办呢?模拟一下递归的过程,我们可以找到一个规律。从而可以让我们模拟递归的过程。具体来讲,如果令 g i = f r e v i g i = f r e v i ,其中 r e v i r e v i 表示将 n n 位二进制数 i i 的位反转得到的数,则第 h h 层就是对:
( ⟨ i 2 n − h + 1 ⋯ i 2 n − h + 1 + 2 n − h − 1 ⟩ , ⟨ i 2 n − h + 1 + 2 n − h ⋯ ( i + 1 ) 2 n − h + 1 − 1 ⟩ ) ( ⟨ i 2 n − h + 1 ⋯ i 2 n − h + 1 + 2 n − h − 1 ⟩ , ⟨ i 2 n − h + 1 + 2 n − h ⋯ ( i + 1 ) 2 n − h + 1 − 1 ⟩ )
进行合并。其中 0 ≤ i < 2 h − 1 0 ≤ i < 2 h − 1 。则我们从底向上模拟即可。
实现的时候为了缩小常数我们可以预处理一下单位根。
void FFT (comp* f)
{
for (int i = 0 ; i < lim; ++i) if (i < rev[i]) std::swap (f[i], f[rev[i]]);
for (int h = 2 , t = 1 , p = lim / h; h <= lim; h <<= 1 , t <<= 1 , p >>= 1 )
{
for (int j = 0 ; j < lim; j += h)
for (int q = 0 , k = j; q < t; ++q, ++k)
{
comp u = f[k], v = f[k + t] * wn[p * q];
f[k] = u + v; f[k + t] = u - v;
}
}
}
其中 wn[i]
表示 ω i l i m ω l i m i 。所以 wn[p * q]
为 ω q l i m h l i m = ω q h ω l i m q l i m h = ω h q 。这里如果是在模 p p 意义下,n n 次单位根就变成了 g p − 1 n g p − 1 n ,其中 g g 是 p p 的原根。单位根存在,当且仅当 n | p − 1 n | p − 1 。这也解释了为什么 N T T N T T 的模数必须满足 k 2 n + 1 k 2 n + 1 的形式。
然后我们来看看 B l u e s t e i n ′ s a l g o r i t h m B l u e s t e i n ′ s a l g o r i t h m 。考虑我们刚刚 D F T D F T 的过程:
^ f i = n − 1 ∑ j = 0 f j ω i j n = n − 1 ∑ j = 0 f j ω ( i + j 2 ) − ( i 2 ) − ( j 2 ) n = ω − ( i 2 ) n n − 1 ∑ j = 0 f j ω − ( j 2 ) n ω ( i + j 2 ) n f ^ i = ∑ j = 0 n − 1 f j ω n i j = ∑ j = 0 n − 1 f j ω n ( i + j 2 ) − ( i 2 ) − ( j 2 ) = ω n − ( i 2 ) ∑ j = 0 n − 1 f j ω n − ( j 2 ) ω n ( i + j 2 )
其中利用了 i j = ( i + j 2 ) − ( i 2 ) − ( j 2 ) i j = ( i + j 2 ) − ( i 2 ) − ( j 2 ) 这个等式。从而得到了一个差卷积的形式,可以用 F F T F F T 计算。I D F T I D F T 的过程是类似的。
FFT 实现的优化
利用转置原理对 F F T F F T 实现进行的常数优化是非常巨大的,且对于减少码量也有帮助。粗体大写表示矩阵,粗体小写表示向量。本部分不会详细系统介绍转置原理。
首先要知道转置定理:
C = A B ⟺ C T = B T A T C = A B ⟺ C T = B T A T
而我们知道,D F T D F T 的本质是线性变换,所以有:
a = A b ⟺ a T = b T A T a = A b ⟺ a T = b T A T
所以如果我们能找到变换矩阵 A A 的转置,就能以另一种方法计算该线性变换。
对于 F F T F F T ,它的变换矩阵是范德蒙德矩阵,就像我们看到的,它是关于主对角线对称的,所以它的转置等于自身。也就是说转置完等于没转置。看来从整体入手的优化失效了。
考虑把整个大矩阵分成若干小矩阵,即设:
A = A 1 A 2 A 3 ⋯ A n A = A 1 A 2 A 3 ⋯ A n
则:
A T = A T n A T n − 1 A T n − 2 ⋯ A T 1 A T = A n T A n − 1 T A n − 2 T ⋯ A 1 T
即我们可以把整个过程分成若干小的线性变换,然后倒着执行每个的转置过程。
对应到 F F T F F T 上,观察我们的过程:
对序列做 b i t r e v b i t r e v 变换,为模拟递归做准备。
模拟递归第 n n 层。
模拟递归第 n − 1 n − 1 层。
⋮ ⋮
模拟递归第 1 1 层。
倒过来就是:
模拟递归第 1 1 层的转置过程。
⋮ ⋮
模拟递归第 n n 层的转置过程。
对序列做 b i t r e v b i t r e v 变换的转置过程,这个转置过来等于自身。
如果把它和正常的 I D F T I D F T 过程连起来,就能发现 b i t r e v b i t r e v 的过程被消掉了。而事实上,由于内存访问不连续等玄学原因,b i t r e v b i t r e v 是比较费时间的。从而在这里优化了常数。现在我们需要解决的问题是,模拟递归的转置过程是什么?
考虑模拟递归对应的矩阵。第 i i 层对应的是一个 2 n − i + 1 × 2 n − i + 1 2 n − i + 1 × 2 n − i + 1 的矩阵。以 n − i + 1 = 3 n − i + 1 = 3 为例:
⎡ ⎢
⎢
⎢
⎢
⎢
⎢
⎢
⎢
⎢
⎢
⎢
⎢
⎢
⎢
⎢ ⎣ 1 0 0 0 1 0 0 0 0 1 0 0 0 1 0 0 0 0 1 0 0 0 1 0 0 0 0 1 0 0 0 1 ω 0 8 0 0 0 − ω 0 8 0 0 0 0 ω 1 8 0 0 0 − ω 1 8 0 0 0 0 ω 2 8 0 0 0 − ω 2 8 0 0 0 0 ω 3 8 0 0 0 − ω 3 8 ⎤ ⎥
⎥
⎥
⎥
⎥
⎥
⎥
⎥
⎥
⎥
⎥
⎥
⎥
⎥
⎥ ⎦ [ 1 0 0 0 1 0 0 0 0 1 0 0 0 1 0 0 0 0 1 0 0 0 1 0 0 0 0 1 0 0 0 1 ω 8 0 0 0 0 − ω 8 0 0 0 0 0 ω 8 1 0 0 0 − ω 8 1 0 0 0 0 ω 8 2 0 0 0 − ω 8 2 0 0 0 0 ω 8 3 0 0 0 − ω 8 3 ]
转置!
⎡ ⎢
⎢
⎢
⎢
⎢
⎢
⎢
⎢
⎢
⎢
⎢
⎢
⎢
⎢
⎢
⎢
⎢ ⎣ 1 0 0 0 ω 0 8 0 0 0 0 1 0 0 0 ω 1 8 0 0 0 0 1 0 0 0 ω 2 8 0 0 0 0 1 0 0 0 ω 3 8 1 0 0 0 − ω 0 8 0 0 0 0 1 0 0 0 − ω 1 8 0 0 0 0 1 0 0 0 − ω 2 8 0 0 0 0 1 0 0 0 − ω 3 8 ⎤ ⎥
⎥
⎥
⎥
⎥
⎥
⎥
⎥
⎥
⎥
⎥
⎥
⎥
⎥
⎥
⎥
⎥ ⎦ [ 1 0 0 0 ω 8 0 0 0 0 0 1 0 0 0 ω 8 1 0 0 0 0 1 0 0 0 ω 8 2 0 0 0 0 1 0 0 0 ω 8 3 1 0 0 0 − ω 8 0 0 0 0 0 1 0 0 0 − ω 8 1 0 0 0 0 1 0 0 0 − ω 8 2 0 0 0 0 1 0 0 0 − ω 8 3 ]
这样就得到了转置之后的式子。
^ f i = ^ f 0 , i + ^ f 1 , i , ^ f i + n / 2 = ω i n ( ^ f 0 , i − ^ f 1 , i ) ( i < n / 2 ) f ^ i = f ^ 0 , i + f ^ 1 , i , f ^ i + n / 2 = ω n i ( f ^ 0 , i − f ^ 1 , i ) ( i < n / 2 )
从而可以写出代码。
inline void NTT (int * f)
{
for (int h = lim, t = lim >> 1 , p = 1 ; h >= 2 ; h >>= 1 , t >>= 1 , p <<= 1 )
for (int j = 0 ; j < lim; j += h)
for (int k = j, q = 0 ; q < t; ++q, ++k)
{
int u = f[k], v = f[k + t];
f[k] = (u + v) % mod; f[k + t] = (ll)w[0 ][p * q] * (u + mod - v) % mod;
}
}
inline void INTT (int * f)
{
for (int h = 2 , t = 1 , p = (lim >> 1 ); h <= lim; h <<= 1 , t <<= 1 , p >>= 1 )
for (int j = 0 ; j < lim; j += h)
for (int k = j, q = 0 ; q < t; ++q, ++k)
{
int u = f[k], v = (ll)w[1 ][p * q] * f[k + t] % mod;
f[k] = (u + v) % mod; f[k + t] = (u + mod - v) % mod;
}
for (int i = 0 , inv = ksm (lim, mod - 2 ); i < lim; ++i) f[i] = (ll)f[i] * inv % mod;
}
其中 w[0/1][i]
分别表示 ω i l i m ω l i m i 和 ω − i l i m ω l i m − i 。真的很快还好写
写完了。我大概是想不开了才会在 N O I N O I 前总结这东西。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 如何调用 DeepSeek 的自然语言处理 API 接口并集成到在线客服系统
· 【译】Visual Studio 中新的强大生产力特性
· 2025年我用 Compose 写了一个 Todo App