位运算卷积学习笔记
位运算卷积学习笔记
位运算卷积,即快速沃尔什变换 \(\text{FWT}\) 和快速莫比乌斯变换 \(\text{FMT}\),但事实上最常用的是 \(\text{FWT}\),因为 \(\text{FMT}\) 所求解的内容是 \(\text{FWT}\) 的子集。
位运算卷积
首先要知道位运算卷积指的是
形式的式子,其中 \(\odot\) 代指了任意位运算符,也就是 \(\text{or},\text{and},\text{xor}\)。利用 \(\text{FMT}\) 能够求解 \(\text{or},\text{and}\) 卷积,而利用 \(\text{FWT}\) 能够求解 \(\text{or},\text{and},\text{xor}\) 卷积。
快速莫比乌斯变换
\(\text{or}\) 卷积
先考虑暴力求解的时间复杂度是 \(O(n^2)\) 的,不妨考虑和 \(\text{FFT}\) 相同的思路,找到一个 \(\text{FMT}(a)\) 满足 \(\text{FMT}(c)_i=\text{FMT}(a)_i\times\text{FMT}(b)_i\),这样我们便可以通过顺逆变换快速求出 \(c\)。
定义 \(\text{FMT}(a)_i=\sum_{j\cup i=i}a_j\),其中 \(\cup\) 表示按位或,而这种构造显然符合要求,因为有
由此我们得到了一个构造,而且不难发现这其实就是 \(a\) 的高维前缀和,按照前缀和的思路将 \(n\) 维二进制看作 \(n\) 维数组求解前缀和即可。而对于逆变换只需要进行差分即可。时间复杂度是 \(O(n\log n)\) 的。
代码
int n;
int A[1<<n],B[1<<n],C[1<<n];
void FMT_OR(int n,int *a,int type){
for(int i=0;i<n;i++){
for(int j=0;j<(1<<n);j++){
if(j&(1<<i))a[j]+=type*a[j^(1<<i)];
}
}
return ;
}
int main(){
cin>>n;
for(int i=0;i<(1<<n);i++)cin>>A[i];
for(int i=0;i<(1<<n);i++)cin>>B[i];
FMT_OR(n,A,1);
FMT_OR(n,B,1);
for(int i=0;i<(1<<n);i++)C[i]=A[i]*B[i];
FMT_OR(n,C,-1);
for(int i=0;i<(1<<n);i++)cout<<C[i]<<" ";
return 0;
}
\(\text{and}\) 卷积
和 \(\text{or}\) 卷积同理,考虑构造一个 \(\text{FMT}(a)_i\),在这里定义 \(\text{FMT}(a)_i=\sum_{j\cap i=i}a_j\),\(\cap\) 表示按位与,那么有
于是考虑如何求解 \(\text{FMT}(a)_i\),发现其为高位后缀和的形式,于是有复杂度 \(O(n\log n)\)。
代码
int n;
int A[1<<n],B[1<<n],C[1<<n];
void FMT_AND(int n,int *a,int type){
for(int i=0;i<n;i++){
for(int j=0;j<(1<<n);j++){
if(j&(1<<i))a[j^(1<<i)]+=type*a[j];
}
}
return ;
}
int main(){
cin>>n;
for(int i=0;i<(1<<n);i++)cin>>A[i];
for(int i=0;i<(1<<n);i++)cin>>B[i];
FMT_AND(n,A,1);
FMT_AND(n,B,1);
for(int i=0;i<(1<<n);i++)C[i]=A[i]*B[i];
FMT_AND(n,C,-1);
for(int i=0;i<(1<<n);i++)cout<<C[i]<<" ";
return 0;
}
快速沃尔什变换
\(\text{or}\) 变换
其实有关于 \(\text{or},\text{and}\) 的 \(\text{FWT}\) 和 \(\text{FMT}\) 的构造完全相同,只是求解的方法不同而已,我们在这里考虑分治的做法,也就是更接近 \(\text{FFT}\) 的做法。依旧构造 \(\text{FWT}(a)_i=\sum_{j\cup i=i}a_j\),正确性不加复述,下面考虑分治。对于分治序列 \(\text{FWT}(a)\) 的第 \(i\) 位,我们将其分为两部分 \(\text{FWT}(a)_0,\text{FWT}(a)_1\),分别表示在 \(i-1\) 位分治后序列左半部分和右半部分,于是有
这个正确性是显然的,因为对于 \(\text{FWT}(a)_0\) 来说,其分治位置上均为 \(0\),所需要累加的对应位置 \(j\) 若要满足 \(j\cup i=i\) 则其当前分治位也需为 \(0\)。而对于 \(\text{FWT}(a)_1\) 来说,\(j\) 的当前分治位即可以为 \(1\),也可以为 \(0\),因此只需要将后半段加上前半段即可。
关于逆变换则恰相反,只需要将前半段对后半段的贡献消去即可,也就是在后半段减去前半段。也就是
代码
int n;
int A[1<<n],B[1<<n],C[1<<n];
void FWT_OR(int n,int *a,int type){
for(int len=1;len<n;len<<=1){
for(int l=0;l<n;l+=(len<<1)){
for(int k=0;k<len;k++)a[l+len+k]+=type*a[l+k];
}
}
return ;
}
int main(){
ios::sync_with_stdio(0);
cin.tie(0);
cin>>n;
for(int i=0;i<(1<<n);i++)cin>>A[i];
for(int i=0;i<(1<<n);i++)cin>>B[i];
FWT_OR(1<<n,A,1);
FWT_OR(1<<n,B,1);
for(int i=0;i<(1<<n);i++)C[i]=A[i]*B[i];
FWT_OR(1<<n,C,-1);
for(int i=0;i<(1<<n);i++)cout<<C[i]<<" ";
return 0;
}
\(\text{and}\) 卷积
依旧同理有 \(\text{FWT}(a)_i=\sum_{j\cap i=i}a_j\),正确性不加复述,考虑分治的做法。对于分治序列 \(\text{FWT}(a)\) 的第 \(i\) 位,我们将其分为两部分 \(\text{FWT}(a)_0,\text{FWT}(a)_1\),分别表示在 \(i-1\) 位分治后序列左半部分和右半部分,于是有
同理,当第 \(i\) 位为 \(0\) 时,\(j\) 的当前分治位即可以是 \(0\) 也可以是 \(1\),而当第 \(i\) 位为 \(1\) 时,\(j\) 的当前分治位只能是 \(1\),逆变换同理减回去即可。
代码
int n;
int A[1<<n],B[1<<n],C[1<<n];
void FWT_AND(int n,int *a,int type){
for(int len=1;len<n;len<<=1){
for(int l=0;l<n;l+=(len<<1)){
for(int k=0;k<len;k++)a[l+k]+=type*a[l+len+k];
}
}
return ;
}
int main(){
ios::sync_with_stdio(0);
cin.tie(0);
cin>>n;
for(int i=0;i<(1<<n);i++)cin>>A[i];
for(int i=0;i<(1<<n);i++)cin>>B[i];
FWT_AND(1<<n,A,1);
FWT_AND(1<<n,B,1);
for(int i=0;i<(1<<n);i++)C[i]=A[i]*B[i];
FWT_AND(1<<n,C,-1);
for(int i=0;i<(1<<n);i++)cout<<C[i]<<" ";
return 0;
}
\(\text{xor}\) 卷积
引入一个新的运算符 \(\circ\)。定义 \(x\circ y=\text{popcnt}(x\cap y)\bmod 2\),其中 \(\text{popcnt}(x)\) 表示 \(x\) 二进制下 \(1\) 的个数。我们发现它满足 \((x\circ y)\oplus(x\circ z)=x\circ (y\oplus z)\)。
更感性的理解,\(x\circ y\) 表示 \(x,y\) 均为 \(1\) 位数的奇偶性。对于每一位的 \(x,y,z\),可以列出下表
\(x\) | \(y\) | \(z\) | \(y\oplus z\) | \(x\circ y\) | \(x\circ z\) | \(x\circ (y\oplus z)\) |
---|---|---|---|---|---|---|
\(0\) | \(0\) | \(0\) | \(0\) | \(0\) | \(0\) | \(0\) |
\(0\) | \(0\) | \(1\) | \(1\) | \(0\) | \(0\) | \(0\) |
\(0\) | \(1\) | \(0\) | \(1\) | \(0\) | \(0\) | \(0\) |
\(0\) | \(1\) | \(1\) | \(0\) | \(0\) | \(0\) | \(0\) |
\(1\) | \(0\) | \(0\) | \(0\) | \(0\) | \(0\) | \(0\) |
\(1\) | \(0\) | \(1\) | \(1\) | \(0\) | \(1\) | \(1\) |
\(1\) | \(1\) | \(0\) | \(1\) | \(1\) | \(0\) | \(1\) |
\(1\) | \(1\) | \(1\) | \(0\) | \(1\) | \(1\) | \(0\) |
不难看出,不管 \(x,y,z\) 的每一位如何变化,对应位对答案贡献的奇偶性始终不变,故上式成立。
由此,设 \(\text{FWT}(a)_i=\sum_{j\circ i=0}a_j-\sum_{j\circ i=1}a_j\)。则有
由此,该构造的正确性得证,接着考虑如何分治求解。当前位 \(i\) 为 \(0\) 时,不论 \(j\) 取 \(0,1\),\(i\circ j\) 均为 \(0\),那么 \(\text{FWT}(a)_0,\text{FWT}(a)_1\) 对该位的贡献均为正,而当 \(i\) 为 \(1\) 时,如果 \(j\) 取 \(0\),则对答案没有影响,但若取 \(1\),则应对原贡献取负。即
而在逆变换时有
于是对于传参稍作改动,逆变换时 \(\text{type}\) 取 \(\frac{1}{2}\)。
代码
int n;
int A[1<<n],B[1<<n],C[1<<n];
void FWT_XOR(int n,int *a,int type){
for(int len=1;len<n;len<<=1){
for(int l=0;l<n;l+=(len<<1)){
for(int k=0;k<len;k++){
ll A=a[l+k],B=a[l+len+k];
a[l+k]=(A+B)/type;
a[l+len+k]=(A-B)/type;
}
}
}
return ;
}
int main(){
ios::sync_with_stdio(0);
cin.tie(0);
cin>>n;
for(int i=0;i<(1<<n);i++)cin>>A[i];
for(int i=0;i<(1<<n);i++)cin>>B[i];
FWT_XOR(1<<n,A,1);
FWT_XOR(1<<n,B,1);
for(int i=0;i<(1<<n);i++)C[i]=A[i]*B[i];
FWT_XOR(1<<n,C,2);
for(int i=0;i<(1<<n);i++)cout<<C[i]<<" ";
return 0;
}