【学习笔记】线性基
指异或线性基,用一组线性无关的基底表示一些数异或的信息。
性质
-
任意一个原序列中的数度都能被线性基的几个基底异或得到。
-
线性基任意一个子集异或和不为 \(0\)。
-
一个序列对应多个线性基,但线性基的规模都一定,且是满足性质 \(1\) 中所有基底组成集合最小的一个。
证明将在插入操作中展开。
具体操作
插入
对于数 \(x\),从高到低位枚举,如果这一位是 \(1\),且目前这一位的基底还没有,就直接更新基底并退出;反之将 \(x\) 异或这一位的基底。
inline void insert(ll x){
for(int i=62;i>=0;--i){
if(x&(1ll<<i)){
if(!bs[i]){
bs[i]=x,++cnt;
break;
}
else x^=bs[i];
}
}
chk0=1;
}
对于性质 \(1\),这样插入如果最终也没有更新过一个基底,那么一定是被异或为 \(0\),也就是被表示出来了,反之相当于更新的基底异或上之前的位置,也得到了这个数。
对于性质 \(2\),如果出现异或和为 \(0\),那么最后一个数不会被插入。
对于性质 \(3\),设想仅改变两个数 \(x,y\) 的插入顺序,而构成的线性基不同,那么一定会在一位产生分歧,即这一位被先插入的数 \(x\) 占据,讨论 \(y\) 最后是否被插入,如果没有被插入,则没有任何影响;如果被插入,那么原来插入在这个位置的数将会被顶替,就变成了又一个这样的问题,只不过是位置变低,依此归纳,最终最后一位被占据的将无法插入,也就是线性基规模是不变的。
查询是否可以被表示
按照插入方法,判断最终是否为 \(0\) 即可。
inline bool query(ll x){
for(int i=62;i>=0;--i){
if(x&(1ll<<i)) x^=bs[i];
}
return !x;
}
查询异或最大值
接下来我们的操作都等价到线性基上,线性基的每一个基底最高位都不同,这是很友好的性质。
从高到低枚举,这样多异或一个数不会改变更高位的值,于是贪心异或,如果可以变更大就异或。
inline ll query_max(){
ll res=0;
for(int i=62;i>=0;--i){
if((res^bs[i])>res) res^=bs[i];
}
return res;
}
查询异或最小值
如果可以出现 \(0\),答案必然是 \(0\),反之答案应该是最小的基底,因为只要有更大的基底参与,那么一定会在更高位上有 \(1\),而大于这个数。
inline ll query_min(){
if(chk0) return 0;
for(int i=0;i<=62;++i){
if(bs[i]) return bs[i];
}
}
查询 \(k\) 小值
相对较难。思考这样一个问题:如果基底都是 \(2^k\),的确满足了完全表示的性质,并且这种中规中矩的基底,可以直接根据 \(k\) 的二进制来得到,也就是说,不会因为多选取一个元素而导致结果更小,或者是说,基底互不干扰。
但这样不满足线性基最小规模,例如 \((1011)_2,(0001)_2\),两个基底 \((1010)_2,(0001)_2\) 就满足了互不干扰和规模最小两个条件。
如何构造这种互不干扰的线性基?
实际上是对于一个高位基底,尽量地消去低位的 \(1\),使得一个基底的最高位只在自己位置是 \(1\),其余基底均为 \(0\)。这样的得到满足线性基性质却又可以进行上述查询的基底。
接下来类似于变进制转换,直接把 \(k\) 中对应位是 \(1\) 的基底异或起来即可。
inline void rebuild(){
for(int i=62;i>=0;--i){
for(int j=i-1;j>=0;--j){
if(bs[i]&(1ll<<j)) bs[i]^=bs[j];
}
}
for(int i=0;i<=62;++i){
if(bs[i]) rbs[++tot]=bs[i];
}
}
inline ll kth(ll k){
if(chk0){
if(k==1) return 0;
--k;
}
ll res=0;
for(int i=tot;i>=1;--i){
if(k&(1ll<<i-1)) res^=rbs[i];
}
return res;
}