线性基
定义
对于一个数集\(A\),它的线性基是一个最小的数集\(B\),使得\(A\)中任意一个数可以通过\(B\)中的一些数异或得到。
性质
- 定义
- 线性基的异或集合中不会出现0
- 线性基中每个数最高位不同
- 线性基中数互相异或不会改变异或集合
求法
下面的求法都基于这样一个性质:
对于一个数集中的两个数\(a\)和\(b\),将其中一个异或另一个不会改变这个数集的异或集合,即令\(a=a \ xor \ b\)不改变异或集合。
这个性质是显然的,可以通过再次异或将\(a\)变回原来的数,相当与没有改动。
高斯消元
把每个数拆成二进制位放到矩阵中,对于一个数的一位是1,就用他异或其他所有的这一位为1的数,使得这一位只有这个数为1,从而求得线性基。
复杂度为:\(O(len^2n)\),\(len\)是数的二进制长度。
贪心方法
求一个线性基相当于不断把数插入已有的线性基,那么考虑插入。从最高位开始,对于这个数的每一位,若这个位在线性基中没有出现过,那么把数插入线性基的这一位。否则,把这个数异或线性基中的这一位,保证下面考虑低位的时候高位没有数。最后要么这个数变为0,要么被插入了线性基中。这样就可以在\(O(len)\)的时间把一个数插入线性基,于是可以在\(O(nlen)\)的时间内完成数集的求线性基。
求完线性基之后一定要记得\(O(len^2)\)把前面的有这一位的数的异或一下,因为有可能前面插入的数为53,后面的是20,这样会发现53^20=33,还变小了,所以我们要把53替换成33,使得线性基满足性质。
两个线性基的合并就可以暴力插入合并,启发式合并的复杂度为\(O(len*nlogn)\).
用途
线性基的用途与定义和性质相关。
异或和第k小
线性基的最大值很明显就是线性基中所有值异或起来,最小值就是其中最小的那个数。
对于第k小,由于线性基有每一个最高位都只有一个数有,那么我们按这个最高位从低到高排起来,那么可以发现,如果线性基的长度为\(h\),那么就有\(2^h\)种取法,所以对应地把k二进制分解,把答案异或上对应的线性基中的数即可。注意,不是最高位是第几位排第几,而是按最高位从小到大排,中间空出来的直接排进去。
这里有一个0的问题,即原数集能否异或出0呢?我们无法通过线性基中的数判断,但可以根据线性基的大小判断。如果线性基的大小与原数集大小相同,那么无法异或出0,否则可以。这个可以通过线性基的插入证明,即如果异或出0则不插入,导致线性基的大小小于原数集的大小。
代码
这里一定要记得,在插入一个数的时候,先用比他小的消他,再用他消比他大的,不可交换。否则会出现把比他大的数变小了,自己也变小了之类的奇怪情况。
#include<cstdio>
#include<cctype>
#include<algorithm>
#include<cstring>
using namespace std;
typedef long long giant;
giant read() {
giant x=0,f=1;
char c=getchar();
for (;!isdigit(c);c=getchar()) if (c=='-') f=-1;
for (;isdigit(c);c=getchar()) x=x*10+c-'0';
return x*f;
}
const giant maxn=1e4+10;
const giant maxj=62;
giant a[maxj],size,b[maxj];
void insert(giant x) {
for (giant j=maxj-1;j>=0;--j) if (x&(1ll<<j)) {
if (!a[j]) {
for (int i=j-1;i>=0;--i) if (x&(1ll<<i)) x^=a[i];
for (int i=j+1;i<maxj;++i) if (a[i]&(1ll<<j)) a[i]^=x;
a[j]=x;
++size;
break;
} else x^=a[j];
}
}
int main() {
#ifndef ONLINE_JUDGE
freopen("test.in","r",stdin);
#endif
giant T=read();
for (giant tt=1;tt<=T;++tt) {
printf("Case #%lld:\n",tt);
memset(a,0,sizeof a),memset(b,0,sizeof b),size=0;
giant l=0,n=read();
for (giant i=1;i<=n;++i) insert(read());
//for (giant i=0;i<maxj;++i) if (a[i]) for (giant j=i+1;j<maxj;++j) if (a[j]&(1ll<<i)) a[j]^=a[i];
for (giant i=0;i<maxj;++i) if (a[i]) b[l++]=a[i];
giant m=read();
while (m--) {
giant k=read(),ans=0;
if (size<n) --k;
if (k==0) puts("0"); else if (k>=(1ll<<size)) puts("-1"); else {
for (giant j=0;j<size;++j) if (k&(1ll<<j)) {
if ((ans^b[j])>ans) ans^=b[j];
}
printf("%lld\n",ans);
}
}
}
}
能否得到一个数
问题是能否在原数集中异或出一个给定的数。
直接把给定的数从高位到低位,如果一位上为1就异或对应的线性基中的数,若最后得到0,那么可行。这里利用了线性基的定义。
线性基作为一个工具,在很多跟异或有关的题目中都有广泛的应用。