线性基的理解及其应用
(一)定义:
基:在线性代数中,基(也称为基底)是描述、刻画向量空间的基本工具。向量空间的基是它的一个特殊的子集,基的元素称为基向量。向量空间中任意一个元素,都可以唯一地表示成基向量的线性组合。如果基中元素个数有限,就称向量空间为有限维向量空间,将元素的个数称作向量空间的维数。
同样的,线性基是一种特殊的基,它通常会在异或运算中出现,它的意义是:通过原集合S的某一个最小子集S1使得S1内元素相互异或得到的值域与原集合S相互异或得到的值域相同。
(二)性质:
①线性基能相互异或得到原集合的所有相互异或得到的值。
②线性基是满足性质1的最小的集合
③线性基没有异或和为0的子集。
(以上来自百度百科)
(三)求解:
接下来我们重点关注线性基怎么求解。首先确认一条十分有用的性质:
将原集合的子集(A0,A1,A2,...Ai)相互异或后的值替换为该子集中的任意一个元素,原集合的向量空间不变。证明十分简单:
①如果不需要用到A1,那么替换A1后自然没有影响;
②如果子集的异或值为x,被替换的数为A1,那么需要用到A1时,只需要将x与(A0,A2,A3...Ai)进行异或即可得到A1。
那么我们利用这一条性质可以得到这样一组线性基(A0,A1,A2...Ai),其中Ax的二进制表示的最高位的1在第x位。
设线性基为base[max_bit](base[i]的二进制表示的最高位的1在第i位),具体构造过程如下:
对于原集合中的数X而言,如果X的第i为1:
①如果base[i]存在则将X与base[i]进行异或,然后继续判断下一位;
②如果base[i]不存在,则令base[i]=X,结束对于X的判断。
为什么这样是正确的呢?利用上述性质可以一个解释:首先,由于base[i]的性质决定了base[i]不能由其它的base[j]异或得到,所以当base[i]不存在时,直接把X加入线性基中,那么当前得到的线性基当然线性无关。而如果base[i]以及存在了,我们用X与base[i]进行异或,假设得到Y,那么Y显然是由原集合的一个子集异或得到的,继续对Y继续判断相当于替换了原集合中的X,由上述说明我们知道替换之后的向量空间不变,故线性基也不会变。
或者可以想,如果当前在已经得到线性基中的元素已经可以通过相互异或得到X了,那么X显然是不需要加入线性基中的,而对于base[i]不存在,而X的第i为1的情况,X显然还不能被异或出来,所以将X加入线性基中。
(四)代码:
void init(ll n){ //n-原集合中的元素个数
while(n--){
scanf("%lld",&x); //x-原集合中的元素
for(int i=0;i<32;i++){ //32-线性基中最多的元素个数(取决于x的大小)
if((x>>i)&1){ //如果x的第i为1
if(!base[i]){base[i]=x;break;} //如果base[i]不存在,那么将x加入线性基
else x^=base[i]; //否则x^=base[i]
}
}
}
}
(五)应用:
Ⅰ)首先是一道入门题:判断n个数的子集是否可以相互异或得到x
题解:求出线性基以后,如果x的第i为1,那么base[i]必须要异或进去(否则不可能使异或得到的结果的第i为1)。故对于x为1的位,直接将x异或上base[i],如果最终得到的x为0,那么x可以被异或出来,否则不行。
代码:
#include<iostream>
#include<cstring>
#include<string>
#include<cstdio>
#include<algorithm>
#define ll long long
using namespace std;
const int maxn=1e5+10;
ll base[33],N,Q,x,y;
void init(ll n){
while(n--){
scanf("%lld",&x);
for(int i=0;i<32;i++){
if((x>>i)&1){
if(!base[i]){base[i]=x;break;}
else x^=base[i];
}
}
}
}
void answer(ll q){
while(q--){
scanf("%lld%lld",&x,&y);x^=y;
for(int i=0;i<32;i++){
if((x>>i)&1){
if(base[i])x^=base[i];
else break;
}
}
printf(x?"NO\n":"YES\n");
}
}
int main(){
#ifdef DanDan
freopen("in.txt","r",stdin);
#endif // DanDan
scanf("%lld",&N);init(N);
scanf("%lld",&Q);answer(Q);
return 0;
}
题解:
首先还是求这n个数的线性基,那么我们就可以由线性基中的数得到所有可得到的数。
假设线性基中有x个元素,则由线性基可以组成2^x-1个数(是否可以得到0需要特判,因为线性基中的元素是不能通过异或得到0的)。
由于线性基中的各个元素的最高位为1的位置不同,那么我们异或的值的最高位为1的位越低,得到的值就越小。也就是说优先选最高位为1的位置低的数。那么我们将k化成二进制串,二进制串中为1的位置就是对应的就是线性基中需要选取的元素。
比如线性基中有5个元素,分别为a0,a1,a2,a3,a4。k=5。那么我们显然就选a0,a2即可。
首先,显然不取a3,因为只从a0,a1,a2中取已经可以7个数了,而取a3大于这7个数的任意组合。而由于a0,a1只能得到3个数,所以a2必取。
其次,我们先假设a2中的<a1中为1的最高位>所在的位置为0(如果不为0,那么异或a2可以将这一位归0),那么我们就应该尽可能不选a1,而选a0。
上述过程实际上就是一个从线性基中从小到大选一个集合的过程,仔细想一下就可以发现和二进制的枚举子集是一样的。
代码:
#include<iostream>
#include<cstring>
#include<string>
#include<cstdio>
#include<vector>
#include<algorithm>
#define ll long long
using namespace std;
ll base[64],t,q,n,x,o,c=1,mx,ans;
vector<ll>_base;
void init(){
_base.clear();o=0;
memset(base,0,sizeof base);
scanf("%lld",&n);
for(int i=0;i<n;i++){
scanf("%lld",&x);
for(int k=63;k>=0;k--){
if((x>>k)&1){
if(base[k])x^=base[k];
else{base[k]=x;break;}
}
}
o=o|!x;
}
for(int i=0;i<64;i++)
if(base[i])
for(int j=i+1;j<64;j++)
if((base[j]>>i)&1)
base[j]^=base[i];
for(int i=0;i<64;i++)
if(base[i])_base.push_back(base[i]);
mx=(1ll<<(int)_base.size());
}
void work(){
printf("Case #%lld:\n",c++);
scanf("%lld",&q);
while(q--){
scanf("%lld",&x);
ans=0;x-=o;
if(x>=mx)ans=-1;
else
for(int i=0;i<64;i++)
if((x>>i)&1)ans^=_base[i];
printf("%lld\n",ans);
}
}
int main(){
#ifdef DanDan
freopen("in.txt","r",stdin);
#endif // DanDan
scanf("%lld",&t);
while(t--){init();work();}
return 0;
}
(ⅲ)推荐一篇我参考的大佬博客