线性基

摸一手线性基


由于线性代数没学好,所以就先不讲线性基的线代部分。


定义

线性基是从一个数集(大小为 \(N\))构造出的另一个数集(大小为 \(\log _2N\))。满足线性基中的元素任意异或起来原数集中元素任意异或起来,两者值域相同。个人理解在异或运算下,线性基是原数集的简化。
比如对于一个数集 \(\{1,2,3\}\) ,它的线性基是 \(\{1,3\}\)\(\{1,2\}\)\(\{2,3\}\)


构造

我们先来考虑构造,因为有些性质容易由根据构造过程得出。

根据定义,线性基只需要满足线性基的元素任意异或能够得到原数集的任意元素。因为只要能得到原数集的所有数了,那么原数集能通过异或表达的数,线性基也一定能表达。

所以,我们的目的就是构造出一个能表达出原数列中每个数的线性基。我们考虑根据原数集,以增量构造法来构造。

首先,我们将线性基的元素按位排序,即线性基 \(P_0,P_1,P_2,P_3\cdots\) 分别表示线性基中元素二进制最高位是第 \(0,1,2,3\cdots\) 位的元素。每一位只需要一个数。

然后,我们遍历原序列 \(A\),按以下操作将 \(a_i\) “插入”线性基

  1. 考虑 \(a_i\) 的二进制最高位 \(i\)

  2. \(P_i=0\),则令 \(P_i\)\(a_i\) 并结束操作。
    否则令 \(a_i\)\(a_i\bigotimes P_i\),返回操作1.

如果退出操作时 \(a_i=0\) 那么说明 \(a_i\) 已经可以被线性基内元素表达出来;
否则则说明线性基中新插入了一个元素。

代码实现

void getp(){//m是原数列大小,n是数的二进制的大小限制
	for(int i=1;i<=m;i++)
		for(int j=n;j>=0;j--)
			if(a[i]&(1ll<<j)){
				if(!p[j]){p[j]=a[i];break;}
				a[i]^=p[j];
			}
}

性质

好了,我们来讨论以下线性基的良好性质。


性质1

线性基中的元素任意异或起来原数集中元素任意异或起来,两者值域相同。

等价于线性基的元素任意异或能够得到原数集的任意元素。

证明:对于一个原数集中的数 \(a_i\),如果它返回时是 \(0\) ,说明已经可以被操作中异或过的 \(P_j\) 表达出来了,如果不是,那么新加入的这个元素可以和插入之前异或过的那些 \(P_j\) 表达出来 \(a_i\)


性质2

线性基中元素任意子集的异或和不为 \(0\)

证明:以子集大小为 \(3\) 为例,假设存在 \(P_1\bigotimes P_2\bigotimes P_3=0\),那么 \(P_1\bigotimes P_2=P_3\),而我们由线性基的构造过程可知,这样的 \(P_3\) 是不可能被加入线性基的,故该性质得证。


性质3

一个数集的线性基可能有多个,它们元素不同但大小相同。
并且线性基在满足性质1的条件下大小是最小的。

首先考虑证第一句话。

首先如果所有的原数集元素都成功插入线性基了,显然满足性质3。
如果存在一个元素 \(x\) 没有插入,那么以大小为 \(3\) 的子集为例,存在 \(P_i\bigotimes P_j\bigotimes P_k=x\),我们如果改变插的顺序就会发现:
\(P_i\bigotimes P_j\bigotimes x=P_k\)
\(P_i\bigotimes x\bigotimes P_k=P_j\)
原本是 \(x\) 插不进去,现在是 \(P_k\)\(P_j\) 插不进去,换言之一个插进去了另一个就会出来。所以线性基的大小是一定的,但元素可能不同。

再考虑第二句话。

考虑线性基的构造过程,因为线性基每个元素都是为了能够表示出原数集中某元素才被添加的,也就是说每个元素都是必要的,所以这样的线性基的元素缺一不可,也就是大小最小。


性质4

任意能被原数集异或表达出来的数在线性基中的异或方式一定。

换言之,就是线性基任意子集异或结果不同。

考虑反证:仍然以大小为 \(3\) 的子集为例,假设存在 \(P_1\bigotimes P_2\bigotimes P_3=P_4\bigotimes P_5\bigotimes P_6\),那么就有\(P_1\bigotimes P_2\bigotimes P_3\bigotimes P_4\bigotimes P_5\bigotimes P_6=0\),这与性质2矛盾,故性质4得证。

另外,我们由此可以有推论,就是一个线性基能异或出 \(2^{m}-1\) 个数,其中 \(m\) 是线性基中元素个数,减 \(1\) 是由于不能异或出 \(0\)。而对于一个数集,我们只需要讨论是否能异或出 \(0\) 就可以通过线性基确定它能异或出的数的多少。


应用

应用1

查询一个元素能否被一个数集中的元素异或得到。

我们把数集的线性基跑出来,考虑构造线性基的过程,让这个数模拟一遍就可以通过结束后是否为 \(0\) 判断了。


应用2

在一个数集任取子集异或,求最大异或和。

我们只需要从高位到低位贪心地选择就好了,因为线性基中元素最高位都不同,所以只需要考虑最高位最大。


应用3

在一个数集任取子集异或,求最小异或和。

首先跑出线性基,然后我们发现线性基中最小的元素不管异或上其他的哪一个都会变大,所以答案就是线性基中的最小值。
但还有要特判的情况,如果在构造线性基时有返回 \(0\) 的情况,就说明原数集存在异或和为 \(0\) 情况,这种情况直接输出 \(0\)


应用4

在一个数集任取子集异或和,求第 \(K\) 小异或和。

准确地说,是在所有能异或出的数字中第 \(K\) 小的那一个。
看起来是前两个的加强版,但是好像无法在前两个的基础上继续做。

我们需要重新对当前线性基进行调整,具体的,对于 \(i\) 从小到大枚举,处理 \(p[i]\),枚举 \(j\)\(i-1\)\(0\),如果 \(p[i]\) 二进制第 \(j\) 位是 \(1\),那么 \(p[i]\) 异或上 \(p[j]\)

for(int i=1;i<=60;i++)
	for(int j=i;j>=1;j--)
		if(p[i]&(1ll<<j))p[i]^=p[j];

这样就处理完了,查询过程是这样:
先特判是否有能异或出零的情况。
然后 ans 就是 \(k\) 所有 \(1\) 位置对应的 \(p[i]\) 之异或和。
这里的 \(p[i]\) 只看不为 \(0\) 的部分。

int ans=0;
for(int i=0;i<=50;i++)
	if(p[i]){
		if(k&1)ans^=p[i];
		k>>=1;
	}

对于正确性,可以预见线性基经过这样处理之后仍然是原数集的一个线性基,因为新线性基内元素一定可以异或得到原线性基元素。

由性质4的推论我们可以知道,线性基所能异或出的数只和线性基中每个数的最高位有关,因此,只要让每个元素的最高位不受其他元素的影响,就可以如上述查询过程一样轻松求解了。
于是我们只需要从低位到高位处理每个元素,并把会影响到低位的 \(1\) 异或掉(因此 \(j\) 是从高位往低位循环),就可以构造出符合要求的线性基了,这样的线性基每个元素最高位只由该元素确定。

贴一发记录


XOR路径问题

这是一个图上路径的线性基相关的问题。

一张无向图,边有边权,求源点到任意点边权异或和最大的路径。

对于这样的问题,有这样一个重要的结论,也并不难想到:
任意 \(1\)\(i\) 的路径异或和都可以由一条 \(1\)\(i\) 的路径异或和与图中一些环的异或和得到。

首先对于环不在这条路径上的情况,我们设想从 \(1\) 走到这个环上再原路返回,这样中间的路就被异或掉了,然后就只得到了环的异或和。
其次对于环在这条路径上的情况,这条路径的一部分和其他边形成环,我们发现用这条路径异或这个环,就会实现把这条路径的一部分换成其他边,从而变换到其他路径。

于是我们只需要用 dfs 找到图中所有环并插入线性基,最后用任意一条路径去贪心地取最大值就可以了。
关于 dfs 找环,我们考虑图的 dfs 生成树,通过返祖边或横叉边来找环,这样可以找到所有只有一条非树边的环。这些环通过相互异或就可以得到有多条非树边的环,于是就可以不漏掉所有环了。

例题P4151 [WC2011]最大XOR和路径

code

#include<bits/stdc++.h>
using namespace std;
#define int long long
#define in read()
inline int read(){
	int p=0,f=1;
	char c=getchar();
	while(!isdigit(c)){if(c=='-')f=-1;c=getchar();}
	while(isdigit(c)){p=p*10+c-'0';c=getchar();}
	return p*f;
}
const int N=5e4+5;
int n,m,p[64];
void getp(int x){
	for(int j=62;j>=0;j--)
		if(x&(1ll<<j)){
			if(!p[j]){p[j]=x;break;}
			else x^=p[j];
		}
}
struct edge{
	int v,w,nxt;
}e[N<<2];
int head[N],en=1;
inline void insert(int u,int v,int w){
	e[++en].v=v;
	e[en].w=w;
	e[en].nxt=head[u];
	head[u]=en;
}
int vis[N],xsum[N],fa[17][N],dep[N];
inline int lca(int x,int y){
	if(dep[x]<dep[y])swap(x,y);
	for(int i=16;i>=0;i--)
		if(dep[fa[i][x]]>=dep[y])
			x=fa[i][x];
	if(x==y)return x;
	for(int i=16;i>=0;i--)
		if(fa[i][x]^fa[i][y])
			x=fa[i][x],y=fa[i][y];
	return fa[0][x];
}
inline void dfs(int u,int f){
	fa[0][u]=f,dep[u]=dep[f]+1;
	for(int i=1;i<=16;i++)
		fa[i][u]=fa[i-1][fa[i-1][u]];
	for(int i=head[u];i;i=e[i].nxt){
		int v=e[i].v,w=e[i].w;
		if(v==f)continue;
		if(!vis[v]) vis[v]=1,xsum[v]=xsum[u]^w,dfs(v,u);
		else getp(w^xsum[u]^xsum[v]);
	}
}
signed main(){
	n=in,m=in;
	for(int i=1;i<=m;i++){
		int x=in,y=in,z=in;
		insert(x,y,z),
		insert(y,x,z);
	}
	vis[1]=1,dfs(1,0);int ans=xsum[n];
	for(int i=62;i>=0;i--)
		ans=max(ans,ans^p[i]);
	cout<<ans;
	return 0;
}

注意long long 要用 1ll

!!!!!!!!!!!!!!!


posted @ 2022-01-25 17:10  llmmkk  阅读(136)  评论(0编辑  收藏  举报