线性基

线性基是线性代数中“极大线性无关组”概念在算法竞赛中的应用。它主要用于处理异或(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 \oplus b[a] \oplus b[b] \cdots = 0 \]

移项一下(异或的移项就是两边同时异或):

\[x = b[a] \oplus b[b] \dots \]

这个 \(x\) 本来就能被旧工具拼出来!它是个“没新意”的工具。所以丢掉它,工具箱的能力一点没变。本来能拼出的尺寸,现在还能拼出;本来拼不出的,还是拼不出。

情况 B:\(x\) 最后被放进了 \(b[j]\) 空位

在放进空位之前,\(x\) 已经被高位的工具消过好几轮了。假设消元过程是:

\[x' = x \oplus b[k_1] \oplus b[k_2] \dots \]

最后这个 \(x'\) 就是放进 \(b[j]\) 的那个值。

那么,原来的 \(x\) 就等于:

\[x = x' \oplus b[k_1] \oplus b[k_2] \dots \]

故为什么这样处理完了以后,可以覆盖每一种情况(因为每一个数都能被表达出来)。

  • 为什么处理,为什么不会重复使用元素?

我们举个例子,比如说我们 \(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\) 与线性基中某些元素异或的最大值

步骤:

  1. DFS 找一条 \(s\) 到 \(t\) 的路径,记录异或和 \(x\)
  2. DFS 过程中找出所有环的异或值,插入线性基
  3. 用线性基与 \(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)
posted @ 2026-08-25 16:30  MistyPost  阅读(9)  评论(0)    收藏  举报