线性基的双重记录机制:最高位映射与方案还原
核心机制设计
线性基的精妙之处在于它建立了两个层次的映射关系:
- 位层次映射:每个二进制位对应一个基向量
- 元素层次映射:每个基向量记录其构成来源
具体实现细节
1. 基于最高位的索引机制
cpp
int bas[31]; // bas[j]:最高位1在位置j的基向量
unsigned long long msk[31]; // msk[j]:这个基向量的构成掩码
int idd[70]; // 原始元素的原始下标
int cnt = 0; // 已插入的原始元素计数
为什么按最高位索引?
- 保证每个基向量线性独立(最高位1在不同位置)
- 方便快速判断和消元(从高位到低位处理)
2. 插入过程的双重记录
cpp
for(int i=1; i<=n; i++){
int v = a[i]^b[i];
if(v == 0) continue;
int cur = v;
unsigned long long curm = 1ull << cnt; // 当前元素标识
for(int j=30; j>=0; j--){
if((cur >> j) & 1){
if(!bas[j]){
// 第一层记录:位->基向量
bas[j] = cur;
// 第二层记录:基向量->原始元素组合
msk[j] = curm;
idd[cnt] = i; // 原始下标
cnt++;
break;
}
// 消元:保证线性独立性
cur ^= bas[j];
curm ^= msk[j]; // 关键:同步更新元素组合
}
}
}
关键点:
curm初始为1<<cnt,唯一标识当前元素- 每次异或基向量时,
curm也异或对应的msk[j] - 最终
msk[j]记录了基向量由哪些原始元素异或而成
方案还原的数学原理
3. 目标向量的表示
cpp
int cur = X;
unsigned long long pick = 0;
for(int j=30; j>=0; j--){
if((cur >> j) & 1){
if(!bas[j]) return; // 无解
cur ^= bas[j];
pick ^= msk[j]; // 记录使用了哪些基向量
}
}
数学解释 :
我们有:X = bas[j1] ⊕ bas[j2] ⊕ ... ⊕ bas[jk]
其中每个 bas[j] = d[i1] ⊕ d[i2] ⊕ ... ⊕ d[im]
所以:X = (d[...]的组合)
pick 记录了需要哪些 bas[j],进而知道需要哪些 d[i]
4. 从位掩码到原始选择
cpp
// pick的每一位表示是否使用第i个原始元素
for(int i=0; i<cnt; i++){
if((pick >> i) & 1){
use[idd[i]] = 1; // idd[i]是原始数组下标
}
}
// 输出结果
for(int i=1; i<=n; i++){
cout << (use[i] ? b[i] : a[i]) << " ";
}
一个数值例子
假设:
d[1] = 5 (101)d[2] = 3 (011)d[3] = 6 (110)X = 4 (100)
构建过程:
插入 d[1]=101:
cur=101, curm=001 (第0位)
j=2: (101>>2)&1=1, bas[2]空
bas[2]=101, msk[2]=001, idd[0]=1, cnt=1
插入 d[2]=011:
cur=011, curm=010 (第1位)
j=1: (011>>1)&1=1, bas[1]空
bas[1]=011, msk[1]=010, idd[1]=2, cnt=2
插入 d[3]=110:
cur=110, curm=100 (第2位)
j=2: (110>>2)&1=1, bas[2]=101存在
cur=110^101=011, curm=100^001=101
j=1: (011>>1)&1=1, bas[1]=011存在
cur=011^011=000, curm=101^010=111
插入失败,但记录了:110 = 101 ⊕ 011
求解 X=100:
cur=100, pick=0
j=2: (100>>2)&1=1, bas[2]=101存在
cur=100^101=001, pick=0^001=001
j=1: (001>>1)&1=0
j=0: (001>>0)&1=1, bas[0]空 -> 无解?等等...
正确应该用bas[1]消元:
j=2: 100需要位2为1,但100^101=001
j=1: 001的位1为0,跳过
j=0: 001的位0为1,bas[0]空 -> 无解
发现无解:X=100 不能被表示
正确性证明要点
-
线性基性质:
- 基向量线性无关
- 能表示所有原始向量可表示的空间
-
msk记录的正确性:
- 初始:
curm唯一标识当前向量 - 每次
cur ^= bas[j],同时curm ^= msk[j] - 最终有:
cur = v ⊕ (某些bas[j]) - 对应:
curm = (1<<cnt) ⊕ (对应msk[j]) - 所以
cur = 0时,curm记录了v的表示方式
- 初始:
-
方案还原的正确性:
pick记录了表示X所需的基向量- 每个基向量的
msk记录了它的原始构成 - 因此
pick中为1的位对应的原始元素就是解
算法优势
- 高效性:O(n log MAX) 时间,O(log MAX) 空间
- 完全性:不仅能判断存在性,还能输出具体方案
- 通用性:可解决各类异或子集问题
总结
这种基于最高位索引的双重记录机制是线性基算法的核心创新:
- 外层:按最高位组织基向量,保证线性独立性
- 内层:记录每个基向量的"生成历史",支持方案还原
- 两者结合,使得算法既高效又功能完整
理解这一机制,就掌握了线性基解决异或子集问题的关键。
代码
cpp
#include <bits/stdc++.h>
using namespace std;
using ll=long long;
int a[202020],b[202020];
int bas[35];
unsigned long long msk[35];
int idd[70];
char use[202020];
void so(){
int n,i,j;int X=0;
cin>>n;for(int i=1;i<=n;i++){
cin>>a[i];X^=a[i];
}for(int i=1;i<=n;i++){
cin>>b[i];
}if(X==0){for(int i=1;i<=n;i++){
cout<<a[i]<<(i==n?'\n':' ');
} return ;}
//构建线性基
for(int i=0;i<=30;i++){
bas[i]=0;msk[i]=0;
}int cnt=0;
for(int i=1;i<=n;i++){
int v=a[i]^b[i];
if(!v)continue;
int cur=v;
unsigned long long curm=1ull<<cnt;//为d[i]分配二进制掩码
//cnt最多为30
for(int j=30;j>=0;j--){
if(((cur>>j)&1)==0)continue;
if(!bas[j]){//给出每位变1的方案
bas[j]=cur;
msk[j]=curm;
idd[cnt]=i;
cnt++;break;
}cur^=bas[j];curm^=msk[j];//用了后变成0
}
}
unsigned long long pick=0;
int cur=X;
for(int j=30;j>=0;j--){
if((cur>>j)&1){//需消去
if(!bas[j]){cout<<-1<<'\n';return;}
cur^=bas[j];//消去第j位
pick^=msk[j];//记录基
}
}for(int i=1;i<=n;i++){
use[i]=0;
}for(int i=0;i<cnt;i++){
if((pick>>i)&1)use[idd[i]]=1;
}for(int i=1;i<=n;i++){
cout<<(use[i]?b[i]:a[i])<<(i==n?'\n':' ');
}
}int main(){
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
int t;cin>>t;
while(t--){so();}
}
题
1 P3812 【模板】线性基
https://www.luogu.com.cn/problem/P3812
cpp
#include <bits/stdc++.h>
using namespace std;
using ll=long long;
const int MN=60;
ll a[61],tmp[61];
bool f;
void ins(ll x){
for(int i=MN;~i;i--){//~i:i>=0,因为-1的补码是1111
if(x&(1ll<<i))
if(!a[i]){a[i]=x;return;}
else x^=a[i];
f=true;
}
}
ll qmax(ll res=0){
for(int i=MN;~i;i--){
res=max(res,res^a[i]);
}return res;
}
int main(){
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
int n;ll x;cin>>n;
for(int i=1;i<=n;i++){
cin>>x;ins(x);
}cout<<qmax();
}
2 P3857 [TJOI2008] 彩灯
https://www.luogu.com.cn/problem/P3857
cpp
#include <bits/stdc++.h>
using namespace std;
using ll=long long;
const int N=51,mod=2008;
int cnt;
ll arr[N];
void ins(ll box){
for(int i=50;~i;i--){
if(box>>i&1)
if(!arr[i]){++cnt;arr[i]=box;return;}
else box^=arr[i];
}
}
int main(){
int n,m; scanf("%d%d",&n,&m);char c;
for(int i=1;i<=m;i++){
ll x=0;
char s[N];
scanf("%s", s);
for(int j=0;j<n;j++){
if(s[j]=='O')x|=(1ll<<j);
}ins(x);
}printf("%lld\n",(1ll<<cnt)%mod);
}
3 P4301 [CQOI2013] 新Nim游戏
https://www.luogu.com.cn/problem/P4301
cpp
#include <bits/stdc++.h>
using namespace std;
using ll=long long;
int n,a[101],d[31];ll ans;
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];
}sort(a+1,a+1+n);
for(int x=a[n];n;--n,x=a[n]){
for(int j=30;~j;j--){
if((x>>j)&1)
if(d[j])x^=d[j];
else{d[j]=x;break;}
}if(!x)ans+=a[n];
//没成功插入,意味着最终异或和为0
//所以小的放最后考虑
//可以由其他的表示会让最终的异或和为零。
//因为后手也可以拿
}cout<<ans;
}
4 P3292 [SCOI2016] 幸运数字
https://www.luogu.com.cn/problem/P3292
cpp
#include <bits/stdc++.h>
using namespace std;
using ll=long long;
const int N=20002;
const int M=N<<1;
const int lg=15;
int n,q;
ll a[N];
int cnt,head[N],nex[M],v[M];
void add(int x,int y){
nex[++cnt]=head[x];head[x]=cnt;v[cnt]=y;
}
int dep[N],f[N][lg];
ll bas[N][61];
int pos[N][61];
void ins(int p,ll bas[],int pos[]){//p:当前插入的节点编号
//bas:要插入的数组,pos:线性基对应的节点的位置数组
ll x=a[p];
if(x==0)return;
for(int i=60;~i;i--){
if((x>>i)&1){
//为空,直接插
if(!bas[i]){
bas[i]=x;pos[i]=p;//记录节点位置
break;
}//树上优化
if(dep[p]>dep[pos[i]]){
swap(pos[i],p);swap(x,bas[i]);
}x^=bas[i];
}
}
}
void dfs(int x,int fa){
dep[x]=dep[fa]+1;
f[x][0]=fa;
for(int i=1;i<lg;i++){
f[x][i]=f[f[x][i-1]][i-1];
}//继承父节点的线性基和位置
for(int i=0;i<=60;i++){
pos[x][i]=pos[fa][i],bas[x][i]=bas[fa][i];
}//当前节点插入线性基
ins(x,bas[x],pos[x]);
//遍历当前节点的领边
for(int i=head[x];i;i=nex[i]){
if(v[i]!=fa)dfs(v[i],x);
}
}
int getLCA(int x,int y){
if(dep[x]<dep[y])swap(x,y);
for(int i=lg-1;i>=0;i--){
if(dep[f[x][i]]>=dep[y])x=f[x][i];
}if(x==y)return x;
for(int i=lg-1;i>=0;i--){
if(f[x][i]!=f[y][i]){x=f[x][i];y=f[y][i];}
}return f[x][0];
}
ll base[61];
int main(){
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
cin>>n>>q;
for(int i=1;i<=n;i++){
cin>>a[i];
}int x,y;
for(int i=2;i<=n;i++){
cin>>x>>y;
add(x,y);add(y,x);
}dfs(1,0);
while(q--){
cin>>x>>y;
int lca=getLCA(x,y);
for(int i=60;i>=0;i--){
if(dep[pos[x][i]]>=dep[lca]){
base[i]=bas[x][i];//在路径上
}else base[i]=0;
}
for(int i=60;i>=0;i--){//插入y
if(dep[pos[y][i]]>=dep[lca]){
ll x=bas[y][i];
if(x==0)continue;
for(int j=i;j>=0;j--){
if((x>>j)&1){
if(!base[j]){base[j]=x;break;}
x^=base[j];
}
}
}
}ll ans=0;
for(int i=60;i>=0;i--){//如果异或后更大,就异或
if((ans^base[i])>ans)ans^=base[i];
}cout<<ans<<'\n';
}
}