目录
- 前言
- 符号约定
- 或运算
- 与运算
- 异或运算
- 另一个角度的 FWT
- 同或运算
- FWT 是线性变换
- 例题
- [例题1:洛谷P4717 【模板】快速莫比乌斯/沃尔什变换 (FMT/FWT)](https://www.luogu.com.cn/problem/P4717)
- 例题2:BZOJ4589 Hard Nim
- [例题3:2020牛客多校2 E题]([E-Exclusive OR_2020牛客暑期多校训练营(第二场) (nowcoder.com)](https://ac.nowcoder.com/acm/contest/5667/E))
- [例题4:hdu5909 Tree Cutting](http://acm.hdu.edu.cn/showproblem.php?pid=5909)
- [例题5:CodeForces662C Binary Table](https://codeforces.com/problemset/problem/662/C)
- 后言
- 参考
前言
其实我还没有退役,又回来更新算法笔记啦~~
我们知道 FFT 可以在 O ( n log n ) O(n\log n) O(nlogn) 的时间内求解 C k = ∑ i + j = k A i × B j C_k=\sum_{i+j=k} A_i \times B_j Ck=∑i+j=kAi×Bj。
而类似的,我们将求和符号中的加号换成其余的位运算符号,即得到了 FWT 。
FWT 可以在 O ( n log n ) O(n\log n) O(nlogn) 的时间内求解:
- 按位或运算 ∪ \cup ∪, C k = ∑ i ∪ j = k A i × B j C_k=\sum_{i\cup j=k} A_i \times B_j Ck=∑i∪j=kAi×Bj
- 按位与运算 ∩ \cap ∩, C k = ∑ i ∩ j = k A i × B j C_k=\sum_{i\cap j=k} A_i \times B_j Ck=∑i∩j=kAi×Bj
- 按位异或运算 ⊕ \oplus ⊕, C k = ∑ i ⊕ j = k A i × B j C_k=\sum_{i\oplus j=k} A_i \times B_j Ck=∑i⊕j=kAi×Bj
且总体流程跟 FWT 一模一样:
- 先对 A , B A,B A,B 做正变换,在 O ( n log n ) O(n\log n) O(nlogn) 的时间内求解得到 A ′ , B ′ A',B' A′,B′
- 随后在
O
(
n
)
O(n)
O(n) 的时间内求解
C
i
′
=
A
i
′
×
B
i
′
C_i'=A'_i\times B'_i
Ci′=Ai′×Bi′
- 也就是两个数组按位置做点积;
- 即两个数组中每个位置分别相乘。
- 最后对 C ′ C' C′ 做逆变换,在 O ( n log n ) O(n\log n) O(nlogn) 的时间内求解得到 C C C。
总时间复杂度还是 O ( n log n ) O(n\log n) O(nlogn) 的。
于是我们需要关注的点就是正变换、逆变换怎么做。
- A ′ = F W T [ A ] A'=FWT[A] A′=FWT[A] 表示对 A A A 做正变换。
- U F W T [ A ′ ] = A UFWT[A']=A UFWT[A′]=A 表示对 A ′ A' A′ 做逆变换。
而上面的第三步,其实就是:
- U F W T [ F W T [ A ] ] = A UFWT[FWT[A]]=A UFWT[FWT[A]]=A
现在我们需要解决的问题,就是:
- 如何构造 F W T , U F W T FWT,UFWT FWT,UFWT 。
- 以及知道定义后,如何快速计算 F W T , U F W T FWT,UFWT FWT,UFWT。
需要注意一个点,位运算始终只对下标,元素的运算还是普通乘法。
符号约定
本文中,默认多项式长度 n n n 均为 2 2 2 的非负整数次幂。文中多项式、数组其实含义基本相同,都是对一个序列处理。
为方便陈述,约定如下符号:
多项式 A A A
-
多项式,长度为 n n n,次数为 n − 1 n-1 n−1 。
-
具体有多项式 A ( x ) = a 0 + a 1 x + a 2 x 2 ⋯ + a n − 1 x n − 1 A(x)=a_0+a_1x+a_2x^2\cdots +a_{n-1}x^{n-1} A(x)=a0+a1x+a2x2⋯+an−1xn−1,
-
简写为 A A A,更像把其理解为一个数组 A = [ a 0 , a 1 , ⋯ , a n − 1 ] A=[a_0,a_1,\cdots,a_{n-1}] A=[a0,a1,⋯,an−1]
系数 a i a_i ai
- 为多项式 A A A 中 x i x^i xi 前的系数,即 [ x i ] A ( x ) [x^i]A(x) [xi]A(x)
- 本文中大写字母表示多项式,小写字母表示具体的元素/系数。
多项式 A 0 , A 1 A_0,A_1 A0,A1
- 分别表示 A A A 的前一半和后一半。
- A 0 = [ a 0 , a 1 , ⋯ , a n / 2 − 1 ] A_0=[a_0,a_1,\cdots,a_{n/2-1}] A0=[a0,a1,⋯,an/2−1]
- A 1 = [ a n / 2 , a n / 2 + 1 , ⋯ , a n ] A_1=[a_{n/2},a_{n/2+1},\cdots,a_n] A1=[an/2,an/2+1,⋯,an]
可以注意到,前两条都涉及到下标,为了含义不冲突。对于单字母的多项式,笔者采用大小写来区分。
多项式加法 A + B A+B A+B
- 表示对两个多项式做加法。
- A + B = [ a 0 + b 0 , a 1 + b 1 , ⋯ , a n − 1 + b n − 1 ] A+B=[a_0+b_0,a_1+b_1,\cdots,a_{n-1}+b_{n-1}] A+B=[a0+b0,a1+b1,⋯,an−1+bn−1]
按位卷积 A ⊕ B A\oplus B A⊕B
- 此处 ⊕ \oplus ⊕ 指的是某种二元位运算,表面含义是异或,也可以换成按位与、或、同或。
- 有 A ⊕ B = ∑ k = 0 n − 1 ∑ i ⊕ j = k a i b j A\oplus B=\sum_{k=0}^{n-1}\sum_{i\oplus j=k}a_ib_j A⊕B=∑k=0n−1∑i⊕j=kaibj
FWT 变换 F W T ( A ) FWT(A) FWT(A)
- 对 A A A 做 FWT 正变换,
FWT 逆变换 U F W T ( A ) UFWT(A) UFWT(A)
- 对 A A A 做 FWT 逆变换
因为不存在歧义,所以我们直接用下标 F W T ( A ) i FWT(A)_i FWT(A)i 表示其 x i x^i xi 前的系数
正变换后的数组 A ′ A' A′
- 即 A ′ = F W T ( A ) A'=FWT(A) A′=FWT(A)
- 二者等价
数组拼接 [ A , B ] [A,B] [A,B]
- 表示对两个数组进行拼接
- 即 [ A , B ] = [ a 0 , a 1 , ⋯ , a n − 1 , b 0 , b 1 , ⋯ , b m − 1 ] [A,B]=[a_0,a_1,\cdots,a_{n-1},b_0,b_1,\cdots,b_{m-1}] [A,B]=[a0,a1,⋯,an−1,b0,b1,⋯,bm−1]
点积 F W T ( A ) ⋅ F W T ( B ) FWT(A)\cdot FWT(B) FWT(A)⋅FWT(B)
- 运算符 ⋅ \cdot ⋅ 用在两个多项式之间时,表示点积,即相同位置乘积。
切片 A [ l : r ] A[l:r] A[l:r]
- 表示多项式 A A A 下标在 [ l , r ) [l,r) [l,r) 之间的系数。
- 即 A [ l : r ] = [ a l , a l + 1 , ⋯ , a r − 1 ] A[l:r]=[a_l,a_{l+1},\cdots,a_{r-1}] A[l:r]=[al,al+1,⋯,ar−1]
- 类似
python
编程语言
或运算
即求解 c k = ∑ i ∪ j = k a i b j c_k=\sum_{i\cup j =k}a_ib_j ck=∑i∪j=kaibj 。
FWT 变换
我们固定下标 k = i ∪ j k=i\cup j k=i∪j,去寻找满足条件的 i , j i,j i,j。
-
显然 i i i 的二进制是 1 1 1 的位,也必然是 k k k 中二进制为 1 1 1 的位。
-
考虑二进制状态压缩,即 i i i 是 k k k 的子集。
-
对于 j j j 同理。
以下构造 F W T ( A ) FWT(A) FWT(A) 的关键规则:
- k k k 的子集的子集,仍然是 k k k 的子集。
- 子集的子集之间两两乘积最后统一求和,等价于子集的和和与另一个子集的总和的乘积,即满足交换律。
这样说可能有点抽象,我们继续往下看。
我们令 A ′ = F W T [ A ] A'=FWT[A] A′=FWT[A],
我们让 a i ′ a_i' ai′ 等于所有是 i i i 的子集的下标 j j j 的系数和 ∑ j ⊆ i a j \sum_{j\subseteq i}a_j ∑j⊆iaj,
即 F W T ( A ) i = a i ′ = ∑ j ⊆ i a j FWT(A)_i=a'_i=\sum_{j\subseteq i}a_j FWT(A)i=ai′=∑j⊆iaj
- 其中 j ⊆ i j\subseteq i j⊆i 指的是,枚举 i i i 的所有二进制子集 j j j。
- 在编程时,表示为
j or i == i
那么对于卷积 F W T ( A ) ⋅ F W T ( B ) FWT(A)\cdot FWT(B) FWT(A)⋅FWT(B),考虑其下标 k k k 有:
F
W
T
(
A
)
k
×
F
W
T
(
B
)
k
=
a
k
′
⋅
b
k
′
=
(
∑
i
⊆
k
a
i
)
(
∑
j
⊆
k
b
j
)
=
∑
i
⊆
k
∑
j
⊆
k
a
i
b
j
=
∑
i
⊆
k
∑
j
⊆
k
a
i
b
j
=
∑
(
i
∪
j
)
⊆
k
a
i
b
j
=
F
W
T
(
C
)
k
\begin{align} FWT(A)_k\times FWT(B)_k&=a'_k\cdot b_k'\\ &=(\sum_{i\subseteq k}a_i)(\sum_{j\subseteq k}b_j)\\ &=\sum_{i\subseteq k}\sum_{j\subseteq k}a_ib_j\\ &=\sum_{i\subseteq k}\sum_{j\subseteq k}a_ib_j\\ &=\sum_{(i\cup j)\subseteq k} a_ib_j\\ &=FWT(C)_k \end{align}
FWT(A)k×FWT(B)k=ak′⋅bk′=(i⊆k∑ai)(j⊆k∑bj)=i⊆k∑j⊆k∑aibj=i⊆k∑j⊆k∑aibj=(i∪j)⊆k∑aibj=FWT(C)k
所以
F
W
T
(
C
)
=
F
W
T
(
A
)
⋅
F
W
T
(
B
)
FWT(C)=FWT(A)\cdot FWT(B)
FWT(C)=FWT(A)⋅FWT(B) 成立。
那么 F W T ( ⋅ ) = ∑ j ⊆ i a j FWT(\cdot )=\sum_{j\subseteq i}a_j FWT(⋅)=∑j⊆iaj 如何求解呢?
- 先考虑暴力,对于每个 i i i 判断所有比它小的 j j j 是否是它的子集。
- 复杂度为 O ( n 2 ) O(n^2) O(n2)。
- 需要继续优化。
对于多项式计算的一个常规套路,就是考虑分治。
- 当 2 2 2 个区间长度为 n 2 \dfrac{n}{2} 2n 的都求解出来之后,
- 考虑合并,得到一个区间长度为 n n n 的答案。
我们引入多项式 A 0 , A 1 A_0,A_1 A0,A1,分别表示 A A A 的前一半和后一半。
即下标最高位分别为 0 0 0 和 1 1 1 的。
- A 0 = [ a 0 , a 1 , ⋯ , a n / 2 − 1 ] A_0=[a_0,a_1,\cdots,a_{n/2-1}] A0=[a0,a1,⋯,an/2−1]
- A 1 = [ a n / 2 , a n / 2 + 1 , ⋯ , a n ] A_1=[a_{n/2},a_{n/2+1},\cdots,a_n] A1=[an/2,an/2+1,⋯,an]
假设已经求解出了 F W T ( A 0 ) , F W T ( A 1 ) FWT(A_0),FWT(A_1) FWT(A0),FWT(A1),那么如何合并求解 F W T ( A ) FWT(A) FWT(A) 呢?
因为当区间长度为 n = 1 n=1 n=1 时, F W T ( A ) = A FWT(A)=A FWT(A)=A,即 a 0 ′ = a 0 a'_0=a_0 a0′=a0,底层肯定成立。
(后面我们会知道,无论什么规则, n = 1 n=1 n=1这条永远不变,因为 F W T FWT FWT 规则有前一半后一半 A 0 , A 1 A_0,A_1 A0,A1 也就说明 n n n 至少为 2 2 2)
类似归纳法,我们只需要向上推即可。
- 我们知道 F W T ( ⋅ ) FWT(\cdot ) FWT(⋅) 含义,是这个下标位置的子集之和。
- 求解
F
W
T
(
A
1
)
FWT(A_1)
FWT(A1) 时,因为区间长度为
n
2
\dfrac{n}{2}
2n。
- 因为递归求解,其实我们此时眼中,其下标只是 0 ∼ n 2 − 1 0\sim \dfrac{n}{2} -1 0∼2n−1。
- 而在求解 F W T ( A ) FWT(A) FWT(A) 时,其下标就变成了 n 2 ∼ ( n − 1 ) \dfrac{n}{2}\sim (n-1) 2n∼(n−1)。
- 而 F W T ( A 0 ) FWT(A_0) FWT(A0) 对应的下标,则没有任何变化,所以变成 F W T ( A ) FWT(A) FWT(A) ,这部分对应的值也没有任何变化。即 F W T ( A ) [ 0 : n / 2 ] = F W T ( A 0 ) FWT(A)[0:n/2]=FWT(A_0) FWT(A)[0:n/2]=FWT(A0)
而后半部分 F W T ( A ) [ n / 2 , n ] FWT(A)[n/2,n] FWT(A)[n/2,n] 则还需要再考虑:
- 对于其中的一个下标
k
k
k,我们知道起二进制表示为
1xxxx
,他已经统计了子集形式为1????
的子集 j j j。 - 现在只需要确保统计进其子集形式为
0????
的子集 j j j 即可,而其中????
显然不能大于xxxx
。 - 而这个信息,在
F
W
T
(
A
0
)
(
k
−
n
/
2
)
FWT(A_0)_{(k-n/2)}
FWT(A0)(k−n/2) 中恰好就有,其下标表示恰好就为
0xxxx
。 - 也就是说,在位置偏移 n / 2 n/2 n/2 的下标,恰好就有我们要的信息。
- 直接加这个数字即可。
于是做多项式加法即可,按下标相加,即 O ( n ) O(n) O(n) 得到了 F W T ( A ) [ n / 2 , n ] = F W T ( A 0 ) + F W T ( A 1 ) FWT(A)[n/2,n]=FWT(A_0)+FWT(A_1) FWT(A)[n/2,n]=FWT(A0)+FWT(A1)。
最后拼接起来有 F W T ( A ) = [ F W T ( A 0 ) , F W T ( A 0 ) + F W T ( A 1 ) ] FWT(A)=[FWT(A_0),FWT(A_0)+FWT(A_1)] FWT(A)=[FWT(A0),FWT(A0)+FWT(A1)]。
总的时间复杂度是 T ( n ) = 2 T ( n / 2 ) + O ( n ) T(n)=2T(n/2)+O(n) T(n)=2T(n/2)+O(n),同归并排序为 T ( n ) = O ( n log n ) T(n)=O(n\log n) T(n)=O(nlogn)。
当初看这块内容时,我突然想到树状数组,然后就瞬间明白了这一块内容,有异曲同工之妙。
逆变换
将得到的 F W T ( A ) , F W T ( B ) FWT(A),FWT(B) FWT(A),FWT(B) 做点积即得到了 F W T ( C ) FWT(C) FWT(C)。
现在考虑如何设计逆变换 U F W T ( ⋅ ) UFWT(\cdot ) UFWT(⋅) 变回 C C C。
依旧类似的,考虑分治。
对于 A A A,求解出了 U F W T ( A 0 ) , U F W T ( A 1 ) UFWT(A_0),UFWT(A_1) UFWT(A0),UFWT(A1),那么如何合并求解 U F W T ( A ) UFWT(A) UFWT(A) 呢?
容易知道 n = 1 n=1 n=1,依旧有 U F W T ( A ) = A UFWT(A)=A UFWT(A)=A,即 a 0 = a 0 ′ a_0=a'_0 a0=a0′。
- 是一个反演的过程,
- U F W T ( ⋅ ) UFWT(\cdot) UFWT(⋅) 的含义,是扣除掉所有这个下标位置的真子集。
- 不难知道, U F W T ( A 0 ) UFWT(A_0) UFWT(A0) 对应的下标,已经扣除完了。
- 而对于
U
F
W
T
(
A
1
)
UFWT(A_1)
UFWT(A1),考虑下标
k
∈
[
n
/
2
,
n
)
k\in [n/2,n)
k∈[n/2,n) ,设
k=1xxxx
,其已经扣掉了1????
这样的真子集了。还差0????
这样的真子集没扣除掉。 - 此时这些信息在 U F W T ( A 0 ) ( k − n / 2 ) UFWT(A_0)_{(k-n/2)} UFWT(A0)(k−n/2) 里面有。
- 只需要扣除这个数字即可。
故,最终有 U F W T ( A ) = [ U F W T ( A 0 ) , U F W T ( A 1 ) − U F W T ( A 0 ) ] UFWT(A)=[UFWT(A_0),UFWT(A_1)-UFWT(A_0)] UFWT(A)=[UFWT(A0),UFWT(A1)−UFWT(A0)]。
时间复杂度依旧为 O ( n log n ) O(n\log n) O(nlogn)。
代码实现
按照上面的思路,我们可以很轻松将递归代码模拟出来。
这个递归过程是**“尾递归”,学过编译原理的同学应该知道,是可以化简为迭代**的。
迭代实现,常数更小
我们发现,
- 只有在处理 F W T ( A 1 ) / U F W T ( A 1 ) FWT(A_1)/UFWT(A_1) FWT(A1)/UFWT(A1) 信息的时候,需要加上/扣掉 F W T ( A 0 ) / U F W T ( A 0 ) FWT(A_0)/UFWT(A_0) FWT(A0)/UFWT(A0) 对应位置的信息。
- 而处理 F W T ( A 0 ) / U F W T ( A 0 ) FWT(A_0)/UFWT(A_0) FWT(A0)/UFWT(A0),则什么都不用做。
- 所以重点应该放在前者上面。
进一步,我们解读迭代代码,更深刻理解
个人认为迭代代码实现的思路,跟狄利克雷前后缀和非常相似
- 读者若更多的空闲时间,可以学习下狄利克雷前后缀和
- 可能会对这份代码有更深的理解。
其中的 type = 1
时表示正变换,type =-1
表示逆变换。
解释如下:
- 外层 x x x 用于表示当前 A A A 的长度,
- k k k 表示区间长度的一半,也就是 A 1 A_1 A1 要找的信息,扣掉这个长度,就在 A 0 A_0 A0 中找到相应的
- i i i 表示枚举每段长度为 x x x 的区间的左端点, j j j 遍历这个区间的每个位置。
- 内层
(a[i + j + k] += a[i + j] * type) %= P;
- 拆分为
(a[i + j + k] += a[i + j] * type)
表示加上/减去对应位置的元素。 - 因为赋值号 返回的是一个左值,所以直接外面套个括号对
a[i + j + k]
取模。 - 最终展示为
(a[i + j + k] += a[i + j] * type) %= P;
。
- 拆分为
void Or(LL a[], LL type)
{ // 迭代实现,常数更小
for (int x = 2; x <= n; x <<= 1)
{
int k = x >> 1;
for (int i = 0; i < n; i += x)
for (int j = 0; j < k; j++)
(a[i + j + k] += a[i + j] * type) %= P;
}
}
与运算
即求解 c k = ∑ i ∩ j = k a i b j c_k=\sum_{i\cap j=k}a_ib_j ck=∑i∩j=kaibj 。
FWT变换和逆变换
其实与运算,跟或运算完全类似,因为他们是对称的。
-
因为子集和超集,是相对应的。
-
i i i 是 k k k 的子集,那么 k k k 就是 i i i 的超集。
于是可以通过以下性质来构造规则:
- 集合 k k k 的超集的超集,还是 k k k 的超集。
- 超集的超集之间两两乘积最后统一求和,等价于超集的和和与另一个超集的总和的乘积,即满足交换律。
并且,我们在处理 A 0 A_0 A0 和 A 1 A_1 A1 的关系时,原本是对于 A 1 A_1 A1 要运算,现在变成对于 A 0 A_0 A0 要运算。
模仿上面的过程,很容易得到:
-
F W T ( A ) = [ F W T ( A 0 ) + F W T ( A 1 ) , F W T ( A 1 ) ] FWT(A)=[FWT(A_0)+FWT(A_1),FWT(A_1)] FWT(A)=[FWT(A0)+FWT(A1),FWT(A1)]。
-
U F W T ( A ) = [ U F W T ( A 0 ) − U F W T ( A 1 ) , U F W T ( A 1 ) ] UFWT(A)=[UFWT(A_0)-UFWT(A_1),UFWT(A_1)] UFWT(A)=[UFWT(A0)−UFWT(A1),UFWT(A1)]。
此过程留给读者自行做练习,不在赘述。
复杂度依旧为 O ( n log n ) O(n\log n) O(nlogn)。
代码实现
只需要将或运算实现中,a[i + j + k]
与 a[i + j]
调换,即可得到与运算的代码
void And(LL a[], LL type)
{ // 迭代实现,常数更小
for (int x = 2; x <= n; x <<= 1)
{
int k = x >> 1;
for (int i = 0; i < n; i += x)
for (int j = 0; j < k; j++)
(a[i + j] += a[i + j + k] * type) %= P;
}
}
异或运算
即求解 c k = ∑ i ⊕ j = k a i b j c_k=\sum_{i\oplus j=k}a_ib_j ck=∑i⊕j=kaibj 。
也是考得最多的。
FWT变换
约定:
- p o p c n t ( x ) popcnt(x) popcnt(x)
- 表示 x x x 二进制中 1 1 1 的数量。
- x ∘ y x\circ y x∘y
- 二者按位与后,剩余的 1 1 1 的数量,的奇偶性。
- 即 x ∘ y = p o p c n t ( x ∩ y ) m o d 2 x\circ y=popcnt(x\cap y)\bmod 2 x∘y=popcnt(x∩y)mod2
可以发现 ∘ \circ ∘ 对于 ⊕ \oplus ⊕ 有分配律。
- 即 ( x ∘ y ) ⊕ ( x ∘ z ) = x ∘ ( y ⊕ z ) (x\circ y)\oplus (x\circ z)=x\circ(y\oplus z) (x∘y)⊕(x∘z)=x∘(y⊕z)
- 汉字意思是:
- x x x 和 y y y 与的奇偶性,跟 x x x 和 z z z 与的奇偶性,的异或
- 结果等价于 x x x 和 y ⊕ z y\oplus z y⊕z 与的奇偶性。
这个分配律就是构造 F W T FWT FWT 规则的关键。
考虑证明该分配律 ( x ∘ y ) ⊕ ( x ∘ z ) = x ∘ ( y ⊕ z ) (x\circ y)\oplus (x\circ z)=x\circ(y\oplus z) (x∘y)⊕(x∘z)=x∘(y⊕z):
- 这个式子的值域是什么?
- 没错只有 0 0 0 和 1 1 1,可以理解为 奇偶性相同、不同 两种情况。
- 考虑将新增一位最高位,其实
x
x
x 新增的一位是什么并不重要,对
y
,
z
y,z
y,z 分类:
- 如果是 0 , 0 0,0 0,0 的话,左边两个奇偶性结果都不变,右边 y ⊕ z y\oplus z y⊕z 也不变
- 如果是 1 , 1 1,1 1,1 的话,左边两个奇偶性同时改变,右边 y ⊕ z y\oplus z y⊕z 也不变,但是左边同时改变奇偶性之后,异或结果仍然跟先前相同(奇偶性只有相同、不同)
- 如果是 0 , 1 0,1 0,1 或 1 , 0 1,0 1,0 的话,左边两个奇偶性不改变,左边奇偶性最后改变,同理右边 y ⊕ z y\oplus z y⊕z 改变,右边奇偶性也改变。
- Q.E.D.
于是我们可以构造 F W T ( A ) FWT(A) FWT(A):
- F W T ( A ) i = a i ′ = ∑ i ∘ j = 0 a j − ∑ i ∘ j = 1 a j FWT(A)_i=a_i'=\sum_{i\circ j=0}a_j-\sum_{i\circ j=1} a_j FWT(A)i=ai′=∑i∘j=0aj−∑i∘j=1aj。
- 即跟当前位的奇偶性为 0 0 0 的减去奇偶性为 1 1 1 的。
证明,对于下标
k
k
k 有:
F
W
T
(
A
)
i
×
F
W
T
(
B
)
i
=
a
i
′
⋅
b
i
′
=
(
∑
i
∘
j
=
0
a
j
−
∑
i
∘
j
=
1
a
j
)
(
∑
i
∘
k
=
0
b
k
−
∑
i
∘
k
=
1
b
k
)
=
(
∑
i
∘
j
=
0
a
j
∑
i
∘
k
=
0
b
k
+
∑
i
∘
j
=
1
a
j
∑
i
∘
k
=
1
b
k
)
−
(
∑
i
∘
j
=
0
a
j
∑
i
∘
k
=
1
b
k
+
∑
i
∘
j
=
1
a
j
∑
i
∘
k
=
0
b
k
)
\begin{align} FWT(A)_i\times FWT(B)_i&=a'_i\cdot b_i'\\ &=(\sum_{i\circ j=0}a_j-\sum_{i\circ j=1} a_j)(\sum_{i\circ k=0}b_k-\sum_{i\circ k=1} b_k)\\ &=(\sum_{i\circ j=0}a_j\sum_{i\circ k=0}b_k+\sum_{i\circ j=1} a_j\sum_{i\circ k=1} b_k)-(\sum_{i\circ j=0}a_j\sum_{i\circ k=1} b_k+\sum_{i\circ j=1} a_j\sum_{i\circ k=0}b_k)\\ \end{align}
FWT(A)i×FWT(B)i=ai′⋅bi′=(i∘j=0∑aj−i∘j=1∑aj)(i∘k=0∑bk−i∘k=1∑bk)=(i∘j=0∑aji∘k=0∑bk+i∘j=1∑aji∘k=1∑bk)−(i∘j=0∑aji∘k=1∑bk+i∘j=1∑aji∘k=0∑bk)
考虑此处求和符号
+
∞
2
\dfrac{+\infty}{2}
2+∞ (bushi) 的下标,
- 因为 $\circ $ 运算的结果只有 0 , 1 0,1 0,1 ,所以我们对其变形,并不容易有信息的损失,
- 如 i ∘ ( j ⊕ k ) = 0 i\circ (j\oplus k)=0 i∘(j⊕k)=0 就代表了第一个括号内的所有内容。
于是有:
F
W
T
(
A
)
i
×
F
W
T
(
B
)
i
=
∑
i
∘
(
j
⊕
k
)
=
0
a
j
b
k
−
∑
i
∘
(
j
⊕
k
)
=
1
a
j
b
k
=
F
W
T
(
C
)
i
\begin{align} FWT(A)_i\times FWT(B)_i&=\sum_{i\circ (j\oplus k)=0}a_jb_k-\sum_{i\circ (j\oplus k)=1 }a_jb_k\\ &=FWT(C)_i \end{align}
FWT(A)i×FWT(B)i=i∘(j⊕k)=0∑ajbk−i∘(j⊕k)=1∑ajbk=FWT(C)i
验证了规则成立,接下来考虑如何求解,依旧采用分治:
假设已经求解出了 F W T ( A 0 ) , F W T ( A 1 ) FWT(A_0),FWT(A_1) FWT(A0),FWT(A1),考虑如何合并求解 F W T ( A ) i = ∑ i ∘ j = 0 a j − ∑ i ∘ j = 1 a j FWT(A)_i=\sum_{i\circ j=0}a_j-\sum_{i\circ j=1} a_j FWT(A)i=∑i∘j=0aj−∑i∘j=1aj
依旧是归纳法,
对于区间长度为 n = 1 n=1 n=1 时,我们令 F W T ( A ) = A FWT(A)=A FWT(A)=A,即 a 0 ′ = a 0 a'_0=a_0 a0′=a0。
- 可能有读者就疑惑了,为什么此处不代入 ∑ i ∘ j = 0 a j − ∑ i ∘ j = 1 a j \sum_{i\circ j=0}a_j-\sum_{i\circ j=1} a_j ∑i∘j=0aj−∑i∘j=1aj 呢?
- 其实做这些变换的时候, n = 1 n=1 n=1 层的规则我们永远不变,就是保留数组本身。
- 因为 FWT 是线性变换,对 n = 1 n=1 n=1 做这些变换没有任何意义,我们最终到 n = 1 n=1 n=1 想要拿回来原本的信息时,还需要再乘一个线性系数,纯属浪费时间。
- 另一方面,我们前文提到的对于 F W T ( A ) FWT(A) FWT(A) 的求解方式,前提是默认其有 A 0 , A 1 A_0,A_1 A0,A1,即 n ≥ 2 n\ge 2 n≥2 才有那一套规则,而非 n = 1 n=1 n=1 时。
继续归纳法,我们向上推。
-
我们知道 F W T ( ⋅ ) FWT(\cdot ) FWT(⋅) 含义,加上 i ∪ j i\cup j i∪j 奇偶性为偶的,减去奇偶性为奇的。
-
而 F W T ( A 0 ) FWT(A_0) FWT(A0) 对应的下标 k k k,
- 对于原本的下标,最高位新增了 0 0 0,因为 0 ∩ 0 = 0 0\cap 0 = 0 0∩0=0 所以并不会改变任何奇偶性,所以这部分保留。
- 还得计算 [ n / 2 , n ) [n/2,n) [n/2,n) 这些下标跟它的关系,因为 0 ∩ 1 = 0 0\cap 1 = 0 0∩1=0,所以也不会改变原先的奇偶性,只需要在累加上 F W T ( A 1 ) ( k + n / 2 ) FWT(A_1)_{(k+n/2)} FWT(A1)(k+n/2) 即可。
-
于是有 F W T ( A ) [ 0 , n / 2 ] = F W T ( A 0 ) + F W T ( A 1 ) FWT(A)[0,n/2]=FWT(A_0)+FWT(A_1) FWT(A)[0,n/2]=FWT(A0)+FWT(A1)
-
同样的考虑 F W T ( A 1 ) FWT(A_1) FWT(A1) 对应的下标 k k k:
- 因为 1 ∩ 0 = 0 , 1 ∩ 1 = 1 1\cap 0 =0,1\cap 1 = 1 1∩0=0,1∩1=1。
- 所以不会改变前半段奇偶性,会改变后半段奇偶性。
-
于是有 F W T ( A ) [ 0 , n / 2 ] = F W T ( A 0 ) − F W T ( A 1 ) FWT(A)[0,n/2]=FWT(A_0)-FWT(A_1) FWT(A)[0,n/2]=FWT(A0)−FWT(A1)
综上有:
- F W T ( A ) = [ F W T ( A 0 ) + F W T ( A 1 ) , F W T ( A 0 ) − F W T ( A 1 ) ] FWT(A)=[FWT(A_0)+FWT(A_1),FWT(A_0)-FWT(A_1)] FWT(A)=[FWT(A0)+FWT(A1),FWT(A0)−FWT(A1)]
逆变换
类似模仿在“或运算”那一节中的考虑方法,其实我们可以就可以推导出 U F W T UFWT UFWT 是什么样子的。
但是,笔者觉得那样太无聊了。
我们再来一点更简单、更新的方式求解逆变换。
另一个角度的 FWT
前文我已说过 FWT是一个线性变换,既然是线性变换,也就存在线性变换矩阵,也就可以表示为矩阵乘法的形式。
我们重新看或运算的公式:
- F W T ( A ) = [ F W T ( A 0 ) , F W T ( A 0 ) + F W T ( A 1 ) ] FWT(A)=[FWT(A_0),FWT(A_0)+FWT(A_1)] FWT(A)=[FWT(A0),FWT(A0)+FWT(A1)]
-
U
F
W
T
(
A
)
=
[
U
F
W
T
(
A
0
)
,
U
F
W
T
(
A
1
)
−
U
F
W
T
(
A
0
)
]
UFWT(A)=[UFWT(A_0),UFWT(A_1)-UFWT(A_0)]
UFWT(A)=[UFWT(A0),UFWT(A1)−UFWT(A0)]
设 f 0 = F W T ( A 0 ) , f 1 = F W T ( A 1 ) f_0=FWT(A_0),f_1=FWT(A_1) f0=FWT(A0),f1=FWT(A1),于是正变换可以表示成:
F W T ( A ) = [ 1 0 1 1 ] [ f 0 f 1 ] = [ f 0 f 0 + f 1 ] = [ f 0 ′ f 1 ′ ] \begin{align} FWT(A)= \begin{bmatrix} 1 & 0\\ 1 & 1 \end{bmatrix} \begin{bmatrix} f_0 \\ f_1 \end{bmatrix} = \begin{bmatrix} f_0 \\ f0+f_1 \end{bmatrix} = \begin{bmatrix} f_0' \\ f_1' \end{bmatrix} \end{align} FWT(A)=[1101][f0f1]=[f0f0+f1]=[f0′f1′]
那么,考虑逆变换,我们现在知道 f 0 ′ , f 1 ′ f_0',f_1' f0′,f1′,如何倒推回去?
对的,乘一个逆矩阵,就是逆变换了
U F W T ( A ) = [ 1 0 − 1 1 ] [ f 0 ′ f 1 ′ ] = [ f 0 ′ f 1 ′ − f 0 ] = [ f 0 f 1 ] \begin{align} UFWT(A)= \begin{bmatrix} 1 & 0\\ -1 & 1 \end{bmatrix} \begin{bmatrix} f_0' \\ f_1' \end{bmatrix} = \begin{bmatrix} f_0' \\ f1'-f_0 \end{bmatrix} = \begin{bmatrix} f_0 \\ f_1 \end{bmatrix} \end{align} UFWT(A)=[1−101][f0′f1′]=[f0′f1′−f0]=[f0f1]
与我们最初推出来的 U F W T ( A ) = [ U F W T ( A 0 ) , U F W T ( A 1 ) − U F W T ( A 0 ) ] UFWT(A)=[UFWT(A_0),UFWT(A_1)-UFWT(A_0)] UFWT(A)=[UFWT(A0),UFWT(A1)−UFWT(A0)] 的形式一模一样!
于是我们可以类似求解 异或运算 的逆变换:
求解如下矩阵的逆矩阵即可:
[
1
1
1
−
1
]
\begin{bmatrix} 1 & 1\\ 1 & -1 \end{bmatrix}
[111−1]
解得为:
[
0.5
0.5
0.5
−
0.5
]
\begin{bmatrix} 0.5 & 0.5\\ 0.5 & -0.5 \end{bmatrix}
[0.50.50.5−0.5]
故对于异或运算,
U
F
W
T
(
A
)
=
[
U
F
W
T
(
A
0
)
+
U
F
W
T
(
A
1
)
2
,
U
F
W
T
(
A
0
)
−
U
F
W
T
(
A
1
)
2
]
UFWT(A)=[\dfrac{UFWT(A_0)+UFWT(A_1)}{2},\dfrac{UFWT(A_0)-UFWT(A_1)}{2}]
UFWT(A)=[2UFWT(A0)+UFWT(A1),2UFWT(A0)−UFWT(A1)]
异或运算代码实现
我们接着上文,实现异或代码
正变换时 type = 1
,逆变换时 type = 1 / 2
,注意此处是
2
2
2 的逆元。
总体实现跟上面相同,最内层循环写成这样仅为了可以通过 type
切换,当然读者也可以写 if-else
来替换增加可读性。
void Xor(LL *a, LL type)
{
for (LL x = 2; x <= n; x <<= 1)
{
LL k = x >> 1;
for (LL i = 0; i < n; i += x)
{
for (LL j = 0; j < k; j++)
{
(a[i + j] += a[i + j + k]) %= P;
(a[i + j + k] = a[i + j] - a[i + j + k] * 2) %= P;
(a[i + j] *= type) %= P;
(a[i + j + k] *= type) %= P;
}
}
}
}
同或运算
即求解 c k = ∑ i ⊙ j = k a i b j c_k=\sum_{i\odot j=k}a_ib_j ck=∑i⊙j=kaibj 。
FWT变换和逆变换
与异或运算类似的,
约定如下符号
-
x
∘
y
x\circ y
x∘y
- 二者按位或后,剩余的 1 1 1 的数量,的奇偶性。
- 即 x ∘ y = p o p c n t ( x ∪ y ‾ ) m o d 2 x\circ y=popcnt(\overline{x\cup y})\bmod 2 x∘y=popcnt(x∪y)mod2
- 先或后非,笔者认为oi-wiki里面写错了。
同样,可以发现 ∘ \circ ∘ 对于 ⊙ \odot ⊙ 有分配律。
类似的构造 F W T ( A ) i = ∑ i ∘ j = 0 a j − ∑ i ∘ j = 1 a j FWT(A)_i=\sum_{i\circ j=0}a_j-\sum_{i\circ j=1} a_j FWT(A)i=∑i∘j=0aj−∑i∘j=1aj
- 因为 ∪ \cup ∪ 和 ∩ \cap ∩ 是对称的
- 其实 ⊕ \oplus ⊕ 和 ⊙ \odot ⊙ 也是对称的
- 只需要在求解 FWT 的结果,将 A 0 A_0 A0 和 A 1 A_1 A1 对调即可,当然拼接时位置也需要对调
类似上面的过程,最终解得
F
W
T
(
A
)
=
[
F
W
T
(
A
1
)
−
F
W
T
(
A
0
)
,
F
W
T
(
A
1
)
+
F
W
T
(
A
0
)
]
U
F
W
T
(
A
)
=
[
U
F
W
T
(
A
1
)
−
U
F
W
T
(
A
0
)
2
,
U
F
W
T
(
A
1
)
+
U
F
W
T
(
A
0
)
2
]
FWT(A)=[FWT(A_1)-FWT(A_0),FWT(A_1)+FWT(A_0)]\\ UFWT(A)=[\dfrac{UFWT(A_1)-UFWT(A_0)}{2},\dfrac{UFWT(A_1)+UFWT(A_0)}{2}]\\
FWT(A)=[FWT(A1)−FWT(A0),FWT(A1)+FWT(A0)]UFWT(A)=[2UFWT(A1)−UFWT(A0),2UFWT(A1)+UFWT(A0)]
FWT 是线性变换
前文中已经提到了 F W T FWT FWT 是线性变换,也强调了 n = 1 n=1 n=1 时的特殊性。
既然是线性变换,也就满足如下性质:
-
F W T ( A + B ) = F W T ( A ) + F W T ( B ) FWT(A+B)=FWT(A)+FWT(B) FWT(A+B)=FWT(A)+FWT(B)
-
F W T ( c ⋅ A ) = c ⋅ F W T ( A ) FWT(c\cdot A)=c\cdot FWT(A) FWT(c⋅A)=c⋅FWT(A)
另一方面,也可以理解为 n = 1 n=1 n=1 时我们就算设计 F W T FWT FWT 规则时乘以一个倍数,最终变换回来也毫无影响
例题
例题1:洛谷P4717 【模板】快速莫比乌斯/沃尔什变换 (FMT/FWT)
题目描述
给定长度为 2 n 2^n 2n 两个序列 A , B A,B A,B,设
C i = ∑ j ⊕ k = i A j × B k C_i=\sum_{j\oplus k = i}A_j \times B_k Ci=j⊕k=i∑Aj×Bk
分别当 ⊕ \oplus ⊕ 是 or, and, xor 时求出 C C C。
输入格式
第一行,一个整数 n n n。
第二行, 2 n 2^n 2n 个数 A 0 , A 1 , … , A 2 n − 1 A_0, A_1, \ldots, A_{2^n-1} A0,A1,…,A2n−1。
第三行, 2 n 2^n 2n 个数 B 0 , B 1 , … , B 2 n − 1 B_0, B_1, \ldots, B_{2^n-1} B0,B1,…,B2n−1。输出格式
三行,每行 2 n 2^n 2n 个数,分别代表 ⊕ \oplus ⊕ 是 or, and, xor 时 C 0 , C 1 , … , C 2 n − 1 C_0, C_1, \ldots, C_{2^n-1} C0,C1,…,C2n−1 的值 m o d 998244353 \bmod\ 998244353 mod 998244353。
样例输入 #1
2 2 4 6 8 1 3 5 7
样例输出 #1
2 22 46 250 88 64 112 56 100 92 68 60
提示
1 ≤ n ≤ 17 1 \le n \le 17 1≤n≤17。
是 FWT 的模板题,我们代入上面的推导即可。
因为代码中其实有类似的处理流程,此时我们可以用函数指针(或者auto)来让代码优雅一点
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
constexpr int N = 2e6 + 10;
constexpr int P = 998244353;
int n, m;
LL a[N], b[N], c[N];
LL sa[N], sb[N];
LL fpow(LL a, LL b, LL md)
{
LL res = 1;
while (b)
{
if (b & 1)
res = res * a % md;
a = a * a % md;
b >>= 1;
}
return res;
}
void Or(LL a[], LL type)
{
for (int x = 2; x <= n; x <<= 1)
{
int k = x >> 1;
for (int i = 0; i < n; i += x)
for (int j = 0; j < k; j++)
(a[i + j + k] += a[i + j] * type) %= P;
}
}
void And(LL a[], LL type)
{
for (int x = 2; x <= n; x <<= 1)
{
int k = x >> 1;
for (int i = 0; i < n; i += x)
for (int j = 0; j < k; j++)
(a[i + j] += a[i + j + k] * type) %= P;
}
}
void Xor(LL *a, LL type)
{
for (LL x = 2; x <= n; x <<= 1)
{
LL k = x >> 1;
for (LL i = 0; i < n; i += x)
{
for (LL j = 0; j < k; j++)
{
(a[i + j] += a[i + j + k]) %= P;
(a[i + j + k] = a[i + j] - a[i + j + k] * 2) %= P;
(a[i + j] *= type) %= P;
(a[i + j + k] *= type) %= P;
}
}
}
}
void work(auto fwt, LL inv)
{
for (int i = 0; i < n; i++)
{
a[i] = sa[i];
b[i] = sb[i];
}
fwt(a, 1);
fwt(b, 1);
for (int i = 0; i < n; i++)
{
c[i] = a[i] * b[i] % P;
}
fwt(c, inv); // 逆变换
for (int i = 0; i < n; i++)
{
if (c[i] < 0) // 可能有负数
c[i] += P;
cout << c[i] << " \n"[i == n - 1];
}
}
void solve()
{
cin >> n;
n = 1 << n;
for (int i = 0; i < n; i++)
cin >> sa[i];
for (int i = 0; i < n; i++)
cin >> sb[i];
work(Or, -1); // 对应的fwt函数,以及逆变换系数
work(And, -1);
work(Xor, fpow(2, P - 2, P));
}
int main()
{
cin.tie(0)->sync_with_stdio(false);
cout.tie(0);
solve();
}
例题2:BZOJ4589 Hard Nim
有 n n n 堆石子,每堆石子的数量均为不超过 M M M 的质数,进行 Nim 游戏。
求解先手必败的开局局面数量,答案取模 1 0 9 + 7 10^9+7 109+7。
范围 N ≤ 1 0 18 , M ≤ 5 × 1 0 4 N\le 10^{18},M\le 5\times 10^4 N≤1018,M≤5×104,数据组数为不超过 80 80 80 组。
构造多项式 A A A,若 i ≤ M i\le M i≤M 且 i i i 是质数,则 a i = 1 a_i=1 ai=1,否则 a i = 0 a_i=0 ai=0。
于是我们需要求解的,就是 A A A 异或卷积 N N N 次的多项式 C C C 即:
-
C = A ⊕ A ⊕ ⋯ ⊕ A ⏟ N times C = \underbrace{A\oplus A\oplus \cdots \oplus A}_{N\rm\ \text{times}} C=N times A⊕A⊕⋯⊕A
-
因为每个位置都是任取一个不超过 M M M 的质数,乘法原理选出 N N N 个即可
根据 SG 函数,我们最终需要选择异或和为 0 0 0 的。
即 c 0 c_0 c0 即是答案。
于是,我们可以:
- 先求得 F W T ( A ) FWT(A) FWT(A)
- 然后对于 $d_i’=(a_{i}')^N $,求得多项式 D D D
- 随后做逆变换 C = U F W T ( D ) C=UFWT(D) C=UFWT(D)
也就是全部将这些 A A A 都做一遍 F W T FWT FWT,然后逐个位置做点积,对于单个位置也就是快速幂了。
总时间复杂度为 O ( M log M log N ) O(M\log M\log N) O(MlogMlogN) 。
类似 FFT ,我们上面的 F W T FWT FWT 的区间长度都是 2 2 2 的整数次幂,这边的 M M M 可能不是。
所以我们可以找到第一个大于 M M M 的 l e n = 2 i len=2^i len=2i,然后以 l e n len len 进行 F W T FWT FWT,并且令 [ M + 1 , l e n ) [M+1,len) [M+1,len) 均为 0 0 0, 效果就是等价的。
因为 BZOJ 现在已经无法提交了,选手可以试一下如下大样例,过了应该就是对了
135186 13215
3156 11536
999999999 50000
129999999 50000
1000000000 47521
1000000000000000000 50000
985000000000000000 47785
结果依次为
270253099
208387442
957671404
882780244
81076371
567652446
904847008
代码实现:
#include <bits/stdc++.h>
using namespace std;
constexpr int N = 50000 + 10, P = 1e9 + 7;
typedef long long LL;
LL n;
int m;
LL fpow(LL a, LL b, LL md)
{
LL res = 1;
while (b)
{
if (b & 1)
res = res * a % md;
a = a * a % md;
b >>= 1;
}
return res;
}
void Xor(LL *a, LL type, int n)
{
for (LL x = 2; x <= n; x <<= 1)
{
LL k = x >> 1;
for (LL i = 0; i < n; i += x)
{
for (LL j = 0; j < k; j++)
{
(a[i + j] += a[i + j + k]) %= P;
(a[i + j + k] = a[i + j] - a[i + j + k] * 2) %= P;
(a[i + j] *= type) %= P;
(a[i + j + k] *= type) %= P;
}
}
}
}
int p[N], tot;
bool isnp[N];
void init(int n)
{
isnp[1] = true;
for (int i = 2; i <= n; i++)
{
if (!isnp[i])
p[++tot] = i;
for (int j = 1; i * p[j] <= n; j++)
{
int t = i * p[j];
isnp[t] = true;
if (i % p[j] == 0)
break;
}
}
}
LL a[N * 2];
void solve()
{
memset(a, 0, sizeof a);
int len = 1;
while (len <= m)
len <<= 1;
for (int i = 1; i <= tot && p[i] <= m; i++)
a[p[i]] = 1;
Xor(a, 1, len);
for (int i = 0; i < len; i++)
a[i] = fpow(a[i], n, P);
Xor(a, fpow(2, P - 2, P), len);
if (a[0] < 0)
a[0] += P;
cout << a[0] << '\n';
}
int main()
{
cin.tie(0)->sync_with_stdio(false);
cout.tie(0);
init(50000);
while (cin >> n >> m)
solve();
}
[例题3:2020牛客多校2 E题](E-Exclusive OR_2020牛客暑期多校训练营(第二场) (nowcoder.com))
大致题意
给出 1 ≤ n ≤ 2 × 1 0 5 1\le n\le 2\times 10^5 1≤n≤2×105 个数 a i ( 0 ≤ a i < 2 18 ) a_i(0\le a_i<2^{18}) ai(0≤ai<218),每个数字可以重复选,问选择出 k ∈ [ 1 , n ] k\in [1,n] k∈[1,n] 个数字时,最大异或和是多少。
输出一行,包含 k k k 个整数,表示答案。
聪明的读者可以发现,这题跟例题2非常相似。
我们同样构造多项式 F ( x ) F(x) F(x),若有 a i a_i ai 这个数,则 f i = 1 f_i=1 fi=1, 否则 f i = 0 f_i=0 fi=0。
也为了避免跟题意的符号冲突,这边换成 F F F
其实也就是初始时可以取到的那些异或和。
同样求解 F F F 异或卷积 k k k 次的多项式 C C C 即:
- C = F k = F ⊕ F ⊕ ⋯ ⊕ F ⏟ k times C = F^k=\underbrace{F\oplus F\oplus \cdots \oplus F}_{k\rm\ \text{times}} C=Fk=k times F⊕F⊕⋯⊕F
那么最大的 i i i 满足 c i > 0 c_i> 0 ci>0,即是答案(因为数值表示方案数,而我们只需要知道是否存在即可)
容易知道 C = F k = F k − 1 ⊕ F C=F^k=F^{k-1}\oplus F C=Fk=Fk−1⊕F
也就是做 n − 1 n-1 n−1 次 FWT即可,设 V = 2 18 V=2^{18} V=218,那么每次的复杂度是 O ( V log V ) O(V\log V) O(VlogV) 的,总复杂度是 O ( n V log V ) O(nV\log V) O(nVlogV) 的。
显然不能接受,需要优化。
设 g i g_i gi 表示取 i i i 个数的最大异或和。
- 于是显然有 g i ≥ g i − 2 g_i\ge g_{i-2} gi≥gi−2
- 因为我们只需要再取两个相同的数,就至少会跟原来一样。
从线性基的角度,
-
不考虑取几个数,那么要取最大异或和,取出 18 18 18 个数即可到达最大异或和。
-
(你可以想一想在线性基上跑最大异或和)
-
也就是说,再取多余的数,没有任何意义了。
那最多做 18 18 18 次异或卷积,在这之后都让 g i = g i − 2 g_i=g_{i-2} gi=gi−2 ,这样是不是对的呢?
- 答案是:错的,需要做 19 19 19 次。
- 可以参考一组 hack 只求到 18 18 18 的测试数据 1 , 2 , 4 , ⋯ , 2 17 , 0 , 0 , ⋯ 1,2,4,\cdots,2^{17},0,0,\cdots 1,2,4,⋯,217,0,0,⋯
- 其中 g 17 = 2 18 − 1 − 1 , g 19 = 2 18 − 1 g_{17}=2^{18}-1-1,g_{19}=2^{18}-1 g17=218−1−1,g19=218−1。
于是,当 i ≤ 19 i\le 19 i≤19 时我们做异或卷积,当 i > 19 i>19 i>19 时,我们直接让 g i = g i − 2 g_i=g_{i-2} gi=gi−2 即可。
时间复杂度为 O ( V log 2 V ) O(V\log^2 V) O(Vlog2V)。
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
constexpr int N = 1 << 18;
const int P = 1e9 + 7;
int n, m;
void Xor(LL *a, LL type, int n)
{
for (LL x = 2; x <= n; x <<= 1)
{
LL k = x >> 1;
for (LL i = 0; i < n; i += x)
{
for (LL j = 0; j < k; j++)
{
(a[i + j] += a[i + j + k]) %= P;
(a[i + j + k] = a[i + j] - a[i + j + k] * 2) %= P;
(a[i + j] *= type) %= P;
(a[i + j + k] *= type) %= P;
}
}
}
}
LL fn[N], f[N];
int g[N];
void solve()
{
cin >> n;
int x;
int mx = 1;
for (int i = 0; i < n; i++)
{
cin >> x;
f[x] += 1;
mx = max(x, mx);
}
int len = 1;
while (len <= mx)
len <<= 1;
for (int i = 0; i <= mx; i++)
fn[i] = f[i];
Xor(f, 1, len); // 做一次正变换,省得以后每次做
for (int i = len - 1; i >= 0; i--)
{
if (fn[i] != 0)
{
g[1] = i;
break;
}
}
LL inv2 = (P + 1) / 2;
for (int k = 2; k <= n; k++)
{
if (k > 19)
{
g[k] = g[k - 2];
}
else
{
Xor(fn, 1, len);
for (int i = 0; i < len; i++)
fn[i] = fn[i] * f[i] % P;
Xor(fn, inv2, len);
for (int i = len - 1; i >= 0; i--)
{
if (fn[i] != 0)
{
g[k] = i;
break;
}
}
}
}
for (int k = 1; k <= n; k++)
cout << g[k] << " \n"[k == n];
}
int main()
{
cin.tie(0)->sync_with_stdio(false);
cout.tie(0);
solve();
}
例题4:hdu5909 Tree Cutting
题目翻译
Byteasar有一棵包含 n n n 个顶点的树 T T T,这些顶点用 1 1 1 到 n n n 的整数标记。树中的每个顶点都有一个整数值 v i v_i vi。非空树 T T T 的权值等于 v 1 ⊕ v 2 ⊕ … ⊕ v n v_1 \oplus v_2 \oplus \ldots \oplus v_n v1⊕v2⊕…⊕vn ,其中 ⊕ \oplus ⊕ 表示按位异或运算。现在对于从 [ 0 , m ) [0, m) [0,m) 的每一个整数 k k k,请计算值等于 k k k 的非空子树的数量。 T T T 的子树是 T T T 的子图,且它也是一棵树。
输入格式
第一行包含一个整数 T ( 1 ≤ T ≤ 10 ) T(1 \leq T \leq 10) T(1≤T≤10),表示测试用例的数量。
在每个测试用例中,
第一行包含两个整数 n ( n ≤ 1000 ) n(n \leq 1000) n(n≤1000) 和 m ( 1 ≤ m ≤ 2 10 ) m(1 \leq m \leq 2^{10}) m(1≤m≤210) ,表示树的大小和 v v v 的上界。
第二行包含 n n n 个整数 v 1 , v 2 , … , v n ( 0 ≤ v i < m ) v_1, v_2, \dots, v_n(0 \leq v_i < m) v1,v2,…,vn(0≤vi<m),表示每个顶点的值。
- 接下来的 n − 1 n-1 n−1 行每行包含两个整数 a i , b i a_i, b_i ai,bi ,表示顶点 a i a_i ai 和 b i b_i bi 之间有一条边 ( 1 ≤ a i , b i ≤ n ) (1 \leq a_i, b_i \leq n) (1≤ai,bi≤n)。
- 保证 m m m 可以表示为 2 k 2^k 2k ,其中 k k k 是非负整数。
输出格式
对于每个测试用例,输出一行包含 m m m 个整数,第 i i i 个数表示值等于 i i i 的非空子树的数量。答案很大,请对 1 0 9 + 7 10^9 + 7 109+7 取模。
不难看出这是一道树形 DP 的题目。
设 f [ u ] [ i ] f[u][i] f[u][i] 表示在 u u u 的子树中,异或和为 i i i 且根为 u u u 的连通子树的个数。
考虑节点 u u u,假设已经跟前 i − 1 i-1 i−1 个合并信息转移了状态,接下来考虑第 i i i 个儿子 v v v。
那么有状态转移方程:
- f [ u ] [ i ⊕ j ] = f [ u ] [ i ⊕ j ] + f [ u ] [ i ] × f [ v ] [ j ] f[u][i\oplus j]=f[u][i\oplus j]+f[u][i]\times f[v][j] f[u][i⊕j]=f[u][i⊕j]+f[u][i]×f[v][j]
- 转移需要枚举 i , j i,j i,j,复杂度为 O ( m 2 ) O(m^2) O(m2)。
- 总时间复杂度为 O ( n m 2 ) O(nm^2) O(nm2) 显然不行。
我们发现,转移的瓶颈在于 f [ u ] [ i ] × f [ v ] [ j ] f[u][i]\times f[v][j] f[u][i]×f[v][j],而他其实就是异或卷积的形式,对于所有 i ⊕ j i\oplus j i⊕j 恰好为某个值的两两乘积。
于是我们用 FWT 优化这一步,即可将转移的复杂度从 O ( m 2 ) O(m^2) O(m2) 优化到 O ( m log m ) O(m\log m) O(mlogm)。
总时间复杂度为 O ( n m log m ) O(nm\log m) O(nmlogm)。
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
typedef pair<int, int> PII;
constexpr int N = 1000 + 10, M = 1 << 10;
const int P = 1e9 + 7;
constexpr int INF = 0x3f3f3f3f;
int n, m;
void Xor(LL *a, LL type, int n)
{
for (LL x = 2; x <= n; x <<= 1)
{
LL k = x >> 1;
for (LL i = 0; i < n; i += x)
{
for (LL j = 0; j < k; j++)
{
(a[i + j] += a[i + j + k]) %= P;
(a[i + j + k] = a[i + j] - a[i + j + k] * 2) %= P;
(a[i + j] *= type) %= P;
(a[i + j + k] *= type) %= P;
}
}
}
}
vector<int> g[N];
int v[N];
LL f[N][M];
LL inv2 = (P + 1) / 2; // 2的逆元
void dfs(int u, int fa)
{
for (auto v : g[u])
if (v != fa)
dfs(v, u);
f[u][v[u]] = 1; // 只有当前点构成的子树
for (auto v : g[u])
{
if (v != fa)
{
// 保留原本的
vector<LL> tmp(m);
for (int i = 0; i < m; i++)
tmp[i] = f[u][i];
// 计算卷积部分的
Xor(f[u], 1, m);
Xor(f[v], 1, m);
for (int i = 0; i < m; i++)
f[u][i] = f[u][i] * f[v][i] % P;
Xor(f[u], inv2, m);
Xor(f[v], inv2, m); // 需要变回去,因为统计答案的时候还需要
for (int i = 0; i < m; i++)
f[u][i] = (f[u][i] + tmp[i]) % P;
}
}
}
void solve()
{
cin >> n >> m;
for (int i = 1; i <= n; i++)
{
cin >> v[i];
g[i].clear(); // 多测清空
}
for (int i = 1; i <= n; i++)
for (int j = 0; j < m; j++)
f[i][j] = 0;
for (int i = 1; i < n; i++)
{
int u, v;
cin >> u >> v;
g[u].push_back(v);
g[v].push_back(u);
}
dfs(1, 0);
vector<LL> ans(m);
// 统计以每个点位根的答案
for (int i = 0; i < m; i++)
{
for (int j = 1; j <= n; j++)
ans[i] = (ans[i] + f[j][i]) % P;
if (ans[i] < 0)
ans[i] += P;
}
for (int i = 0; i < m; i++)
cout << ans[i] << " \n"[i + 1 == m];
}
int main()
{
cin.tie(0)->sync_with_stdio(false);
cout.tie(0);
int T;
cin >> T;
while (T--)
solve();
}
例题5:CodeForces662C Binary Table
题目翻译
给定一个 n n n 行 m m m 列的表格,每个元素都是 0 / 1 0/1 0/1 。
每次操作可以选择一行或一列,把 0 / 1 0/1 0/1 翻转,即把 0 0 0 换为 1 1 1 ,把 1 1 1 换为 0 0 0 。
请问经过若干次操作后,表格中最少有多少个 1 1 1 。
输入格式
第一行,包含两个整数 n n n 和 m ( 1 ≤ n ≤ 20 , 1 ≤ m ≤ 1 0 5 ) m(1\leq n\leq20,1\leq m\leq 10^5) m(1≤n≤20,1≤m≤105)。
随后的 n n n 行,每行一个长度为 m m m 的 01 01 01 字符串。
输出格式
仅一行一个整数,表示答案。
容易发现 n ≤ 20 n\le 20 n≤20 很小,而 m ≤ 1 0 5 m\le 10^5 m≤105 很大。
不难想到枚举 n n n 的所有状态 2 n 2^n 2n。
然后再去看每一列,
- 共有 m m m 列,每列 n n n 个 0 / 1 0/1 0/1 其实可以压缩为一个数字 v v v。
- 假设我们当前枚举的状态是
- 然后根据每一列 1 1 1 的数量的多少,单独决定是否需要翻转。
这样复杂度就是 O ( 2 n m + n m ) O(2^nm+nm) O(2nm+nm)。 2 20 2^{20} 220 大概是 1 0 6 10^6 106,这样也就是 1 0 11 10^{11} 1011 显然是无法接受的。
注意区分行状态、列状态。
考虑优化:
- 类似桶排序的思路,约定 c [ x ] c[x] c[x] 表列状态 x x x 为列的数量
- f ( x ) f(x) f(x) 表示列状态为 x x x 时最小 1 1 1 的数量(也就是不翻转、翻转,两个结果的 p o p c n t popcnt popcnt 取个 min \min min)
若当前行翻转的状态为
y
y
y,那么当前的
1
1
1 的数量就是:
∑
x
=
0
2
n
−
1
c
[
x
]
f
(
x
⊕
y
)
\sum_{x=0}^{2^n-1}c[x]f(x\oplus y)
x=0∑2n−1c[x]f(x⊕y)
突然发现两个下标的异或,是个固定值
y
y
y。
于是可以变成异或卷积的形式:
∑
i
⊕
j
=
y
c
[
i
]
f
(
j
)
\sum_{i\oplus j = y}c[i]f(j)
i⊕j=y∑c[i]f(j)
而上面这个式子,其实只是 FWT 结果里面的一项,我们最终需要求解的结果是:
min
y
=
0
2
n
−
1
∑
i
⊕
j
=
y
c
[
i
]
f
(
j
)
\min_{y=0}^{2^{n}-1}\sum_{i\oplus j = y}c[i]f(j)
y=0min2n−1i⊕j=y∑c[i]f(j)
其实只需要做一次 FWT 就可以都求出来了,FWT的数组长度为
2
n
2^n
2n,所以复杂度为
O
(
2
n
log
(
2
n
)
)
=
O
(
2
n
⋅
n
)
O(2^n\log(2^n))=O(2^n\cdot n)
O(2nlog(2n))=O(2n⋅n)
故总的时间复杂度为 O ( 2 n ⋅ n + n m ) O(2^n\cdot n+nm) O(2n⋅n+nm)。
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
constexpr int B = 20, N = (1 << 20), M = 1e5 + 10;
const int P = 1e9 + 7;
constexpr int INF = 0x3f3f3f3f;
void Xor(LL *a, LL type, int n)
{
for (LL x = 2; x <= n; x <<= 1)
{
LL k = x >> 1;
for (LL i = 0; i < n; i += x)
{
for (LL j = 0; j < k; j++)
{
(a[i + j] += a[i + j + k]) %= P;
(a[i + j + k] = a[i + j] - a[i + j + k] * 2) %= P;
(a[i + j] *= type) %= P;
(a[i + j + k] *= type) %= P;
}
}
}
}
int n, m;
string s[B];
LL c[N];
LL f[N];
void solve()
{
cin >> n >> m;
int U = (1 << n) - 1;
for (int i = 0; i <= U; i++) //预处理f[x]
f[i] = min(__builtin_popcount(i), __builtin_popcount(i ^ U));
for (int i = 0; i < n; i++)
cin >> s[i];
for (int i = 0; i < m; i++)
{
int x = 0;
for (int j = 0; j < n; j++)
x |= (int)(s[j][i] - '0') << j;
c[x]++;
}
int len = 1 << n;
Xor(c, 1, len);
Xor(f, 1, len);
for (int i = 0; i < len; i++)
c[i] = c[i] * f[i] % P;
LL inv2 = (P + 1) / 2;
Xor(c, inv2, len);
LL ans = 1e18;
for (int i = 0; i < len; i++)
{
if (c[i] < 0)
c[i] += P;
ans = min(ans, c[i]);
}
cout << ans << '\n';
}
int main()
{
cin.tie(0)->sync_with_stdio(false);
cout.tie(0);
solve();
}
后言
本文的例题,更偏向于一类 FWT 的基本应用,
或者说:
- 更像推导到某一步,然后发现可以用 FWT 去优化。
- 是一类套板子的题目
虽然只是 FWT 的一角,但也是 FWT 最容易遇到的情况和题目类型。
后续我可能会出一篇从矩阵角度的 FWT 的。
- 类似 K K K 维 FWT
- 也更贴近 FWT 本质。