线性基
线性基是线性代数中“极大线性无关组”概念在算法竞赛中的应用。它主要用于处理异或(XOR)运算下的线性空间问题。
在异或运算下,一个数集可以“张成”一个向量空间。线性基就是这个空间的一组“基底”,具有以下特性:
-
线性基内的任意非空子集的异或和不为 \(0\)(线性无关)
-
原集合中的任意元素都可以由线性基中若干元素的异或和表示
-
线性基的规模不超过数的位数(通常是 \(64\))
我们要维护一组基底 \(b_1, b_2, ..., b_m\),使得原集合中所有数都能被这组基底线性表示。
核心贪心策略:插入一个数 \(x\) 时,从最高位向最低位处理。如果 \(x\) 的当前最高位对应的基底位置为空,就直接放入;否则用该基底消去 \(x\) 的这一位,继续处理。
构建步骤
设 \(b[i]\) 表示最高位为第 \(i\) 位的基底(若存在)。对于要插入的数 \(x\):
-
从高位到低位遍历(假设从 \(63\) 到 \(0\))
-
若 \(x\) 的第 \(i\) 位为 \(0\),跳过
-
若 \(b[i]\) 已存在,令 \(x = x \oplus b[i]\)(消去第 \(i\) 位)
-
若 \(b[i]\) 不存在,令 \(b[i] = x\),插入成功
-
若最终 \(x = 0\),说明 \(x\) 可以被现有基底表示,插入失败。
正确性证明
我们会有如下几个问题:
- 为什么这样处理完了以后,可以覆盖每一种情况?
证明如下:
插入分两种情况:
情况 A:\(x\) 最后被消成了 \(0\)
这说明什么?说明在消元的过程中,我们用已有的工具(\(b[a], b[b], \dots\))把 \(x\) 给“抵消”了:
移项一下(异或的移项就是两边同时异或):
这个 \(x\) 本来就能被旧工具拼出来!它是个“没新意”的工具。所以丢掉它,工具箱的能力一点没变。本来能拼出的尺寸,现在还能拼出;本来拼不出的,还是拼不出。
情况 B:\(x\) 最后被放进了 \(b[j]\) 空位
在放进空位之前,\(x\) 已经被高位的工具消过好几轮了。假设消元过程是:
最后这个 \(x'\) 就是放进 \(b[j]\) 的那个值。
那么,原来的 \(x\) 就等于:
故为什么这样处理完了以后,可以覆盖每一种情况(因为每一个数都能被表达出来)。
- 为什么处理,为什么不会重复使用元素?
我们举个例子,比如说我们 \(b[1] = a[k_1] \oplus a[k_2]\),\(b[2] = x' \oplus a[k_2] \oplus a[k_3]\),那我们 \(b[1] \oplus b[2]=a[k_1] \oplus a[k_2] \oplus a[k_2] \oplus a[k_3]\),看起来 \(a[k_2]\) 被用了两次,实际上这个 \(a[k_2] \oplus a[k_2] =0\)!他们是会抵消的,所以每一个数最多只会被用一次。
常见应用
应用一:最大异或和
问题:给定 \(n\) 个数,选出任意多个(至少一个),使它们的异或和最大。求这个最大值。
例子:给定 {1011, 1110, 1100},最大异或和是?
我们刚才模拟过,线性基能表示的子集异或值有:
0000, 0010, 0101, 0111, 1011, 1001, 1110, 1100
最大的是 1110(十进制 14)。
思路:
- 构造线性基
- 从最高位往低位贪心:如果当前答案 \(res\) 的第 \(i\) 位是 0,就异或 \(b[i]\),让它变成 1
- 因为高位 1 的价值大于所有低位 1 的总和,所以贪心最优
代码:
long long maxXor(LinearBasis &lb) {
long long res = 0;
for (int i = 63; i >= 0; i--) {
if ((res ^ lb.b[i]) > res) {
res ^= lb.b[i];
}
}
return res;
}
变式:给定一个数 \(x\),从集合中选若干个数与 \(x\) 异或,使结果最大。做法一样,初始 \(res = x\),然后从高位贪心。
应用二:最小异或和
问题:给定 \(n\) 个数,选出任意多个,使异或和最小(不能一个都不选)。
思路:
- 线性基中,最小的非零基底就是答案
- 为什么?因为所有非零异或值,一定可以由若干基底异或得到。而基底中最高位最小的那个,它本身的值就是所有可表示的非零值中最小的(因为它最高位最低,其他更高位的基底不可能把它的最高位抵消掉,反而只会让值变大)
代码:
long long minXor(LinearBasis &lb) {
for (int i = 0; i <= 63; i++) {
if (lb.b[i]) return lb.b[i]; // 第一个非零基底就是最小值
}
return 0; // 空集的情况
}
注意:如果原集合可以异或出 0(即线性基的秩小于元素个数),那最小值就是 0。
应用三:判断一个数能否被异或表示
问题:给定集合 \(S\),判断目标数 \(x\) 能否由 \(S\) 的某个子集异或得到。
思路:
- 构造线性基后,用同样的“从高到低消元”方法处理 \(x\)
- 如果 \(x\) 最终被消成 0,说明可以表示;否则不能
代码:
bool canRepresent(LinearBasis &lb, long long x) {
for (int i = 63; i >= 0; i--) {
if ((x >> i) & 1) {
if (!lb.b[i]) return false;
x ^= lb.b[i];
}
}
return x == 0;
}
应用场景:比如博弈论中判断某个局面是否能达到;或者判断某个数是否在集合的子集异或空间中。
应用四:第 \(k\) 小异或和
问题:给定 \(n\) 个数,所有非空子集的异或和排序后,求第 \(k\) 小的值。
例子:{1011, 1110, 1100} 的异或值排序为:
0000(空集), 0010, 0101, 0111, 1001, 1011, 1100, 1110
第 3 小(从 1 开始数)是 0101。
思路:
- 这需要先重建基底(化成“简化阶梯形”),使得每个 \(b[i]\) 的第 \(j\) 位(\(j \ne i\))都是 0
- 重建后,每个基底的最高位是“独占”的,任意两个基底之间没有位重叠
- 这样,所有基底的异或组合就像一个二进制数:选不选 \(b[i]\) 对应第 \(i\) 位是 1 还是 0
- 第 \(k\) 小的值就是 \(k\) 的二进制表示对应的基底组合
重建代码:
void rebuild(LinearBasis &lb) {
for (int i = 63; i >= 0; i--) {
for (int j = i - 1; j >= 0; j--) {
if ((lb.b[i] >> j) & 1) {
lb.b[i] ^= lb.b[j];
}
}
}
}
第 \(k\) 小代码:
long long kthSmallest(LinearBasis &lb, long long k, int originalN) {
rebuild(lb);
vector<long long> v;
for (int i = 0; i <= 63; i++) {
if (lb.b[i]) v.push_back(lb.b[i]);
}
int r = v.size(); // 线性基的秩
// 如果原集合可以异或出 0(秩 < 元素个数),那么 0 是第 1 小
if (r < originalN) {
if (k == 1) return 0;
k--;
}
// 非零异或值有 2^r - 1 个
if (k >= (1LL << r)) return -1; // 不存在
long long res = 0;
for (int i = 0; i < r; i++) {
if ((k >> i) & 1) {
res ^= v[i];
}
}
return res;
}
为什么重建后可以这样?
重建后,\(b[i]\) 的最高位是第 \(i\) 位,且其他基底的第 \(i\) 位都是 0。这意味着每个基底都“独占”一个位。选择 \(b[i]\) 就会让答案的第 \(i\) 位变成 1,不影响其他基底独占的位。
于是,第 \(k\) 小的值就是把 \(k\) 的二进制位映射到基底的选择上。
应用五:求异或和为 0 的子集数量
问题:给定 \(n\) 个数,有多少个子集的异或和为 0?(子集可以为空)
思路:
- 这是线性基最优雅的应用之一
- 设线性基的秩为 \(r\)(即线性基中有 \(r\) 个非零基底)
- 原集合大小为 \(n\)
- 那么异或和为 0 的子集数为 \(2^{n-r}\)
为什么?
线性基的秩 \(r\) 表示原集合的向量张成的空间的维度。
原集合有 \(n\) 个向量,张成 \(r\) 维空间。对于异或和为 0 的子集,本质上是在 \(\mathbb{F}_2\) 上找一个线性相关的组合。
由线性代数知识:\(n\) 个向量张成 \(r\) 维空间,那么线性相关的组合数量是 \(2^{n-r}\)。
直观理解:
- 线性基中的 \(r\) 个向量是“自由”的,它们可以任意选择,得到 \(2^r\) 种不同的异或值
- 原集合有 \(n\) 个向量,每个向量都可以用线性基唯一表示
- 所以 \(n\) 个向量到 \(r\) 维空间有一个映射,映射的“核”(映射到 0 的那些组合)的维度是 \(n - r\)
- 核的大小就是 \(2^{n-r}\)
代码:
long long countZeroSubsets(LinearBasis &lb, int n) {
int r = 0;
for (int i = 0; i <= 63; i++) {
if (lb.b[i]) r++;
}
return 1LL << (n - r); // 包括空集
}
注意:这里的子集包括空集(空集的异或和为 0)。
应用六:线段树维护线性基(区间查询)
问题:给定一个数组,多次查询某个区间 \([l, r]\) 内所有数的最大异或和。
思路:
- 线段树每个节点维护一个线性基,表示该区间所有数的线性基
- 合并两个节点时,把一个节点的所有非零基底插入另一个节点的线性基
- 查询时,合并查询路径上所有节点的线性基
代码(合并操作):
LinearBasis merge(const LinearBasis &a, const LinearBasis &b) {
LinearBasis res = a;
for (int i = 63; i >= 0; i--) {
if (b.b[i]) res.insert(b.b[i]);
}
return res;
}
线段树部分就是标准的区间查询,把路径上的线性基合并起来。
时间复杂度:每次合并 \(O(\log^2 V)\),线段树查询 \(O(\log n)\) 个节点,总复杂度 \(O(\log n \cdot \log^2 V)\)。
应用七:线性基求交、并
并:把两个线性基的所有元素插入一个新线性基即可。
交:两个线性基的交空间。这个比较高级,常见于某些进阶题目。
思路(求交):
- 维护一个临时线性基 \(tmp\),以及一个标记数组
- 对于第一个线性基的每个基底,尝试插入到第二个线性基中,如果插入失败,说明它在两个线性基的交空间中
- 具体实现比较复杂,这里不展开
应用八:路径最大异或和(图论)
问题:给定一张无向图,边有边权,求从节点 \(s\) 到节点 \(t\) 的路径中,边权异或和最大是多少(可以重复走边)。
思路:
- 这是线性基在图论中的经典应用
- 首先,\(s\) 到 \(t\) 的任意路径异或和,等于 \(s\) 到 \(t\) 的任意一条路径的异或和,再异或上若干个环
- 因为从路径上分叉出去绕一个环再回来,路径异或和就多了一个环的值
- 所以,先找一个 \(s\) 到 \(t\) 的路径异或和 \(x\),然后用所有环的异或值构造线性基,再求 \(x\) 与线性基中某些元素异或的最大值
步骤:
- DFS 找一条 \(s\) 到 \(t\) 的路径,记录异或和 \(x\)
- DFS 过程中找出所有环的异或值,插入线性基
- 用线性基与 \(x\) 求最大异或
代码框架:
void dfs(int u, long long cur) {
vis[u] = 1;
if (u == t) pathXor = cur;
for (auto [v, w] : adj[u]) {
if (!vis[v]) {
xorTo[v] = xorTo[u] ^ w;
dfs(v);
} else {
// 找到环
lb.insert(xorTo[u] ^ xorTo[v] ^ w);
}
}
}
// 最后 ans = maxXorWith(lb, pathXor)
本文来自博客园,作者:MistyPost,转载请注明原文链接。

浙公网安备 33010602011771号