感谢这位博主的文章
全面解析的博客
做什么: 一般是在n个数中选择一些数异或起来答案最大. (不同与01字典树!!!)
定义:
设数集T的值域范围为[1,2n−1]。
T的线性基是T的一个子集A={a1,a2,a3,…,an}。
A中元素互相xor所形成的异或集合,等价于原数集T的元素互相xor形成的异或集合。
可以理解为将原数集进行了压缩
性质:
1.设线性基的异或集合中不存在0。
2.线性基的异或集合中每个元素的异或方案唯一,其实这个跟性质1是等价的。
3.线性基二进制最高位互不相同。
4.如果线性基是满的,它的异或集合为[1,2n−1]。
5.线性基中元素互相异或,异或集合不变。
基本操作:
1: 插入
如果向线性基中插入数x,从高位到低位扫描它为1的二进制位。
扫描到第i时,如果ai不存在,就令ai=x,否则x=x⊗ai。
x的结局是,要么被扔进线性基,要么经过一系列操作过后,变成了0。
bool Insert(ll val) {
for (int i = 60 ; i >= 0 ; i --) {
if (val & (1ll << i)) {
if (!d[i]) {
d[i]=val;
break;
}
val^=d[i];
}
}
return val > 0;
// 可判断val是否存在于线性基当中.
}
2: 合并
将一个线性基暴力插入另一个线性基即可。
L_B Union(const L_B &a, const L_B &b) {
L_B res = a;
for (int i = 60 ; i >= 0 ; i --)
if (b.d[i]) res.Insert(b.d[i]);
return res;
}
3: 查询
一: 存在性
如果要查询x
是否存于异或集合中。
从高位到低位扫描x的为1的二进制位。
扫描到第i位的时候x=x⊗ai
如果中途x变为了0,那么表示x
存于线性基的异或集合中。
二:最大值
从高位到低位扫描线性基。
如果异或后可以使得答案变大,就异或到答案中去。
三: 最小值
小值即为最低位上的线性基。
bool Insert(ll val) {
for (int i = 60 ; i >= 0 ; i --) {
if (val & (1ll << i)) {
if (!d[i]) {
d[i]=val;
break;
}
val^=d[i];
}
}
return val > 0;
// 可判断val是否存在于线性基当中.
}
ll query_max() {
ll res = 0;
for (int i = 60 ; i >= 0 ; i --) {
if ((res^d[i]) > res) res ^= d[i];
}
return res;
}
ll query_min() {
for (int i = 0 ; i <= 60 ; i ++) {
if (d[i]) return d[i];
}
return 0;
}
4: k小值
根据性质3。
我们要将线性基改造成每一位相互独立。
具体操作就是如果j < i,di 的第 j 位是1,就将di 异或上 dj。
经过一系列操作之后,对于二进制的某一位i。只有di的这一位是1,其他都是0。
所以查询的时候将k二进制拆分,对于1的位,就异或上对应的线性基。
最终得出的答案就是k小值。(因为线性基的特点!, 类似于二进制枚举)
注意判断0的情况, 也就是原序列有可能异或出0的情况来, 而这个线性基是判断不了的, 所以我们要人为判断, 那怎么判断了, 实际上就是如果插入到线性基中的数不是全体的数, 那么必定有零的这个元素的出现. 想想插入的过程就懂了, 如果有零, 那么还需要将输入的k–再进行判断.
void rebuild() { // 用于求第k小值.需要先进行独立预处理
for (int i = 60 ; i >= 0 ; i --) {
for (int j = i-1 ; j >= 0 ; j --) {
if (d[i] & (1ll<for (int i = 0 ; i <= 60 ; i ++) {
if (d[i]) p[cnt++]=d[i];
}
}
ll kthquery(ll k) {
ll res = 0;
if (k >= (1ll << cnt)) return -1;
for (int i = 60 ; i >= 0 ; i --) {
if (k & (1LL<return res;
}
struct L_B {
ll d[65], p[65];
int cnt;
void init() {
Fill(d, 0); Fill(p, 0);
cnt = 0;
} // 1e18以内的数都适用.
bool Insert(ll val) {
for (int i = 60 ; i >= 0 ; i --) {
if (val & (1ll << i)) {
if (!d[i]) {
d[i]=val;
break;
}
val^=d[i];
}
}
return val > 0;
// 可判断val是否存在于线性基当中.
}
ll query_max() {
ll res = 0;
for (int i = 60 ; i >= 0 ; i --) {
if ((res^d[i]) > res) res ^= d[i];
}
return res;
}
ll query_min() { // 应该预先判断能否是0的情况..QAQ
for (int i = 0 ; i <= 60 ; i ++) {
if (d[i]) return d[i];
}
return 0;
}
void rebuild() { // 用于求第k小值.需要先进行独立预处理
for (int i = 60 ; i >= 0 ; i --) {
for (int j = i-1 ; j >= 0 ; j --) {
if (d[i] & (1ll<for (int i = 0 ; i <= 60 ; i ++) {
if (d[i]) p[cnt++]=d[i];
}
}
ll kthquery(ll k) { // 注意判断原序列异或出0的情况, 此时应该将k -- 在进行后面的操作.
ll res = 0;
if (k >= (1ll << cnt)) return -1;
for (int i = 60 ; i >= 0 ; i --) {
if (k & (1LL<return res;
}
void Merge(const L_B &b) { // 把b这个线性基插入到当前这个线性基中.
for (int i = 60 ; i >= 0 ; i --)
if (b.d[i]) Insert(b.d[i]);
}
};
线性基的题型相对比较固定,看到下面的类型基本上都是线性基了:
最大异或和
第 k 大异或和/异或和是第几大
求所有异或值的和
线性基中的题目中还用到一个技巧:
任意一条 1 到 n 的路径的异或和,都可以由任意一条 1 到 n 路径的异或和与图中的一些环的异或和来组合得到。
(因为我们可以从起点出发走一遍那个环, 然后再回到起点, 此时的值就是该环上的异或和, 因为回到起点后, 起点到环之间的路径经过了两次了, 就抵消啦~)
总共有n个数, 能插入到线性基的有x个数, 如果n == x, 也就是每一个数都插入到了线性基, 那么这n个数的异或和方案数很明显就是2^n(包括空集), 即每种异或和的方案数为1, 但是如果n == x +1, 也就是有一个元素没有插入进去, 那么这个数一定时和当前在线性基中的元素异或等于0乐. 所以每种异或和的方案数等于2,如果n == x + 2, 那么每种异或方案就为4了, 更总结的来说, 有n个数, x个可以插入到线性基中, 那么每种异或和方案数为2^(n-x)种, 证明的话不太会, 意会一下……
这便是线性基的全部东西了。