avatar☀️
Number10Blog

Andrew N. Tang

Would I were the flowing river, That I might meet thee once more.

快速沃尔什变换

简介 ​

快速沃尔什变换用于解决位运算卷积问题,此类问题形如如下形式:

对于给定的长度为 2n2^{n} 的数组 aa 和 bb,求一个长度为 2n2^{n} 的数组 cc,满足

ci=∑i=j⊕kajbkc_{i}=\sum_{i=j\oplus k}a_{j}b_{k}

其中 ⊕\oplus 是一种位运算,例如按位与,按位或,按位异或。

直接朴素计算的时间复杂度为 O(4n)O(4^n),使用快速沃尔什变换可以在 O(n2n)O(n2^n) 的时间复杂的内完成。

具体来说,考虑类似于 FFT 的思想,分为 3 步:

  1. 将 aa 和 bb 数组进行沃尔什变换,得到数组 AA 和 BB。

  2. 将数组 AA 和 BB 对位相乘,得到数组 CC。

  3. 将 CC 进行沃尔什逆变换,得到数组 cc。

按位或变换 ​

当运算为按位或运算时,可以设 Ai=∑i∣j=iajA_i=\sum_{i\mid j=i}a_j,根据 i∣j=ii\mid j=i 且 i∣k=ii\mid k=i 等价于 (j∣k)∣i=i(j\mid k)\mid i=i 可以推导出:

AiBi=∑i∣j=iaj∑i∣k=ibk=∑i∣j=i∑i∣k=iajbk=∑(j∣k)∣i=iajbk=Ci\begin{aligned} A_iB_i&=\sum_{i\mid j=i}a_j\sum_{i\mid k=i}b_k\\ &=\sum_{i\mid j=i}\sum_{i\mid k=i}a_jb_k\\ &=\sum_{(j\mid k)\mid i=i}a_j b_k\\ &=C_i \end{aligned}

给定 aa 求 AA 可以对下标按位考虑。上代码:

c++
void orTrans(LL *a,LL type){
  for(int l=2;l<=n;l<<=1){ // 枚举考虑到第几个二进制位
    int k=l>>1;
    for(int i=0;i<n;i+=l){
      for(int j=0;j<k;++j){ // 枚举该二进制位及更高位的值为 i+j
        (a[i+j+k]+=a[i+j]*type)%=mod;
        // 那么比枚举的位更低的位,若为 1,则可以合并原来为 0 的位
      }
    }
  }
}

逆变换时带入 type 为 -1 即可。

按位与变换 ​

同理或运算,类比可得,设 Ai=∑i&j=iajA_i=\sum_{i\& j=i}a_j,类似地得到变换方法:

c++
void andTrans(LL *a,LL type){
  for(int l=2;l<=n;l<<=1){
    int k=l>>1;
    for(int i=0;i<n;i+=l){
      for(int j=0;j<k;++j){
        (a[i+j]+=a[i+j+k]*type)%=mod;
      }
    }
  }
}

按位异或运算 ​

定义运算 ∘\circ 为 a∘b=popcnt(a&b) mod 2a\circ b=\text{popcnt}(a\& b)\bmod 2,⊕\oplus 为按位异或运算。

设 Ai=∑i∘j=0aj−∑i∘j=1ajA_i=\sum_{i\circ j=0}a_j-\sum_{i\circ j=1}a_j,可以验证,仍然满足上述条件。

具体来说,∘\circ 运算具有性质:(i∘j)⊕(i∘k)=i∘(j⊕k)(i\circ j)\oplus(i\circ k)=i\circ(j\oplus k)。

证明考虑 ∘\circ 运算的意义:aa 与 bb 二进制下同为 11 的位数的奇偶性。分类讨论:考虑某一个二进制位,

  • 若 jj 和 kk 这一位均为 11,那么 jj 与 kk 异或后这一位为 00,故右式中 ii 与 j⊕jj\oplus j 不会同时为 11,贡献为 00。

    考虑左式,若 ii 这一位为 00,则两者均为 00,不产生贡献;若 ii 这一位为 11,左式中 i∘ji\circ j 和 i∘ki\circ k 都会因为多了一个同为 11 的位数而奇偶性改变,异或结果为 00,也不改变。

其它情况分类讨论,同理可得。

c++
void xorTrans(LL *a,LL type){
  for(int l=2;l<=n;l<<=1){
    int k=l>>1;
    for(int i=0;i<n;i+=l){
      for(int j=0;j<k;++j){
        (a[i+j]+=a[i+j+k])%=mod;
        a[i+j+k]=(a[i+j]+mod-a[i+j+k]+mod-a[i+j+k])%mod;
        (a[i+j]*=type)%=mod;
        (a[i+j+k]*=type)%=mod;
        if(a[i+j+k]<0)a[i+j+k]+=mod;
      }
    }
  }
}

逆变换时 type=12\frac{1}{2}。

下面给出 洛谷模板题 的代码

Sample Code(C++)
c++
#include<bits/stdc++.h>
using namespace std;
using LL=long long ;
const int N=1.4e5,mod=998244353;
int n;
void orTrans(LL *a,LL type){
  for(int l=2;l<=n;l<<=1){
    int k=l>>1;
    for(int i=0;i<n;i+=l){
      for(int j=0;j<k;++j){
        (a[i+j+k]+=a[i+j]*type)%=mod;
      }
    }
  }
}
void andTrans(LL *a,LL type){
  for(int l=2;l<=n;l<<=1){
    int k=l>>1;
    for(int i=0;i<n;i+=l){
      for(int j=0;j<k;++j){
        (a[i+j]+=a[i+j+k]*type)%=mod;
      }
    }
  }
}
void xorTrans(LL *a,LL type){
  for(int l=2;l<=n;l<<=1){
    int k=l>>1;
    for(int i=0;i<n;i+=l){
      for(int j=0;j<k;++j){
        (a[i+j]+=a[i+j+k])%=mod;
        a[i+j+k]=(a[i+j]+mod-a[i+j+k]+mod-a[i+j+k])%mod;
        (a[i+j]*=type)%=mod;
        (a[i+j+k]*=type)%=mod;
        if(a[i+j+k]<0)a[i+j+k]+=mod;
      }
    }
  }
}
void cpy(LL *a,LL *b){
  for(int i=0;i<n;++i)a[i]=b[i];
}
void print(LL *a){
  for(int i=0;i<n;++i)printf("%lld ",a[i]);
  putchar(10);
}
LL a[N],b[N];
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]);
  static LL A[N],B[N];
  cpy(A,a);
  cpy(B,b);
  orTrans(A,1);
  orTrans(B,1);
  for(int i=0;i<n;++i)A[i]=A[i]*B[i]%mod;
  orTrans(A,mod-1);
  print(A);

  cpy(A,a);
  cpy(B,b);
  andTrans(A,1);
  andTrans(B,1);
  for(int i=0;i<n;++i)A[i]=A[i]*B[i]%mod;
  andTrans(A,mod-1);
  print(A);

  cpy(A,a);
  cpy(B,b);
  xorTrans(A,1);
  xorTrans(B,1);
  for(int i=0;i<n;++i)A[i]=A[i]*B[i]%mod;
  xorTrans(A,499122177);
  print(A);

  return 0;
}
拓展中国剩余定理
多项式基本运算