线性基

定义

线性基是一个集合,每个序列都拥有至少一个线性基,取线性基中若干个数异或起来可以得到原序列中的任何一个数。

性质

1、原序列任意一个数可以由线性基里若干个数异或得到
2、线性基里任意取数异或起来不可能为 0
3、对于一个序列,线性基不一定唯一,但线性基中数的个数固定且在满足性质 1 的条件下最小
4、对于一个值域为 \([1,N]\) 的序列,线性基的长度小于等于 \(log_2 N\)

线性基构造

void ins(int x){
	for(int i=M;i>=0;i--){
		if(x&(1ll<<i)){
			if(d[i])x^=d[i];
			else{
				d[i]=x;
				break;
			}
		}
	}
}

由此可知线性基中如果 \(d_i\) 不为空,则 \(d_i\) 的二进制第 \(i+1\) 位为 \(1\) ,且为最高位;记为性质 5

性质的证明

gu....

求最大值

即求原序列中若干数异或和最大
由性质 5 易得方法为:
从线性基的最高位开始,假如当前的答案异或线性基的这个元素可以变得更大,那么就异或它

int ma(){
	int ans=0;
	for(int i=M;i>=0;i--) {
		if((ans^d[i])>ans)ans^=d[i];
	}
	return ans;
}

最小值

如果有元素不能插入线性基,最小值就是 0 ,否则为最小的 \(d_i\)

K 小值

把线性基处理一下,使得从小到大枚举 \(d\) 异或 \(d_i\) 一定变大
操作为若 存在 \(j\) 满足 \(j<i\ and\ d_j=1\) ,则让 \(d_i\) 在第 \(j\) 位为 \(0\)
处理完之后 \(d\) 还是原序列线性基,注意判断原序列异或能否为 \(0\) ,即有没有不能插入的数

void rebuild(){
	for(int i=0;i<=60;i++){
		for(int j=0;j<i;j++){
			if(d[i]&(1ll<<(j)))d[i]^=d[j];
		}
		if(d[i])tot++;
	}
}
int k_th(int k){
	if(tot<n)k--;
	if(k>=(1ll<<tot))return -1;
	int ans=0;
	for(int i=0;i<=60;i++){
		if(!d[i])continue;
		if(k&1)ans^=d[i];
		k/=2;
	}
	return ans;
}

删除

模板

新Nim游戏

Nim 游戏为序列异或和不为 0 则先手必胜。
所以本题第一步要使得对方无法构造出异或和为 0 的序列。
想到线性基的性质 2 所以要将不能插入线性基的数全部拿走。
为了最小化,将序列从大到小排序后插入;
为什么是对的?
感性理解因为如果原序列有两个数拥有相同的二进制位为 1 ,它们都可以插入线性基,但此时插入更大的显然是不劣且对其它操作没有影响的。

元素

板子题

彩灯

用开关序列构造线性基,如果某一位有就说明这个彩灯能被打开,记录能打开的彩灯数量 \(k\) ,答案即为 \(2^k\)
原理:线性基的性质保证能够得到原序列的所有可能异或和,同时不会重复。

最大异或和路径

发现可以重复走,所以我们除了从 \(1\) 走到 \(n\) 外还可以走一些 ,发现走到环的起点再回来会经过“链接路径”两次,所以不需要管。所以答案就是在所有环、所有 \(1-n\) 路径里找异或最大值。
所有环好找,但所有 \(1-n\) 路径不好找,但我们发现两条路径实际就是一个环,所以我们随便选一条路径即可。把环和路径的异或和构造线性基即可。
然后发现其实以一个点出发找到的环不是所有的环,但就异或来讲可以得到所有异或值,所以方法正确

#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=1e5+10;
int n,m,head[N],idx;
struct edge{
	int w,v,next;
}e[N<<1];
void con(int u,int v,int w){
	idx++;e[idx].v=v;e[idx].next=head[u];e[idx].w=w;head[u]=idx;
}
int d[63];
void ins(int x){
	for(int i=60;i>=0;i--){
		if(x&(1ll<<i)){
			if(d[i])x^=d[i];
			else {d[i]=x;return;}
		}
	}
}
int vis[N],pre[N];
void dfs(int u,int sum){
	pre[u]=sum,vis[u]=1;
	for(int i=head[u];i;i=e[i].next){
		int v=e[i].v,w=e[i].w;
		if(!vis[v])dfs(v,sum^w);
		else {ins(w^sum^pre[v]);}
	}
}
signed main(){
	cin>>n>>m;
	for(int i=1;i<=m;i++){
		int u,v,w;
		cin>>u>>v>>w;
		con(u,v,w),con(v,u,w);
	}
	dfs(1,0);
	int ans=pre[n];
	for(int i=60;i>=0;i--){
		if((ans^d[i])>ans)ans^=d[i];
	}
	cout<<ans;
	return 0;
}

albus就是要第一个出场

很好求去重后每个数的排名,像这样:

for(int i=0,j=0;i<=60;i++){
	if(!d[i])continue;
	if(b&(1ll<<i))ans=(ans+(1ll<<j))%mod;
	j++;
}

排名即为 \(ans+1\)
怎么求原序列呢,结论:每个异或值出现 \(2^{n-k}\) 次,其中 \(k\) 是线性基元素个数
简单证明:对于无法插入线性基的数的集合,随便选一个子集有 \(2^{n-k}\) 种,每个子集的异或和都可与线性基内的一些异或和异或得到 \(0\) ,然后用 \(0\) 去异或所有异或值,所以每个异或值有 \(2^{n-k}\) 种组成方式。
解法显然

#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=1e5+10,mod=10086;
int n,b,tot,d[62],p[62];
int ksm(int a,int b){
	int ans=1;
	while(b){
		if(b&1)ans*=a;
		a*=a;a%=mod;ans%=mod;b>>=1;
	}return ans;
}
void ins(int x){
	for(int i=60;i>=0;i--){
		if(x&(1ll<<i)){
			if(d[i])x^=d[i];
			else {tot++,d[i]=x;return;}
		}
	}
}
signed main(){
	cin>>n;
	for(int i=1;i<=n;i++){
		int u;cin>>u;ins(u);
	}
	cin>>b;
	int ans=0;
	for(int i=0,j=0;i<=60;i++){
		if(!d[i])continue;
		if(b&(1ll<<i))ans=(ans+(1ll<<j))%mod;
		j++;
	}
	cout<<(ans*ksm(2ll,n-tot)%mod+1ll)%mod<<" ";
	return 0; 
}

前缀线性基

用于处理一段区间里异或值
思路就是对于每个 \(i\) 记录 \([1,i]\) 的线性基,同时保证尽可能用靠 \(i\) 的数作为基;
为了确定一个基在某个区间内,还要记录这个基是由哪个数形成的,记为 \(pos\)

void ins(int x){
	num++;
	for(int i=0;i<=60;i++){
		d[num][i]=d[num-1][i];
		pos[num][i]=pos[num-1][i];
	}
	int p=num;
	for(int i=60;i>=0;i--){
		if(x&(1ll<<i)){
			if(d[num][i]){
				if(pos[num][i]<p){
					swap(pos[num][i],p);
					swap(d[num][i],x);
				}
				x^=d[num][i];
			}
			else{
				d[num][i]=x;
				pos[num][i]=p;
				return;
			}
		}
	}
}

幸运数字

前缀线性基,不过把 \(d,pos_{u,i}\) 定义改为根节点到 \(u\) 的线性基
\(lca\) ,深度小于 \(dep_{lca}\) 就不要,每次开一个临时线性基暴力合并 \(u,v\) 即可

for(int i=1;i<=n;i++){
	cin>>a[i];
}
for(int i=1;i<n;i++){
	int u,v;
	cin>>u>>v;
	con(u,v),con(v,u);
}
dfs(1,0);
for(int i=1;i<=q;i++){
	int u,v;cin>>u>>v;
	int lc=lca(u,v),ans=0;
	for(int i=60;i>=0;i--){
		now[i]=0;
		if(dep[pos[u][i]]>=dep[lc])now[i]=d[u][i];
	}
	for(int i=60;i>=0;i--){
		if(pos[v][i]<dep[lc])continue;
		int x=d[v][i];
		if(!x)continue;
		for(int j=i;j>=0;j--){
			if(x&(1ll<<j)){
				if(now[j]) x^=now[j];
				else{now[j]=x;break;}
			}
		}
	}
	for(int i=60;i>=0;i--)
		if((ans^now[i])>ans)ans^=now[i];
	cout<<ans<<"\n";
}
posted @ 2025-07-12 09:31  LC_Nocl  阅读(9)  评论(0)    收藏  举报