FWT 是什么
FWT 是一个用来快速求出下标进行位运算卷积的方法。
FWT 怎么实现
FFT 的快速是怎么个快呢?它是把多项式转成点值表示法,再直接相乘,又换回来系数表示法。
类似的,这时我们只需要找到一个变换
T
T
T,对于所有的卷积,都有
T
(
A
⋅
B
)
=
T
(
A
)
⋅
T
(
B
)
T(A\cdot B)=T(A)\cdot T(B)
T(A⋅B)=T(A)⋅T(B)
I
T
(
T
(
A
⋅
B
)
)
=
A
⋅
B
IT(T(A\cdot B))=A\cdot B
IT(T(A⋅B))=A⋅B
其中第一条式子后面的点乘为同次系数相乘, I T IT IT 表示 T T T 的逆变换。
下面简单介绍或、与和异或这三种常用卷积的 FWT 方法。
或卷积
想办法构造一个变换 T T T,满足上述条件。
默认 n n n 为 2 2 2 的幂。
以下记 [ i ∣ j = j ] [i|j=j] [i∣j=j] 为 i ⊆ j i\subseteq j i⊆j
多项式 A A A 经过变换后是一个数组。
记变换后的 A A A 的第 k k k 项 T ( A ) k = ∑ j ⊆ k A j T(A)_k=\sum\limits_{j\sube k}A_j T(A)k=j⊆k∑Aj
则 T ( A ⋅ B ) p = ∑ i ⊆ p ∑ j ∣ k = i A j B k = ∑ ( j ∣ k ) ⊆ p A j b k T(A\cdot B)_p=\sum\limits_{i\sube p}\sum\limits_{j|k=i}A_jB_k=\sum\limits_{(j|k)\sube p}A_jb_k T(A⋅B)p=i⊆p∑j∣k=i∑AjBk=(j∣k)⊆p∑Ajbk
这时有个命题: ( i ∣ j ) ⊆ p ⟺ ( i ⊆ p ) ∧ ( j ⊆ p ) (i|j)\sube p\iff (i\sube p)\land(j\sube p) (i∣j)⊆p⟺(i⊆p)∧(j⊆p)
怎么证明呢?
首先看 i ⊆ j i\sube j i⊆j 意味着什么?说明 i i i 的二进制位的位置是被 j j j 的二进制位的位置包含的(这个解释是不是很像集合的用语?所以我才把它记作 i ⊆ j i\sube j i⊆j)。
( i ∣ j ) ⊆ p (i|j)\sube p (i∣j)⊆p 显然说明 ( i ⊆ p ) ∧ ( j ⊆ p ) (i\sube p)\land(j\sube p) (i⊆p)∧(j⊆p),反过来是否成立?
i ∣ j i|j i∣j 的二进制位为 1 1 1 的位置不可能凭空产生,一定是 i i i 或 j j j 有。如果 i ∣ j ⊈ p i|j\not\sube p i∣j⊆p,由上面的命题 i i i 或 j j j 至少有一个不含于 p p p,矛盾。于是得证。
则有
T
(
A
⋅
B
)
p
=
∑
(
j
∣
k
)
⊆
p
A
j
B
k
=
∑
(
j
⊆
p
)
∧
(
k
⊆
p
)
A
j
B
k
=
(
∑
j
⊆
p
A
j
)
(
∑
k
⊆
p
B
k
)
=
T
(
A
)
p
⋅
T
(
B
)
p
\begin{aligned} T(A\cdot B)_p&=\sum\limits_{(j|k)\sube p}A_jB_k\\ &=\sum\limits_{(j\sube p)\land(k\sube p)}A_jB_k\\ &=\left(\sum\limits_{j\sube p}A_j\right)\left(\sum\limits_{k\sube p}B_k\right)\\ &=T(A)_p\cdot T(B)_p \end{aligned}
T(A⋅B)p=(j∣k)⊆p∑AjBk=(j⊆p)∧(k⊆p)∑AjBk=(j⊆p∑Aj)
k⊆p∑Bk
=T(A)p⋅T(B)p
好了,构造出一个合适的变换 T T T 了,如何快速求它呢,就像 FFT 一样?
同样采用分治的方法。 T ( A ) T(A) T(A) 有 n n n 项,一开始是一整个块,每次分治把每个整块分成两块。
对于一个长度大于 1 1 1 的块 B B B,设其长度为 m m m,左边的块为 B 0 B_0 B0,右边的块为 B 1 B_1 B1。
对于在块 B B B 的下标 x 0 x_0 x0,和 x 0 + m 2 x_0+\frac m2 x0+2m 相比,二进制位只有一位不同。
观察 T ( B ) x 0 T(B)_{x_0} T(B)x0 和 T ( B ) x 0 + m 2 T(B)_{x_0+\frac m2} T(B)x0+2m 的关系。
T
(
A
)
x
0
=
∑
j
⊆
x
0
A
j
T(A)_{x_0}=\sum\limits_{j\sube x_0}A_j
T(A)x0=j⊆x0∑Aj
T
(
A
)
x
0
+
m
2
=
∑
j
⊆
(
x
0
+
m
2
)
A
j
T(A)_{x_0+\frac m2}=\sum\limits_{j\sube(x_0+\frac m2)}A_j
T(A)x0+2m=j⊆(x0+2m)∑Aj
容易得到 ∀ j ⊆ x 0 \forall j\sube x_0 ∀j⊆x0,有 j ⊆ ( x 0 + m 2 ) j\sube(x_0+\frac m2) j⊆(x0+2m)。
意思是 T ( A ) x 0 T(A)_{x_0} T(A)x0 的贡献可以加到 T ( A ) x 0 + m 2 T(A)_{x_0+\frac m2} T(A)x0+2m 里。
这时可以得出一个式子:
T
(
A
)
=
merge
(
T
(
A
0
)
,
T
(
A
1
)
+
T
(
A
0
)
)
T(A)=\operatorname{merge}(T(A_0),T(A_1)+T(A_0))
T(A)=merge(T(A0),T(A1)+T(A0))
其中 merge \operatorname{merge} merge 表示数组拼接。
逆变换的分治的式子如下
I
T
(
A
)
=
merge
(
I
T
(
A
0
)
,
I
T
(
A
1
)
−
I
T
(
A
0
)
)
IT(A)=\operatorname{merge}(IT(A_0),IT(A_1)-IT(A_0))
IT(A)=merge(IT(A0),IT(A1)−IT(A0))
感性理解,正变换在原来的基础上加上了 T ( A 0 ) T(A_0) T(A0),逆变换时就要减掉 I T ( A 0 ) IT(A_0) IT(A0)。
下面是代码实现
void fwt_or(ll a[],ll fl)
{
for(int i=2;i<=n;i<<=1){
for(int j=0;j<n;j+=i){
for(int k=j;k<j+i/2;k++){
a[k+i/2]=(a[k+i/2]+fl*a[k]+mod)%mod;
}
}
}
}
与卷积
和或卷积类似,构造变换 T T T。
上面或卷积构造 T T T 的方法实际上是利用了 ( i ∣ j ) ⊆ p ⟺ ( i ⊆ p ) ∧ ( j ⊆ p ) (i|j)\sube p\iff (i\sube p)\land(j\sube p) (i∣j)⊆p⟺(i⊆p)∧(j⊆p) 的性质。
这里也要弄出类似的命题。
记 [ i & j = i ] [i\&j=i] [i&j=i] 为 i ⊆ j i\sube j i⊆j。
有 p ⊆ ( i & j ) ⟺ ( p ⊆ i ) ∧ ( p ⊆ j ) p\sube(i\&j)\iff(p\sube i)\land(p\sube j) p⊆(i&j)⟺(p⊆i)∧(p⊆j)
所以设 T ( A ) p = ∑ p ⊆ i A i T(A)_p=\sum\limits_{p\sube i}A_i T(A)p=p⊆i∑Ai
可得
T
(
A
⋅
B
)
p
=
∑
p
⊆
(
i
&
j
)
A
i
B
j
=
∑
(
p
⊆
i
)
∧
(
p
⊆
j
)
A
i
B
j
=
(
∑
p
⊆
i
A
i
)
(
∑
p
⊆
j
B
j
)
=
T
(
A
)
p
⋅
T
(
B
)
p
\begin{aligned} T(A\cdot B)_p&=\sum\limits_{p\sube(i\&j)}A_iB_j\\ &=\sum\limits_{(p\sube i)\land(p\sube j)}A_iB_j\\ &=\left(\sum\limits_{p\sube i}A_i\right)\left(\sum\limits_{p\sube j}B_j\right)\\ &=T(A)_p\cdot T(B)_p \end{aligned}
T(A⋅B)p=p⊆(i&j)∑AiBj=(p⊆i)∧(p⊆j)∑AiBj=(p⊆i∑Ai)(p⊆j∑Bj)=T(A)p⋅T(B)p
要求
T
(
A
)
T(A)
T(A) 同样分治,直接给出式子。
T
(
A
)
=
merge
(
T
(
A
0
)
+
T
(
A
1
)
,
T
(
A
1
)
)
T(A)=\operatorname{merge}(T(A_0)+T(A_1),T(A_1))
T(A)=merge(T(A0)+T(A1),T(A1))
符号和字母意义同上。
这里的式子和上面或卷积的不一样。这里是由后面贡献前面,为什么呢?
因为对于一个大小大于一的块 B B B,长度为 m m m,如果 x 0 x_0 x0 是块 B B B 里的下标,一定满足 x 0 ⊆ ( x 0 + m 2 ) x_0\sube(x_0+\frac m2) x0⊆(x0+2m),所以只能用右边给左边做贡献。
至于逆变换也和上面差不多,为
I
T
(
A
)
=
merge
(
I
T
(
A
0
)
−
I
T
(
A
1
)
,
I
T
(
A
1
)
)
IT(A)=\operatorname{merge}(IT(A_0)-IT(A_1),IT(A_1))
IT(A)=merge(IT(A0)−IT(A1),IT(A1))
代码实现如下
void fwt_and(ll a[],ll fl)
{
for(int i=2;i<=n;i<<=1){
for(int j=0;j<n;j+=i){
for(int k=j;k<j+i/2;k++){
a[k]=(a[k]+fl*a[k+i/2]+mod)%mod;
}
}
}
}
异或卷积
到了异或,情况就比上面复杂多了。
首先得知道一个性质,设 d n d_n dn 为 n n n 的二进制中 1 1 1 的个数的奇偶性。
则有 d n ⊕ d m = d n ⊕ m d_n\oplus d_m=d_{n\oplus m} dn⊕dm=dn⊕m
证明如下
等号左边代表 n , m n,m n,m 二进制 1 1 1 的个数之和的奇偶性。
异或操作里面对于每一位如果都是 1 1 1 结果为 0 0 0,减少的 1 1 1 是两个,而其他情况 1 1 1 的数量是不会减少。因为减少两个 1 1 1 不会改变奇偶性,得证。
根据这个结论,可推出
d
i
&
p
⊕
d
j
&
p
=
d
(
i
&
p
)
⊕
(
j
&
p
)
=
d
(
i
⊕
j
)
&
p
\begin{aligned} d_{i\&p}\oplus d_{j\&p}&=d_{(i\&p)\oplus(j\&p)}\\ &= d_{(i\oplus j)\&p} \end{aligned}
di&p⊕dj&p=d(i&p)⊕(j&p)=d(i⊕j)&p
有什么式子的值会随着奇偶性的变化而变化呢?就是 ( − 1 ) n (-1)^n (−1)n。
所以令 T ( A ) p = ∑ i = 0 n − 1 ( − 1 ) d i & p A i T(A)_p=\sum\limits_{i=0}^{n-1}(-1)^{d_{i\&p}}A_i T(A)p=i=0∑n−1(−1)di&pAi
则有
T
(
A
⋅
B
)
p
=
∑
i
=
0
n
−
1
(
−
1
)
d
(
j
⊕
k
)
&
p
A
j
B
k
=
∑
i
=
0
n
−
1
(
−
1
)
d
j
&
p
⊕
d
k
&
p
A
j
B
k
=
(
∑
i
=
0
n
−
1
(
−
1
)
d
j
&
p
A
j
)
(
∑
i
=
0
n
−
1
(
−
1
)
d
k
&
p
B
k
)
=
T
(
A
)
p
⋅
T
(
B
)
p
\begin{aligned} T(A\cdot B)_p&=\sum\limits_{i=0}^{n-1}(-1)^{d_{(j\oplus k)\& p}}A_jB_k\\ &=\sum\limits_{i=0}^{n-1}(-1)^{d_{j\&p}\oplus d_{k\&p}}A_jB_k\\ &=\left(\sum\limits_{i=0}^{n-1}(-1)^{d_{j\& p}}A_j\right)\left(\sum\limits_{i=0}^{n-1}(-1)^{d_{k\&p}}B_k\right)\\ &=T(A)_p\cdot T(B)_p \end{aligned}
T(A⋅B)p=i=0∑n−1(−1)d(j⊕k)&pAjBk=i=0∑n−1(−1)dj&p⊕dk&pAjBk=(i=0∑n−1(−1)dj&pAj)(i=0∑n−1(−1)dk&pBk)=T(A)p⋅T(B)p
分治求出
T
(
A
)
T(A)
T(A) 的式子如下:
T
(
A
)
=
merge
(
T
(
A
0
)
+
T
(
A
1
)
,
T
(
A
0
)
−
T
(
A
1
)
)
T(A)=\operatorname{merge}(T(A_0)+T(A_1),T(A_0)-T(A_1))
T(A)=merge(T(A0)+T(A1),T(A0)−T(A1))
怎么证明呢?
考虑较小的前 i i i 位,不考虑其他位。
分成的两个小块对应的位, p p p 只有第 ( i − 1 ) (i-1) (i−1) 位才会改变。
对于前半部分 p p p 的那一位为 0 0 0,有 0 & 0 = 0 , 0 & 1 = 0 0\&0=0,0\&1=0 0&0=0,0&1=0,发现结果都是 0 0 0 不会有影响,所以都加起来。
对于后半部分 p p p 的那一位为 1 1 1,有 1 & 0 = 0 , 1 & 1 = 1 1\&0=0,1\&1=1 1&0=0,1&1=1,前半部分为 0 0 0 不变,后半部分为 1 1 1,所以贡献会乘上 − 1 -1 −1。
而逆变换就是反过来
I
T
(
A
)
=
merge
(
I
T
(
A
0
)
+
I
T
(
A
1
)
2
,
I
T
(
A
0
)
−
I
T
(
A
1
)
2
)
IT(A)=\operatorname{merge}\left(\frac{IT(A_0)+IT(A_1)}2,\frac{IT(A_0)-IT(A_1)}2\right)
IT(A)=merge(2IT(A0)+IT(A1),2IT(A0)−IT(A1))
手动验证一下
I T ( A 0 ) + I T ( A 1 ) 2 + I T ( A 0 ) − I T ( A 1 ) 2 = I T ( A 0 ) \dfrac{IT(A_0)+IT(A_1)}2+\dfrac{IT(A_0)-IT(A_1)}2=IT(A_0) 2IT(A0)+IT(A1)+2IT(A0)−IT(A1)=IT(A0)
I T ( A 0 ) + I T ( A 1 ) 2 − I T ( A 0 ) − I T ( A 1 ) 2 = I T ( A 1 ) \dfrac{IT(A_0)+IT(A_1)}2-\dfrac{IT(A_0)-IT(A_1)}2=IT(A_1) 2IT(A0)+IT(A1)−2IT(A0)−IT(A1)=IT(A1)
结果是正确的。
代码实现如下
void fwt_xor(ll a[],ll fl)
{
for(int i=2;i<=n;i<<=1){
for(int j=0;j<n;j+=i){
for(int k=j;k<j+i/2;k++){
ll x=a[k]+a[k+i/2],y=a[k]-a[k+i/2]+mod;
a[k]=x*(fl==1?1:inv2)%mod,a[k+i/2]=y*(fl==1?1:inv2)%mod;
}
}
}
}
例题
P4717 【模板】快速莫比乌斯/沃尔什变换 (FMT/FWT)
参考代码如下
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=17;
const ll mod=998244353,inv2=499122177;
int n;
ll a[1<<N],b[1<<N],A[1<<N],B[1<<N];
void fwt_or(ll a[],ll fl)
{
for(int i=2;i<=n;i<<=1){
for(int j=0;j<n;j+=i){
for(int k=j;k<j+i/2;k++){
a[k+i/2]=(a[k+i/2]+fl*a[k]+mod)%mod;
}
}
}
}
void fwt_and(ll a[],ll fl)
{
for(int i=2;i<=n;i<<=1){
for(int j=0;j<n;j+=i){
for(int k=j;k<j+i/2;k++){
a[k]=(a[k]+fl*a[k+i/2]+mod)%mod;
}
}
}
}
void fwt_xor(ll a[],ll fl)
{
for(int i=2;i<=n;i<<=1){
for(int j=0;j<n;j+=i){
for(int k=j;k<j+i/2;k++){
ll x=a[k]+a[k+i/2],y=a[k]-a[k+i/2]+mod;
a[k]=x*(fl==1?1:inv2)%mod,a[k+i/2]=y*(fl==1?1:inv2)%mod;
}
}
}
}
int main()
{
scanf("%d",&n);
n=1<<n;
for(int i=0;i<n;i++) scanf("%lld",&a[i]);
for(int i=0;i<n;i++) scanf("%lld",&b[i]);
for(int i=0;i<n;i++) A[i]=a[i],B[i]=b[i];
fwt_or(A,1),fwt_or(B,1);
for(int i=0;i<n;i++) A[i]=A[i]*B[i]%mod;
fwt_or(A,-1);
for(int i=0;i<n;i++) printf("%lld ",A[i]);
puts("");
for(int i=0;i<n;i++) A[i]=a[i],B[i]=b[i];
fwt_and(A,1),fwt_and(B,1);
for(int i=0;i<n;i++) A[i]=A[i]*B[i]%mod;
fwt_and(A,-1);
for(int i=0;i<n;i++) printf("%lld ",A[i]);
puts("");
for(int i=0;i<n;i++) A[i]=a[i],B[i]=b[i];
fwt_xor(A,1),fwt_xor(B,1);
for(int i=0;i<n;i++) A[i]=A[i]*B[i]%mod;
fwt_xor(A,-1);
for(int i=0;i<n;i++) printf("%lld ",A[i]);
}