线性基
概述
线性基是一种擅长处理异或问题的数据结构.设值域为[1, N N N],就可以用一个长度为 ⌈ log 2 N ⌉ \lceil \log_2N \rceil ⌈log2N⌉的数组来描述一个线性基。特别地,线性基第 i i i位上的数二进制下最高位也为第 i i i位。
性质
- 原序列里面的任意一个数都可以由线性基里面的一些数异或得到(也能异或得到一些不存在于集合的数)
- 线性基里面的任意一些数异或起来都不能得到0
- 线性基里面的数的个数唯一,并且在保持性质一的前提下,数的个数是最少的
注意:性质1还有另一种说法:通过原集合S的某一个最小子集S1使得S1内元素相互异或得到的值域与原集合S相互异或得到的值域相同。
插入(初始化)
初始化即是将逐个元素插入线性基的过程.
由线性基的定义可知,一位上只有一个数字
我们考虑插入的操作,令插入的数为 x x x,考虑x的二进制最高位 i i i,
- 若线性基的第 i i i位为0,则直接在该位插入 x x x,退出;
- 若线性基的第 i i i位已经有值 a i a_i ai,则 x = x ⊕ a i x = x\oplus a_i x=x⊕ai,重复以上操作直到 x = 0 x=0 x=0。
如果退出时 x = 0 x=0 x=0,则此时线性基已经可以表示原先的 x x x了;反之,则说明为了表示 x x x,往线性基中加入了一个新元素。
void add(ll x) {
for (int i = 50; i >= 0; --i) {
if (x & (1LL << i)) {
// x的最高位是1
if (d[i]) {
// 当前位置已经有线性基元素了,所以就异或一下
x ^= d[i];
} else {
// 当前位置没有,就插入x,然后退出
d[i] = x;
break;
}
}
}
}
验证
无法插入即不可表示.
bool check(ll x) {
for (int i = 50; i >= 0; --i) {
if (x & (1LL << i)) {
// //注意,如果i大于31,前面的1的后面一定要加ll
if (d[i]) {
// 如果此位上已有元素,则异或一下
x ^= d[i];
} else {
// 如果没有,则说明无法构成
return false;
}
}
}
return true; // 可以构成
}
查询异或最小值
查询最小值相对简单一些.我们不难发现,在线性基的元素中,每个元素的最高位都是不同的(不会有两个线性基的元素的最高位相同).所以线性基中最小的元素,只要与其他元素异或,所得的结果必然比其本身大,因此,异或的最小值就是线性基的最小值(此处关于0的问题,看下文查询第k小.
查询异或最大值
这个算法相对固定.因为线性基中所有元素的最高位都不相同,所以可以从高到低逐步贪心获得.
ll cal() {
ll ans = 0;
for (int i = 50; i >= 0; --i) {
if ((ans ^ d[i]) > ans) {
// 只要能更新结果就更新
ans = ans ^ d[i];
}
}
return ans;
}
查询第k小
完整的说,应该是——从一个序列中取任意个元素进行异或,求能异或出的所有数字中第k小的那个。
这里就出现了一个问题.如果说原集合的元素个数是N,线性基的个数也是N,那么就一定无法凑出0,也就是线性基的最小值不是0,而是线性基中的最小值.
如果线性基的元素个数(下文称tot)小于N,那么最小值就是0.
在解决这个问题之前,我们对线性基进行一些预处理.我们将线性基元素都转化为形如 2 i 2^i 2i的形式,
void work()//处理线性基
{
for (int i = 0; i <= 50; i++) {
for (int j = 0; j < i