线性基
基(basis)是线性代数中的一个概念,它是描述、刻画向量空间的基本工具。而在现行的 OI 题目中,通常在利用基在异或空间中的一些特殊性质来解决题目,而这一类题目所涉及的知识点被称作「线性基」。
首先来看一个问题:
给出N个数,要从中选出一个最大的子集,使得子集中的任意个元素异或值不为0.
这个和极大线性无关组有些类似。异或可以看出是模2域下的加法运算,如果把一个数转化为二进制,对应成一个由01构成的向量, 所有这些向量就构成了一个线性空间。
原问题就转化为求这个向量组的极大线性无关组,把这样一个极大线性无关组成为线性基。 可以用O(60 * 60 * n)的高斯消元来解决。
但是还有更加快速的构造线性基的方法: 复杂度是O(60 * n)
首先来证明一个性质:
取向量组中的两个向量a,b,把a,b中的某一个替换成a xor b, 替换前后向量组中的向量的线性组合得到的空间相同。 通俗的说就是 替换前后 能异或出来的值一样。
证明:
不妨把b替换成了c=a xor b.
对于一个值x,如果得到它 不需要用到b 那么替换没有影响,照样能得到x。
如果需要b b可以通过b=c xor a来得到, 因此替换后照样能组合出x。
也就是说 旧的向量组能组合出来的数 新的向量组也能组合出来。
反过来,对于新的向量组,如果把c替换成 c xor a 就得到了旧的向量组,根据上面的证明
新的向量组能组合出来的数 旧的向量组也能组合出来。
因此替换前后能组合出来的数一样。
所以如果把向量组里的向量互相异或, 极大线性无关向量组的大小是不变的。
线性基数组中有个性质,就是原数组中的任何数都可以通过线性基数组中的数异或和得到。
void solve(long long x){
for(long long i=55;i>=0;i--){
if(!(x>>i))continue;
if(!p[i]){
p[i]=x;
break;
}
x^=p[i];
}
}
例题
P3812 【模板】线性基
#include<bits/stdc++.h>
using namespace std;
const int N=100;
long long n,a,p[N];
long long ans;
void solve(long long x){
for(long long i=55;i>=0;i--){
if(!(x>>i))continue;
if(!p[i]){
p[i]=x;
break;
}
x^=p[i];
}
}
int main(){
cin>>n;
for(int i=1;i<=n;i++){
cin>>a;
solve(a);
}
for(int i=55;i>=0;i--){
if((ans^p[i])>ans){
ans=ans^p[i];
}
}
cout<<ans<<endl;
return 0;
}
P4151 [WC2011] 最大XOR和路径
将每个环的异或值求出来,再找一条主路径,然后求这条主路径异或一些环的最大值即可
异或的最大值用线性基就可以了,但主路径怎么选呢?
仔细一想就会知道,其实主路径选哪条不影响最终结果,因为如果从起点到终点的路径有多条,那么这些路径之间一定构成了一些环,无论主路径怎么选,最终异或几个环总能得到所有的路径
#include<bits/stdc++.h>
using namespace std;
#define int long long
int n,m;
const int N=5e4+10;
const int M=1e5+10;
int sum[N],p[N];
int idx,h[M*2],ne[M*2],to[M*2];
int w[M*2],vis[N];
void add(int x,int y,int z){
to[++idx]=y;
w[idx]=z;
ne[idx]=h[x];
h[x]=idx;
}
void insert(int x){
for(int i=63;i>=0;i--){
if((x>>i)&1){
if(!p[i]){
p[i]=x;
break;
}
else {
x^=p[i];
}
}
}
}
void dfs(int x,int s){
sum[x]=s;
vis[x]=1;
for(int i=h[x];i;i=ne[i]){
int y=to[i];
if(!vis[y])dfs(y,sum[x]^w[i]);
else insert(sum[x]^sum[y]^w[i]);
}
}
signed main(){
cin>>n>>m;
for(int i=1;i<=m;i++){
int a,b,c;
cin>>a>>b>>c;
add(a,b,c);
add(b,a,c);
}
dfs(1,0);
int ans=sum[n];
for(int i=63;i>=0;i--){
ans=max(ans,ans^p[i]);
}
cout<<ans<<endl;
return 0;
}
P4301 [CQOI2013] 新Nim游戏
那么,本题中,先手要胜利,则必须让后手无论拿掉哪几堆都弄不出异或和为0的情况。显然,这要用到线性基。
在插入线性基时,如果没成功插入,即最后x=0,那么意味着会出现异或和为0的情况,这时就要把x取走。显然,聪明的先手一定必胜,所以这题输出-1是不可能得分的了,我们只用考虑如何让取走的数量最小。
这时我们就要用到贪心思想:从大到小来试,能插入就插入,不能则取走。
那为啥这样结果最小呢?
粗略的证明一下,因为异或是不进位加法,那么,如果不是从大到小,假设ab…=x(a≤b≤...≤x),由于中途没有进位,因此,ab…≤a+b+...,所以显然取走x比取走那几堆更优。
如果并不是a≤b≤...≤x呢?排个序呗。
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=110;
int a[N];
int n;
int p[N];
signed main(){
cin>>n;
for(int i=1;i<=n;i++){
cin>>a[i];
}
sort(a+1,a+n+1);
int ans=0;
for(int i=n;i>=1;i--){
int x=a[i];
for(int j=32;j>=0;j--){
if((x>>j)&1){
if(!p[j]){
p[j]=x;
break;
}
else{
x^=p[j];
}
}
}
if(!x)ans+=a[i];
}
if(ans)cout<<ans<<endl;
else cout<<-1<<endl;
return 0;
}