位运算卷积学习笔记

位运算卷积学习笔记

位运算卷积,即快速沃尔什变换 FWT 和快速莫比乌斯变换 FMT,但事实上最常用的是 FWT,因为 FMT 所求解的内容是 FWT 的子集。

位运算卷积

首先要知道位运算卷积指的是

ci=jk=iajbk

形式的式子,其中 代指了任意位运算符,也就是 or,and,xor。利用 FMT 能够求解 or,and 卷积,而利用 FWT 能够求解 or,and,xor 卷积。

快速莫比乌斯变换

or 卷积

先考虑暴力求解的时间复杂度是 O(n2) 的,不妨考虑和 FFT 相同的思路,找到一个 FMT(a) 满足 FMT(c)i=FMT(a)i×FMT(b)i,这样我们便可以通过顺逆变换快速求出 c

定义 FMT(a)i=ji=iaj,其中 表示按位或,而这种构造显然符合要求,因为有

FMT(a)i×FMT(b)i=ji=iajki=ibk=(jk)i=iajbk=FMT(c)i

由此我们得到了一个构造,而且不难发现这其实就是 a 的高维前缀和,按照前缀和的思路将 n 维二进制看作 n 维数组求解前缀和即可。而对于逆变换只需要进行差分即可。时间复杂度是 O(nlogn) 的。

代码

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;
}

and 卷积

or 卷积同理,考虑构造一个 FMT(a)i,在这里定义 FMT(a)i=ji=iaj 表示按位与,那么有

FMT(a)i×FMT(b)i=ji=iajki=ibk=(jk)i=iajbk=FMT(c)i

于是考虑如何求解 FMT(a)i,发现其为高位后缀和的形式,于是有复杂度 O(nlogn)

代码

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;
}

快速沃尔什变换

or 变换

其实有关于 or,andFWTFMT 的构造完全相同,只是求解的方法不同而已,我们在这里考虑分治的做法,也就是更接近 FFT 的做法。依旧构造 FWT(a)i=ji=iaj,正确性不加复述,下面考虑分治。对于分治序列 FWT(a) 的第 i 位,我们将其分为两部分 FWT(a)0,FWT(a)1,分别表示在 i1 位分治后序列左半部分和右半部分,于是有

FWT(a)=merge(FWT(a)0,FWT(a)0+FWT(a)1)

这个正确性是显然的,因为对于 FWT(a)0 来说,其分治位置上均为 0,所需要累加的对应位置 j 若要满足 ji=i 则其当前分治位也需为 0。而对于 FWT(a)1 来说,j 的当前分治位即可以为 1,也可以为 0,因此只需要将后半段加上前半段即可。

关于逆变换则恰相反,只需要将前半段对后半段的贡献消去即可,也就是在后半段减去前半段。也就是

a=merge(a0,a1a0)

代码

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;
}

and 卷积

依旧同理有 FWT(a)i=ji=iaj,正确性不加复述,考虑分治的做法。对于分治序列 FWT(a) 的第 i 位,我们将其分为两部分 FWT(a)0,FWT(a)1,分别表示在 i1 位分治后序列左半部分和右半部分,于是有

FWT(a)=merge(FWT(a)0+FWT(a)1,FWT(a)1)

同理,当第 i 位为 0 时,j 的当前分治位即可以是 0 也可以是 1,而当第 i 位为 1 时,j 的当前分治位只能是 1,逆变换同理减回去即可。

a=merge(a0a1,a1)

代码

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;
}

xor 卷积

引入一个新的运算符 。定义 xy=popcnt(xy)mod2,其中 popcnt(x) 表示 x 二进制下 1 的个数。我们发现它满足 (xy)(xz)=x(yz)

更感性的理解,xy 表示 x,y 均为 1 位数的奇偶性。对于每一位的 x,y,z,可以列出下表

x y z yz xy xz x(yz)
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 的每一位如何变化,对应位对答案贡献的奇偶性始终不变,故上式成立。

由此,设 FWT(a)i=ji=0ajji=1aj。则有

FWT(a)i×FWT(b)i=(ji=0ajji=1aj)(ji=0bjji=1bj)=(ji=0ajki=0bk+ji=1ajki=1bk)(ji=0ajki=1bk+ji=1ajki=0bk)=(jk)i=0ajbk(jk)i=1ajbk=FWT(c)i

由此,该构造的正确性得证,接着考虑如何分治求解。当前位 i0 时,不论 j0,1ij 均为 0,那么 FWT(a)0,FWT(a)1 对该位的贡献均为正,而当 i1 时,如果 j0,则对答案没有影响,但若取 1,则应对原贡献取负。即

FWT(a)=merge(FWT(a)0+FWT(a)1,FWT(a)0FWT(a)1)

而在逆变换时有

a=merge(a0+a12,a0a12)

于是对于传参稍作改动,逆变换时 type12

代码

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;
}

拓展知识

矩阵角度的 FWT

假设 c(i,j)ajFWT(a)i 的贡献系数,我们可以重新描述 FWT 变换的过程

FWT(a)i=j=0n1c(i,j)aj

因为 FWT(a)i×FWT(b)i=FWT(c)i,所以有

FWT(a)i×FWT(b)i=j=0n1c(i,j)ajk=0n1c(i,k)bk=j=0n1k=0n1c(i,j)c(i,k)ajbk=jk=0n1c(i,jk)ajbk=FWT(c)i

所以应当有 c(i,j)c(i,k)=c(i,jk)。同时 c 函数还应当能够按位处理。也就是我们将变换的形式

FWT(a)i=j=0n1c(i,j)aj

拆分成

FWT(a)i=j=0n21c(i,j)aj+j=n2n1c(i,j)aj

考虑到式子分出的 j 只有最高位不同,所以我们将 i,j 出去最高位的值为 i,j,并且记 i0i 的最高位,则有

FWT(a)i=c(i0,0)j=0n21c(i,j)aj+c(i0,1)j=n2n1c(i,j)aj

i0=0 时,有

FWT(a)i=c(0,0)j=0n21c(i,j)aj+c(0,1)j=n2n1c(i,j)aj

而当 i0=1 时,则有

FWT(a)i=c(1,0)j=0n21c(i,j)aj+c(1,1)j=n2n1c(i,j)aj

不难考虑分治维护 j=0n21c(i,j)ajj=n2n1c(i,j)aj,于是转移就可以通过矩阵

[c(0,0)c(0,1)c(1,0)c(1,1)]

进行,我们称这个矩阵为位矩阵。而如果需要进行逆变换,则需要上面位矩阵的逆矩阵。即可以通过

ai=j=0n1c1(i,j)FWT(a)j

得到原序列。当然,逆矩阵不一定存在。当原矩阵存在一行或一列都是 0 时则该矩阵没有逆,需要在构造的时候注意。

or 卷积

不难构造出矩阵

[1011]

满足 c(i,j)c(i,k)=c(i,jk),不难发现,这等价于 FWT(a)=merge(FWT(a)0,FWT(a)0+FWT(a)1)。而下面这个矩阵虽然也符合条件,但并不常见

[1110]

而下面的矩阵虽然也符合 c(i,j)c(i,k)=c(i,jk),但其没有逆,因此不合法

[0011]

考虑最上方的矩阵的逆

[1011]

这和逆变换的 a=merge(a0,a1a0) 等价。

and 卷积

同理构造

[1101]

满足 c(i,j)c(i,k)=c(i,jk),其逆矩阵为

[1101]

xor 卷积

构造

[1111]

满足 c(i,j)c(i,k)=c(i,jk),其逆矩阵为

[12121212]

FWT 的性质

FWT 是线性变换,这意味着

FWT(A+B)=FWT(A)+FWT(B)FWT(cA)=cFWT(A)

KFWT

max 运算

重新审视 运算,发现它实际上是二进制下每一位取 max。如果将其拓展到 K 进制,有

c(i,j)c(i,k)=c(i,max(j,k))

j=k 时,上式转换成 c(i,j)2=c(i,j),于是每一位只能取 1,0,而每一行的 1 必须出现在 0 前,不然不符合要求,不难构造出

[1000110011101111]

其逆矩阵为

[1000110001100001]

min 运算

重新审视 运算,发现它实际上是二进制下每一位取 min。如果将其拓展到 K 进制,有

c(i,j)c(i,k)=c(i,min(j,k))

j=k 时,上式转换成 c(i,j)2=c(i,j),于是每一位只能取 1,0,而每一行 1 后面不能有 0,于是不难构造

[1111011100110001]

其逆矩阵为

[1100011000100001]

posted @   DycIsMyName  阅读(34)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律
点击右上角即可分享
微信分享提示