【学习笔记】线性基
被卡2026牛客多校2的B,得知线性基秒杀,遂学习
简介
ACM中,“线性基”通常特指异或线性基。
线性基可以理解为:从一组向量中去掉冗余部分,保留一组线性无关的向量,并且这些向量仍能表示原向量组张成的整个空间。
异或线性基是一种擅长处理异或问题的数据结构.设值域为 \([1,N]\),就可以用一个长度为 \([log_2N]\) 的数组来描述一个线性基。
线性基可以高效解决:
- 一组数的子集异或最大、最小、第 \(k\) 大、第 \(k\) 小值;
- 某个数能否由若干元素异或得到;
- 不同子集异或值的数量;
理解
考虑二维向量:
因为:$ v3 \ = \ v1 \ + \ v2 $ ,即 \(v3\) 可以被空间中其他向量线性表示;
所以 \(v3\) 是冗余的。只保留:$ {v1, \ v2} $;
他们是线性无关的,即不能相互表示,但他们仍然可以表示原来三个向量张成的整个二维空间,因此它们构成一组基。
异或线性空间是同样的思想,只是:
- 向量的每个分量只能是 0 或 1;
- 加法变成按位异或;
- 线性组合的系数只能是 0 或 1。
例如:$ 3 = 011, \ 5 = 101,\ 6 = 110 $
由于:$ 3 \oplus 5 = 6 $
所以 6 可以由 3 和 5 表示。集合 {3,5,6} 的一组线性基可以是 {3,5}。
整数的每一个二进制位,都是线性基中向量的一个维度。
存储
设整数的二进制位数为 \(B\),使用数组 base[B] 存储线性基,其中 base[i] 表示最高位的 \(1\) 位于第 \(i\) 位的基向量。
每个最高位至多对应一个基向量。这与高斯消元中的主元类似:不同基向量拥有不同的最高位,因此它们线性无关。
插入 + 判断表示
插入整数 x 时,从最高位向最低位扫描:
- 如果
x的第 \(i\) 位为 \(0\),则跳过该位; - 如果第 \(i\) 位为 \(1\) 且
basis[i]为空,则令basis[i] = x,插入成功; - 如果
basis[i]已存在,则执行x ^= basis[i],消去x的第 \(i\) 位; - 如果最终
x变为 \(0\),说明它可以被原有线性基表示,无需插入。
插入过程可以理解为:
- 分离
x中当前未存在于线性基中的维度basis[i],同时由于在异或空间中,去掉这一维度的x会变为x ^= basis[i]
判断的过程也很简单,是插入的逆过程:
- 在线性基中遍历所有维度;
- 如果线性基中没有
x中为1的一维,显然无法表示x,因为线性基无法消去这一维; - 否则与这一维的基
basis[i]作异或;
void insert(ll x) {
for (ll i = n; i >= 0; --i) {
if (((x >> i) & 1ll) == 0) continue;
if (base[i] == 0) {
base[i] = x;
tot++;
return;
}
x ^= base[i];
}
uninsed = true;
}
bool check(ll x) {
for (int i = n; i >= 0; --i) {
if (((x >> i) & 1ll) == 0) continue;
if (base[i] == 0) return false;
x ^= base[i];
}
return true;
}
查询最值
查最大值的思路很简单:
- 从高位到低位遍历线性基。如果异或当前基向量能使答案变大,就选择该向量;
这一贪心成立,是因为二进制数的高位比所有低位一起对答案的贡献更大。
查最小值同理:
- 从低位到高位遍历线性基,如果出现了一个向量立刻停止;
这一贪心也成立,因为越后方的向量维护权值更大的维度;
注意,如果在插入时,当前需要插入的数 x 没有被插入,则最小值显然为 \(0\);
由于当前值 \(x\) 未被插入,则可知线性基中有一组 \(x\) 的线性表示,且 \(x\) 不在这组表示中,那么显然对于整个原数列来说最小值是0
ll qmx(ll x) {
ll res = 0;
for (int i = n; i >= 0;--i) {
res = max(res, (res ^ base[i]));
}
return res;
}
ll qmn(ll x) {
if (uninsed) return 0ll;
for (ll i = 0; i <= n;++i) {
if (base[i]) return base[i];
}
}
查询第k小
首先,根据查询最小值中的特判,我们也可以知道答案为 \(0\) 的特殊情况;
随后,这一步需要我们进一步简化线性基;
原因是原线性基的高位向量仍然携带着低位,因此按二进制枚举之后不会按单调顺序排列,不便于我们查询。
所以很自然想到要用低位向量进一步消掉高位中的低位主元,且显然从低向高简化;
经过这一步操作后,设线性基内共有 cnt 个数,则它们共可以表示出 \(2^{cnt}\) 个数。
随后根据 \(k\) 进行二进制枚举即可。
ll query(ll k) {
ll res = 0, cnt = 0;
if (uninsed && --k == 0) return 0;
for (ll i = 0; i <= n; ++i) {
for (ll j = i - 1; j >= 0; --j)
if (base[i] & (1ll << j)) base[i] ^= base[j];
if (base[i]) t[cnt++] = t[i];
}
if (k >= (1ll << cnt)) return -1;
for (ll i = 0;i < cnt;i++)
if (k & (1ll << i)) res ^= t[i];
return res;
}
合并线性基
合并两个集合时,只需把一个线性基中的所有非零向量插入另一个线性基:
void merge(const Basis& other) {
for (int i = 0; i < 64; ++i) {
if (other.basis[i] != 0) {
insert(other.basis[i]);
}
}
}
这一性质使线性基可以与线段树、树上倍增等结构结合,处理区间或树上路径的异或查询。
完整代码
struct Basis {
ll n, tot = 0;
bool uninsed = false;
vector<ll> base, t;
Basis(ll _n) : n(_n), base(n + 1), t(n + 1) {}
void insert(ll x) {
for (ll i = n; i >= 0; --i) {
if (((x >> i) & 1ll) == 0) continue;
if (base[i] == 0) {
base[i] = x;
tot++;
return;
}
x ^= base[i];
}
uninsed = true;
}
bool check(ll x) {
for (int i = n; i >= 0; --i) {
if (((x >> i) & 1ll) == 0) continue;
if (base[i] == 0) return false;
x ^= base[i];
}
return true;
}
ll qmx() {
ll res = 0;
for (int i = n; i >= 0;--i) {
res = max(res, (res ^ base[i]));
}
return res;
}
ll qmn() {
if (uninsed) return 0ll;
for (ll i = 0; i <= n;++i) {
if (base[i]) return base[i];
}
return 0ll;
}
ll query(ll k) {
ll res = 0, cnt = 0;
if (uninsed && --k == 0) return 0;
for (ll i = 0; i <= n; ++i) {
for (ll j = i - 1; j >= 0; --j)
if (base[i] & (1ll << j)) base[i] ^= base[j];
if (base[i]) t[cnt++] = base[i];
}
if (k >= (1ll << cnt)) return -1;
for (ll i = 0;i < cnt;i++)
if (k & (1ll << i)) res ^= t[i];
return res;
}// k-th smallest
void merge(const Basis& other) {
for (int i = 0; i <= n; ++i) {
if (other.base[i] != 0) {
insert(other.base[i]);
}
}
}
};

浙公网安备 33010602011771号