最通俗易懂的FWT变换讲解(快速沃尔什变换)

前言

其实我还没有退役,又回来更新算法笔记啦~~

我们知道 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=ij=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=ij=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=ij=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 n1

  • 具体有多项式 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+an1xn1

  • 简写为 A A A,更像把其理解为一个数组 A = [ a 0 , a 1 , ⋯   , a n − 1 ] A=[a_0,a_1,\cdots,a_{n-1}] A=[a0,a1,,an1]

系数 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/21]
  • 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,,an1+bn1]

按位卷积 A ⊕ B A\oplus B AB

  • 此处 ⊕ \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 AB=k=0n1ij=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,,an1,b0,b1,,bm1]

点积 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,,ar1]
  • 类似 python 编程语言

或运算

即求解 c k = ∑ i ∪ j = k a i b j c_k=\sum_{i\cup j =k}a_ib_j ck=ij=kaibj

FWT 变换

我们固定下标 k = i ∪ j k=i\cup j k=ij,去寻找满足条件的 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 jiaj

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=jiaj

  • 其中 j ⊆ i j\subseteq i ji 指的是,枚举 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=akbk=(ikai)(jkbj)=ikjkaibj=ikjkaibj=(ij)kaibj=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()=jiaj 如何求解呢?

  • 先考虑暴力,对于每个 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/21]
  • 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 02n1
    • 而在求解 F W T ( A ) FWT(A) FWT(A) 时,其下标就变成了 n 2 ∼ ( n − 1 ) \dfrac{n}{2}\sim (n-1) 2n(n1)
  • 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)(kn/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)(kn/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=ij=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=ij=kaibj​ 。

也是考得最多的。

FWT变换

约定:

  • p o p c n t ( x ) popcnt(x) popcnt(x)
  • 表示 x x x 二进制中 1 1 1 的数量。
  • x ∘ y x\circ y xy
  • 二者按位与后,剩余的 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 xy=popcnt(xy)mod2

可以发现 ∘ \circ 对于 ⊕ \oplus 有分配律。

  • ( x ∘ y ) ⊕ ( x ∘ z ) = x ∘ ( y ⊕ z ) (x\circ y)\oplus (x\circ z)=x\circ(y\oplus z) (xy)(xz)=x(yz)
  • 汉字意思是:
    • x x x y y y 与的奇偶性,跟 x x x z z z 与的奇偶性,的异或
    • 结果等价于 x x x y ⊕ z y\oplus z yz 与的奇偶性。

这个分配律就是构造 F W T FWT FWT 规则的关键。

考虑证明该分配律 ( x ∘ y ) ⊕ ( x ∘ z ) = x ∘ ( y ⊕ z ) (x\circ y)\oplus (x\circ z)=x\circ(y\oplus z) (xy)(xz)=x(yz)

  • 这个式子的值域是什么?
    • 没错只有 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 yz 也不变
    • 如果是 1 , 1 1,1 1,1 的话,左边两个奇偶性同时改变,右边 y ⊕ z y\oplus z yz 也不变,但是左边同时改变奇偶性之后,异或结果仍然跟先前相同(奇偶性只有相同、不同)
    • 如果是 0 , 1 0,1 0,1 1 , 0 1,0 1,0 的话,左边两个奇偶性不改变,左边奇偶性最后改变,同理右边 y ⊕ z y\oplus z yz 改变,右边奇偶性也改变。
  • 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=ij=0ajij=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=aibi=(ij=0ajij=1aj)(ik=0bkik=1bk)=(ij=0ajik=0bk+ij=1ajik=1bk)(ij=0ajik=1bk+ij=1ajik=0bk)
考虑此处求和符号 + ∞ 2 \dfrac{+\infty}{2} 2+ (bushi) 的下标,

  • 因为 $\circ $ 运算的结果只有 0 , 1 0,1 0,1 ,所以我们对其变形,并不容易有信息的损失,
  • i ∘ ( j ⊕ k ) = 0 i\circ (j\oplus k)=0 i(jk)=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(jk)=0ajbki(jk)=1ajbk=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=ij=0ajij=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 ij=0ajij=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 n2 才有那一套规则,而非 n = 1 n=1 n=1 时。

继续归纳法,我们向上推。

  • 我们知道 F W T ( ⋅ ) FWT(\cdot ) FWT() 含义,加上 i ∪ j i\cup j ij 奇偶性为偶的,减去奇偶性为奇的。

  • F W T ( A 0 ) FWT(A_0) FWT(A0) 对应的下标 k k k

    • 对于原本的下标,最高位新增了 0 0 0,因为 0 ∩ 0 = 0 0\cap 0 = 0 00=0 所以并不会改变任何奇偶性,所以这部分保留。
    • 还得计算 [ n / 2 , n ) [n/2,n) [n/2,n) 这些下标跟它的关系,因为 0 ∩ 1 = 0 0\cap 1 = 0 01=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 10=0,11=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]=[f0f1]
    那么,考虑逆变换,我们现在知道 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)=[1101][f0f1]=[f0f1f0]=[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} [1111]
解得为:
[ 0.5 0.5 0.5 − 0.5 ] \begin{bmatrix} 0.5 & 0.5\\ 0.5 & -0.5 \end{bmatrix} [0.50.50.50.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=ij=kaibj​ 。

FWT变换和逆变换

与异或运算类似的,

约定如下符号

  • x ∘ y x\circ y xy
    • 二者按位或后,剩余的 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 xy=popcnt(xy)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=ij=0ajij=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(cA)=cFWT(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=jk=iAj×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,,A2n1
第三行, 2 n 2^n 2n 个数 B 0 , B 1 , … , B 2 n − 1 B_0, B_1, \ldots, B_{2^n-1} B0,B1,,B2n1

输出格式

三行,每行 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,,C2n1 的值   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 1n17

是 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 N1018,M5×104,数据组数为不超过 80 80 80 组。

构造多项式 A A A,若 i ≤ M i\le M iM 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 AAA

  • 因为每个位置都是任取一个不超过 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 1n2×105 个数 a i ( 0 ≤ a i < 2 18 ) a_i(0\le a_i<2^{18}) ai(0ai<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 FFF

那么最大的 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=Fk1F

也就是做 n − 1 n-1 n1 次 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} gigi2
  • 因为我们只需要再取两个相同的数,就至少会跟原来一样。

线性基的角度,

  • 不考虑取几个数,那么要取最大异或和,取出 18 18 18​ 个数即可到达最大异或和。

  • (你可以想一想在线性基上跑最大异或和)

  • 也就是说,再取多余的数,没有任何意义了。

那最多做 18 18 18 次异或卷积,在这之后都让 g i = g i − 2 g_i=g_{i-2} gi=gi2 ,这样是不是对的呢?

  • 答案是:错的,需要做 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=21811,g19=2181

于是,当 i ≤ 19 i\le 19 i19 时我们做异或卷积,当 i > 19 i>19 i>19 时,我们直接让 g i = g i − 2 g_i=g_{i-2} gi=gi2 即可。

时间复杂度为 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 v1v2vn ,其中 ⊕ \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(1T10),表示测试用例的数量。

在每个测试用例中,

第一行包含两个整数 n ( n ≤ 1000 ) n(n \leq 1000) n(n1000) m ( 1 ≤ m ≤ 2 10 ) m(1 \leq m \leq 2^{10}) m(1m210) ,表示树的大小和 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(0vi<m),表示每个顶点的值。

  • 接下来的 n − 1 n-1 n1 行每行包含两个整数 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) (1ai,bin)
  • 保证 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 i1 个合并信息转移了状态,接下来考虑第 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][ij]=f[u][ij]+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 ij 恰好为某个值的两两乘积。

于是我们用 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(1n20,1m105)

随后的 n n n 行,每行一个长度为 m m m 01 01 01 字符串。

输出格式

仅一行一个整数,表示答案。

容易发现 n ≤ 20 n\le 20 n20 很小,而 m ≤ 1 0 5 m\le 10^5 m105 很大。

不难想到枚举 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=02n1c[x]f(xy)
突然发现两个下标的异或,是个固定值 y y y

于是可以变成异或卷积的形式:
∑ i ⊕ j = y c [ i ] f ( j ) \sum_{i\oplus j = y}c[i]f(j) ij=yc[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=0min2n1ij=yc[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(2nn)

故总的时间复杂度为 O ( 2 n ⋅ n + n m ) O(2^n\cdot n+nm) O(2nn+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 本质。

参考

  1. oi-wiki:快速沃尔什变换
  2. 知乎:牛客竞赛:算法笔记|快速沃尔什变换 FWT
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值