(笔记)异或空间线性基

\(\oplus\) 表示按位异或操作。

这是一种较为简单的位运算技巧。

具体来说,向量空间中存在基底线性表示空间内所有向量这一说。这里对于二进制数集,异或操作也有相应的“基底”。

对于每一个集合 \(S\) 都可以找到一个对应的 \(S'\),满足 \(|S'|\) 最小且任意组合异或的集合是等价的。具体怎么找呢?我们利用这种方法:对于一个异或集合二元组 \(\{{a,b}\}\),它和 \(\{a,a\oplus b\}\) 是等价的。对于两个集合,它们的所有组合都是 \(\{a,b,a\oplus b\}\)。我们可以将这个技巧扩展到所有大小的集合。

那么我们就得到了一个方法,可以简化一个二进制数集,那就是对于每个最高位记录一个数 \(p_i\),每次加入新数从高到低扫这个东西。如果不存在直接赋值,存在就异或。这样得到的 \(p\) 就是我们要的“基底”了。

更进一步地,我们还可以用高斯消元法解决这种问题。这样我们就可以得到一个二进制矩阵,它可以辅助我们解决许多问题。具体来说,每次固定一个 bit 的位置(不妨令其为第 \(x\) 位),找到任意一个该位为 \(1\) 的数,将其放在第 \(x\) 行(或 \(n-x\) 行)上,然后利用它消去其他所有数该位上的 \(1\)。最后忽略剩余被消完的数字的 \(0\) 我们就可以得到一组线性基,利用它可以表示原集合的所有异或组合。

以下是用高斯消元法解决P3812 【模板】线性基的一个简单示例。

#include<bits/stdc++.h>
using namespace std;
const int N=55;
int n;
long long a[N];
int main(){
  ios::sync_with_stdio(0);
  cin.tie(0);cout.tie(0);
  cin>>n;
  for(int i=1;i<=n;i++)
    cin>>a[i];
  int pos=0;
  for(int i=49;i>=0;i--){
    int id=0;
    for(int j=pos+1;j<=n;j++)
      if((a[j]>>i)&1){id=j;break;}
    if(id){
      swap(a[++pos],a[id]);
      for(int j=1;j<=n;j++)
        if(((a[j]>>i)&1)&&(j!=pos))a[j]^=a[pos];
    }
  }
  long long ans=0;
  for(int i=1;i<=pos;i++)ans^=a[i];
  cout<<ans;
  return 0;
}

当然我们也可以直接维护每一位上的主元 \(p_i\)(就相当于高斯消元矩阵中的第 \(i\) 行),这样是支持动态加入的,更适用于某些需要随时加入即刻求出异或最大值的情景,加入一个数 \(x\) 也是从高到低位扫,如果该位为 \(1\)\(p_i\) 存在,利用 \(p_i\) 消掉 \(x\) 该位上的 \(1\),否则直接 \(p_i\leftarrow x\)。但是需要注意的是,这跟高斯消元矩阵不是很一样,如果希望方便动态维护的话,每次加入尽量控制在 \(O(bit)\) 级别的,所以不能用新加入的数消去其他数该位为 \(1\) 的地方,也就不是一个标准的高斯消元矩阵。统计答案,相应地不能全部异或起来,而是需要贪心地先取最高位,从高到低考虑,如果该位还没有再异或上该行的主元。观察插入的过程我们不难发现,和高斯消元一样,低位的主元在相应的高位上是没有值的,因此异或只会使答案更大。

#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N=55,M=51;
int n,m;
LL a[N],p[N],ans;
void ins(LL x){
	for(int i=M;i>=0;i--){
		if(x&(1ll<<i)){
			if(!p[i]){p[i]=x;return ;}
			else x^=p[i];
		}
	}
}
int main(){
	scanf("%d",&n);
	for(int i=1;i<=n;i++)
		scanf("%lld",&a[i]),ins(a[i]);
	for(int i=M;i>=0;i--)
		if(p[i]&&(!((ans>>i)&1)))ans^=p[i];
	printf("%lld",ans);
	return 0;
}

最小异或和

输出高斯阶梯矩阵中的最小数(可能含 0,需要判断)即可。

最大异或和

输出高斯阶梯矩阵中的所有数异或和即可。

\(k\) 大/小异或和

先进行高斯消元,然后用二进制表示 \(k\),映射到每行数选或不选即可。

P4869 albus就是要第一个出场

#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N=1e5+5;
const LL MOD=10086;
int n,m,id[N],a[N];
LL pw2[N],ans;
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0);cout.tie(0);
	cin>>m;n=30;
	for(int i=1;i<=m;i++)
		cin>>a[i];
	pw2[0]=1;
	for(int i=1;i<=m;i++)
		pw2[i]=pw2[i-1]*2%MOD;
	int cnt=0;
	for(int i=n;i>=0;i--){
		int pos=0;
		for(int j=cnt+1;j<=m;j++){
			if((a[j]>>i)&1){
				pos=j;
				break;
			}
		}
		if(!pos)continue;
		cnt++;
		swap(a[pos],a[cnt]);
		id[cnt]=i;
		for(int j=1;j<=m;j++)
			if(j!=cnt&&((a[j]>>i)&1))
				a[j]^=a[cnt];
	}
	int Q;cin>>Q;
	for(int i=1;i<=m;i++){
		if((Q>>id[i])&1){
			Q^=a[i];
			ans=(ans+pw2[m-i])%MOD;
		}
	}
	cout<<ans+1;
	return 0;
}

最大XOR和路径

P4151 [WC2011] 最大XOR和路径

这类问题的特点是可以走重边重点,要求一个点到另一个点的最大异或路径。结论是我们可以直接随便找一棵生成树,其余边分别与树构成环。把每个环的异或和算出来,丢进线性基里。可以证明,只要当前图是联通的,必然存在一种方法使得走所需的路径时可经过任意一条环,然后答案中必然先选(初始化为) \(x\to y\) 路径的异或和,直接在线性基上求异或最大值可。

#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N=1e5+5;
int n,m,fa[N];
int head[N],idx,cnth;
LL pf[65],srt[N];
void insert(LL x){
	for(int i=63;i>=0;i--){
		if((x>>i)&1){
			if(pf[i])x^=pf[i];
			else {pf[i]=x;return ;}
		}
	}
}
struct Edge{int v,next;LL w;}e[N<<1],E[N];
int fr(int x){return fa[x]==x?x:fa[x]=fr(fa[x]);}
bool ins(int x,int y){
	int frx=fr(x),fry=fr(y);
	if(frx==fry)return 0;
	fa[frx]=fry;return 1;
}
void linkedge(int x,int y,LL z){
	e[++idx].v=y;
	e[idx].next=head[x];
	e[idx].w=z;
	head[x]=idx;
}
void dfs(int u,int fa){
	for(int i=head[u];i;i=e[i].next){
		int v=e[i].v;
		if(v==fa)continue;
		LL w=e[i].w;
		srt[v]=srt[u]^w;
		dfs(v,u);
	}
}
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0);cout.tie(0);
	cin>>n>>m;
	for(int i=1;i<=n;i++)fa[i]=i;
	for(int i=1;i<=m;i++){
		int u,v;LL w;cin>>u>>v>>w;
		if(ins(u,v)){
			linkedge(u,v,w);
			linkedge(v,u,w);
		}
		else E[++cnth]=(Edge){u,v,w};
	}
	dfs(1,0);
	for(int i=1;i<=cnth;i++)
		insert(srt[E[i].v]^srt[E[i].next]^E[i].w);
	LL ans=srt[n];
	for(int i=63;i>=0;i--)
		if(!((ans>>i)&1))
			ans^=pf[i];
	cout<<ans;
	return 0;
}
posted @ 2025-04-23 17:19  TBSF_0207  阅读(65)  评论(0)    收藏  举报