介绍
基是线性代数中的一个概念,而在现行OI中,通常利用基在异或空间的一些特殊性质来解决问题,而这类问题涉及的知识被称为【线性基】
异或运算下的基:
对于数 $a_0, a_1,..., a_n$ 将 $a_i$ 的二进制表示成 $(b_0,b_1,...b_n)_2$ 可看作一个向量,异或运算下的基就是 $(a_0, a_1, ... a_n)$ 中的极大线性无关组。
设 $V$ 是 $(a_1, a_2,...,a_n)$ 构成的空间,求 $V$ 的基。初始化 $alpha = (a_0, a_1, ...a_n)$.
- 若 $a_1=0$,则从 $alpha$ 中去掉 $a_1$;否则保持 $alpha$ 不变
- 若 $a_2 in V(a_1)$,则从 $alpha$ 中去掉 $a_2$;否则保持 $alpha$ 不变
- 若 $a_jin V(a_1,a_2,...,a_{j-1})$,则从 $alpha$ 中去掉 $a_j$;否则保持 $alpha$ 不变
如何判断一个向量是否已经属于线性空间,可以利用高斯消元,判断 $a_j$ 能否表示成 $a_1,a_2,...,a_{j-1}$ 的线性组合。
一个线性基满足,对于它所表示的所有数的集合 $S$,$S$ 中任意多个数异或所得的结果均能表示为线性基中的元素互相异或的结果,即意,线性基能使用异或运算来表示原数集使用异或运算能表示的所有数。运用这个性质,我们可以极大地缩小异或操作所需的查询次数。
插入和判断
令插入的数为 $x$,考虑 $x$ 的二进制最高位 $i$:
- 若线性基的第 $i$ 位为0,则直接在该为插入 $x$,推出;
- 若线性基的第 $i$ 位已经有了值 $a_i$,则 $x = x igoplus a_i$,重复以上操作直到 $x=0$
如果退出时 $x=0$,则此时线性基已经能表示原先的 $x$ 了;反之,则说明为了表示 $x$,往线性基中加入了新的元素。
很容易证明这样的复杂度为 $O(log_2n)$(即二进制的位数),也可以通过这种方法判断能否通过原数列异或得到一个数 $x$.
查询异或最值
查询最小值相对比较简单。考虑插入的过程,因为每一次跳转操作,$x$ 的二进制最高位必定单调降低,所以不可能插入两个二进制最高位相同的数。而此时,线性基中最小值异或上其他数,必定会增大。。所以,直接输出线性基中的最小值即可。当然,如果线性基的个数小于原集合元素个数,最小值可取0。
考虑异或最大值,从高到低遍历线性基,考虑到第 $i$ 位时,如果当前的答案 $x$ 第 $i$ 位为0,就将 $x$ 异或上 $a_i$;否则否则不做任何操作。显然,每次操作后答案不会变劣,最终的 $x$ 即为答案。
同样,我们考虑对于一个数 $x$,它与原数列中的数异或的最值如何获得。用求序列异或最大值类似的贪心即可解决。
查询第 $K$ 小值
我们考虑进一步简化线性基。显然,一个线性基可以表示成若干个形如 $2^i$ 的数。从高到低处理线性基的每一位,对于每一位向后扫,如果当前数第$i$ 位为0,且线性基第 $i$ 位为0,则将当前数异或上 $a_i$。这个操作可在 $O(n^2)$ 的时间内解决。
经过这一步操作后,设线性基内共有 $cnt$ 个数,则他们共可以表示出 $2^{cnt}$ 个数。当然,对于0特殊考虑,如果 $n$ 与 $cnt$ 相等,就无法表示0.
随后,我们考虑将 $k$ 二进制拆分,用于快速幂类似的方法求出第 $k$ 小值。
求线性基
有关线性基的一切运算都可以看做矩阵的初等行列变换,也就可以将其看做线性规划问题。同样,可以离线使用高斯消元来构造极小线性基。
具体而言,对 $n$ 个数进行一遍插入操作即可。
并与交
并集非常简单,只需将一个插入另外一个中。
如何求两个线性基的交?
- 若 $V_1,V_2$ 是线性空间,$B_1,B_2$s 是它们的一组基,令 $W = B_2 cap V_1$,若 $B_1 cup (B_2 - W)$ 线性无关,则 $W$ 是$V_1 cap V_2$ 的一组基
- 我们从低到高考虑 $B_2$ 中的元素,同时维护由 $B_1$ 和 $B_2$ 已经插入的向量构成的线性基,并记录这个线性基中每个元素由 $V_1$ 贡献的部分。如果这个元素不能被旧的线性基表示出就直接加入线性基,否则它由 $V_1$ 贡献的部分加入答案。
两者的时间复杂度都是 $O(logn^2)$
模板
const int bits = 31; struct LBase { //const static int bits = 31; //0~31位 ll d[bits+1], tmp[bits+1]; //线性基 bool flag = false; //记录是否cnt < n LBase() {memset(d, 0, sizeof d);} void insert(ll x) { for(int i=bits;~i;i--) if(x&(1ll<<i)) { if(!d[i]){ d[i]=x; break;} else x ^= d[i]; } flag = true; } bool check(ll x) //返回true表示已经能被表示 { for(int i=bits;~i;i--) if(x&(1ll<<i)) { if(!d[i]) return false; else x ^= d[i]; } return true; } ll qmax(ll res=0) { for(int i=bits;~i;i--) res=max(res,res^d[i]); return res; } ll qmin() { if(flag) return 0; for(int i=0;i<=bits;i++) if(d[i]) return d[i]; } ll query(ll k) //查询第k小 { ll res=0; int cnt=0; k-=flag; if(!k)return 0; for(int i=0;i<=bits;i++){ for(int j=i-1;~j;j--) if(d[i]&(1ll<<j)) d[i]^=d[j]; if(d[i]) tmp[cnt++]=d[i]; } if(k>=(1ll<<cnt))return -1; for(int i=0;i<cnt;i++) if(k&(1ll<<i)) res^=tmp[i]; return res; } void merge(const LBase &a) { //求并集 for (int i = bits; i >= 0; --i) if (a.d[i]) insert(a.d[i]); } }; LBase intersection(const LBase &a,const LBase &b) //求交集 { LBase ans, c=b,d=b; for(int i = 0;i <= bits;i++) { ll x =a.d[i]; if(!x) continue; int j=i;ll T=0; for(;j>=0;--j) if((x>>j)&1) if(c.d[j]){x^=c.d[j];T^=d.d[j];} else break; if(!x)ans.d[i]=T; else {c.d[j]=x;d.d[j]=T;} } return ans; }