【算法题】【线性代数3】【线性基】【 2.8 牛客寒假3】

线性基的双重记录机制:最高位映射与方案还原

核心机制设计

线性基的精妙之处在于它建立了两个层次的映射关系:

  1. 位层次映射:每个二进制位对应一个基向量
  2. 元素层次映射:每个基向量记录其构成来源

具体实现细节

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 不能被表示

正确性证明要点

  1. 线性基性质

    • 基向量线性无关
    • 能表示所有原始向量可表示的空间
  2. msk记录的正确性

    • 初始:curm 唯一标识当前向量
    • 每次 cur ^= bas[j],同时 curm ^= msk[j]
    • 最终有:cur = v ⊕ (某些bas[j])
    • 对应:curm = (1<<cnt) ⊕ (对应msk[j])
    • 所以 cur = 0 时,curm 记录了 v 的表示方式
  3. 方案还原的正确性

    • pick 记录了表示X所需的基向量
    • 每个基向量的 msk 记录了它的原始构成
    • 因此 pick 中为1的位对应的原始元素就是解

算法优势

  1. 高效性:O(n log MAX) 时间,O(log MAX) 空间
  2. 完全性:不仅能判断存在性,还能输出具体方案
  3. 通用性:可解决各类异或子集问题

总结

这种基于最高位索引的双重记录机制是线性基算法的核心创新:

  • 外层:按最高位组织基向量,保证线性独立性
  • 内层:记录每个基向量的"生成历史",支持方案还原
  • 两者结合,使得算法既高效又功能完整

理解这一机制,就掌握了线性基解决异或子集问题的关键。

代码

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

}

5 P4570 [BJWC2011] 元素

相关推荐
寻寻觅觅☆7 小时前
东华OJ-基础题-106-大整数相加(C++)
开发语言·c++·算法
偷吃的耗子7 小时前
【CNN算法理解】:三、AlexNet 训练模块(附代码)
深度学习·算法·cnn
化学在逃硬闯CS8 小时前
Leetcode1382. 将二叉搜索树变平衡
数据结构·算法
ceclar1238 小时前
C++使用format
开发语言·c++·算法
Gofarlic_OMS9 小时前
科学计算领域MATLAB许可证管理工具对比推荐
运维·开发语言·算法·matlab·自动化
夏鹏今天学习了吗9 小时前
【LeetCode热题100(100/100)】数据流的中位数
算法·leetcode·职场和发展
忙什么果10 小时前
上位机、下位机、FPGA、算法放在哪层合适?
算法·fpga开发
董董灿是个攻城狮10 小时前
AI 视觉连载4:YUV 的图像表示
算法
Evand J10 小时前
【定位方法】到达时间(TOA)用于三边定位,建模、解算步骤、公式推导
线性代数·toa·定位方法·三边定位
ArturiaZ11 小时前
【day24】
c++·算法·图论