类 FFT 状物笔记(持续更新)
_ShiinaMahiru_ · · 算法·理论
?!积积!?
记录了 Hootime 学过的所有类 FFT 状物。如果 Hootime 学了更多的类 FFT 状物,它会来修改这篇文章。另外你也可以在 cnblog 上阅读这篇文章。
推导的时候有一定的跳步。
共通性 / Commons
都是这么一个问题:
给定两个序列
A, B ,你需要求出一个序列C ,其中C_i = \sum_{j \oplus k = i} A_j B_k 。你需要一个\mathrm O(n \log n) 的做法。
都是这么一个做法。
构造一个序列
f(A) ,其中将A 变换到f(A) 或者将f(A) 变换到A 是n \log n 的,且f(C)_{i} = f(A)_{i} f(B)_{i} 。
只是不同的问题有不同的做法。
快速傅里叶变换 / FFT
需要解决的问题是:
我们将序列
问题在于这个转换是
离散傅里叶变换 / DFT
指将
以下定义
\omega_n^k 为第k 个n 次单位根。以下设序列长度
n = 2^m 。如果n 不是2 的幂次,可以通过补0 让它是。序列是 0-index 的。
我们观察以下这个式子。以下将
然后我们定义:
于是发现:
于是我们考虑将
我们将
同理,我们将
于是我们发现
然后递归常数太大了,我们要删掉递归。你可以试试不删,你不一定能过 luogu 的板子
我们思考一下有没有迭代的做法。我们观察一下递归的顺序,发现:
于是惊奇地发现,在递归时被分在一组的位置在二进制翻转后是相邻的(翻转指顺序翻转而非 0/1 翻转为 1/0)!于是我们就可以直接迭代了。
于是我们完成了
inline void dft(cdouble a[]){
for(int i = 0; i < n; ++i)
if(i < rev[i]) swap(a[i], a[rev[i]]);
for(int l = 2; l <= n; l <<= 1){
cdouble wn = {cos(2*pi/l), sin(2*pi/l)};
for(int i = 0; i < n; i += l){
cdouble w = {1, 0};
for(int j = i; j < i+l/2; ++j){
cdouble res1 = a[j], res2 = w*a[j+l/2];
a[j] = res1+res2, a[j+l/2] = res1-res2;
w = w*wn;
}
}
}
return;
}
其中 rev[i] 是 i 翻转后的结果,cdouble 是 complex<double>。
离散傅里叶逆变换 / IDFT
接下来我们解决另一部分,即将 我不知道第一个独立构造出来的人是怎么做到的但是真的强啊
我们接下来记
下面我们来推导
然后我们令
当
当
也就是说:
也就是说
于是我们完成了
inline void dft(cdouble a[], int inv){
for(int i = 0; i < n; ++i)
if(i < rev[i]) swap(a[i], a[rev[i]]);
for(int l = 2; l <= n; l <<= 1){
cdouble wn = {cos(2*pi/l), inv*sin(2*pi/l)};
for(int i = 0; i < n; i += l){
cdouble w = {1, 0};
for(int j = i; j < i+l/2; ++j){
cdouble res1 = a[j], res2 = w*a[j+l/2];
a[j] = res1+res2, a[j+l/2] = res1-res2;
w = w*wn;
}
}
}
if(inv == -1)
for(int i = 0; i < n; ++i) a[i] /= n;
return;
}
快速数论变换 / NTT
需要解决的问题是:
我们当然可以使用 FFT 然后取模,但是你精度炸了。
我们将单位根的
constexpr llong p = 998244353;
inline void ntt(llong a[], int inv){
for(int i = 0; i < n; ++i)
if(i < rev[i]) swap(a[i], a[rev[i]]);
for(int l = 2; l <= n; l <<= 1){
llong wn = qpow(3, (p-1)/l);
if(inv == -1) wn = qpow(wn, p-2);
for(int i = 0; i < n; i += l){
llong w = 1;
for(int j = i; j < i+(l>>1); ++j){
llong r1 = a[j], r2 = w*a[j+(l>>1)]%p;
a[j] = (r1+r2)%p, a[j+(l>>1)] = (r1-r2+p)%p;
w = w*wn%p;
}
}
}
if(inv == -1){
int invn = qpow(n, p-2);
for(int i = 0; i < n; ++i) a[i] = a[i]*invn%p;
}
return;
}
注意因为我是唐人,这份代码是 qpow 放在外面,但是我懒。
我们考虑
快速沃尔什变换 / FWT
需要解决的问题是:
OR 卷积
我们还是考虑构造
直接给出构造:
首先显然地,若
j \operatorname{or} i = i, k \operatorname{or} i = i 那么(j \operatorname{or} k) \operatorname{or} i = i 。那么有:
\begin{aligned} f(A)_if(B)_i &= (\sum_{j \operatorname{or} i = i} A_j)(\sum_{k \operatorname{or} i = i} B_k) \\ &= \sum_{j \operatorname{or} i = i} \sum_{k \operatorname{or} i = i} A_j B_k \\ &= \sum_{(j \operatorname{or} k) \operatorname{or} i = i} A_j B_k \\ &= f(C)_i \end{aligned} 于是构造的正确性得证。
于是我们可以使用高维前缀和来实现
同样地,我们可以表示逆变换:
constexpr llong p = 998244353;
inline void _or(llong* a, int inv){
for(int len = 2; len <= n; len <<= 1)
for(int i = 0; i < n; i += len)
for(int j = 0; j < (len>>1); ++j)
(a[i+j+(len>>1)] += inv*a[i+j]) %= p;
return;
}
其中,inv = 1 代表正变换,inv = p-1 代表逆变换。w
AND 卷积
等价于 OR 卷积。
inline void _and(llong* a, int inv){
for(int len = 2; len <= n; len <<= 1)
for(int i = 0; i < n; i += len)
for(int j = 0; j < (len>>1); ++j)
(a[i+j] += inv*a[i+j+(len>>1)]) %= p;
return;
}
XOR 卷积
我们还是考虑构造
还是直接给出构造:定义
还是给出证明:
\begin{aligned} f(A)_if(B)_i &= (\sum_{j \oplus i = 0}A_j - \sum_{j \oplus i = 1}A_j)(\sum_{k \oplus i = 0}B_k - \sum_{k \oplus i = 1}B_k) \\ &= (\sum_{j \oplus i = 0}A_j)(\sum_{k \oplus i = 0}B_k) - (\sum_{j \oplus i = 1}A_j)(\sum_{k \oplus i = 0}B_k) - (\sum_{j \oplus i = 0}A_j)(\sum_{k \oplus i = 1}B_k) + (\sum_{j \oplus i = 1}A_j)(\sum_{k \oplus i = 1}B_k) \\ &= \sum_{(j \operatorname{xor} k) \oplus i = 0} A_jB_k - \sum_{(j \operatorname{xor} k) \oplus i = 1} A_jB_k \end{aligned} 于是得证。
但是怎么变换呢?
我们还是考虑类似 FFT 地,将
那么:
逆变换就是:
也就是说:
inline void _xor(llong* a, int inv){
for(int len = 2; len <= n; len <<= 1){
for(int i = 0; i < n; i += len){
for(int j = 0; j < (len>>1); ++j){
llong a0 = a[i+j], a1 = a[i+j+(len>>1)];
a[i+j] = (a0+a1+p)*inv%p;
a[i+j+(len>>1)] = (a0-a1+p)*inv%p;
}
}
}
return;
}
其中正变换时 inv = 1,逆变换时 inv = (p+1)/2。
注意对于我的写法,写 FWT 模板需要把数组开到 1<<17|5 而非 1<<17,或者关掉 O2。别问,问就是 SIMD 发力了。