(笔记)异或空间线性基
\(\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\),映射到每行数选或不选即可。
#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和路径
这类问题的特点是可以走重边重点,要求一个点到另一个点的最大异或路径。结论是我们可以直接随便找一棵生成树,其余边分别与树构成环。把每个环的异或和算出来,丢进线性基里。可以证明,只要当前图是联通的,必然存在一种方法使得走所需的路径时可经过任意一条环,然后答案中必然先选(初始化为) \(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;
}

浙公网安备 33010602011771号