学习笔记:线性基

线性基是一种处理 异或 问题的工具。将每个二进制数看成一个向量,值域为 \(\{0,1\}\),即向量 \(\alpha_i\in\mathbb F_2^n\),而向量加法就是模 \(2\) 意义下每个元素的加法,也就是异或。这时,生成 \(\{\alpha_1,\cdots,\alpha_k\}\) 的线性无关组称为 \(\{\alpha_1,\cdots,\alpha_k\}\)线性基

换句话说,\(S=\{\alpha_1,\cdots,\alpha_k\}\) 的线性基是一个集合 \(B=\{\beta_1,\cdots,\beta_l\}\),使得对任意 \(S\) 中元素的组合,都能找到 \(S'\) 中元素的组合,使这两个组合异或和相等。

显然,如果值域为 \([0,V)\),那么线性基的大小不超过 \(\lceil\log_2V\rceil\)

构造线性基的算法

方法一

将每一个 \(x\in S\) 不断试着插入线性基 \(B\),如果由现有的线性基可以生成 \(x\),那么就不用插入,否则插入。

具体来说,枚举 \(x\),考虑其 最高有效位(最高的非零位)\(i\),如果线性基中没有第 \(i\) 位为 \(1\) 的,那么插入 \(x\),否则设 \(b_i\) 为线性基中第 \(i\) 位为 \(1\) 的,令 \(x\gets x\oplus b_i\)(表示想要组成 \(x\) 一定需要 \(b_i\),还需要的是 \(x\oplus b_i\)),然后继续试着插入 \(x\) 到线性基中。

ull B[55];
void ins(ull x) {
    for (int j = 50; ~j; j--) {
        if (x >> j & 1) {
            if (B[j]) x ^= B[j];
            else return B[j] = x, void();
        }
    }
    return;
}

方法二

先直接给出算法:

vector<ull> B;
void ins(ull x) {
    for (auto b: B) x = min(x, b ^ x);
    if (!x) return; // 插入失败
    for (auto &b: B) b = min(b, b ^ x);
    B.push_back(x);
    return;
}

算法的解释如下:

  • 第一个 for:尽量用当前线性基中的数来组成 \(x\),设执行完 for 循环后 \(x\) 变为 \(x'\)
  • if 语句:如果 \(x'=0\),说明用当前线性基可以组成 \(x\),那么 \(x\) 就没必要插入线性基。
  • 第二个 for:如果 \(x'\ne0\),说明用当前的线性基无法组成 \(x\),现在剩下的 \(x'\) 就是线性基需要的。
    为了优化线性基的结构,我们将线性基的每个元素中所有不必要的成分去掉,这类似于高斯消元的「向后步骤」。
    比如当前的线性基为 \(\{10111\}\),而现在要插入的是 \(00110\),那么我们可以把线性基先变成 \(\{10001\}\),然后再加入 \(00110\)
    如果要再加入 \(00011\) 的话,就把线性基变成 \(\{10001,00101\}\),再加入 \(00011\)
    也就是说,如果当前加入的 \(x\) 负责线性基中的更低位,那么负责更高位的那些元素就没有必要包含 \(x\) 负责的更低位。

这样构造出的线性基有一个很好的性质:对于线性基中每一个元素 \(b\) 的最高有效位 \(i\),不存在线性基中其他元素的第 \(i\) 位也为 \(1\)

由此,我们可以在 \(O(n\log V)\) 的时间内构造出 \(a_1,\cdots,a_n\) 的线性基,其中 \(V\) 为值域大小。

其他

第二种线性基不使用 vector 的实现:

ull B[55];
void ins(ull x) {
    for (int i = 0; i <= 50; i++) x = min(x, B[i] ^ x);
    if (!x) return; // 插入失败
    int pos = -1;
    for (int i = 50; ~i; i--) {
        if (B[i]) B[i] = min(B[i], B[i] ^ x);
        else if (!(~pos) && (x >> i & 1)) pos = i;
    }
    B[pos] = x;
    return;
}

这种实现的方便之处在于,可以快速地找到第 \(i\) 位为最高位 \(1\) 的元素 \(b_i\)(如果存在的话)。

另外,求两个集合的并的线性基,可以将一个集合的线性基的所有元素插入到另一线性基中。

线性基的应用

查询异或最小值

线性基的构造过程,保证了其中没有两个最高有效位相同的数。因此答案就是整个线性基中的最小值。

查询异或最大值

贪心地令高位最大,如果当前答案的第 \(i\) 位为 \(0\),就异或 \(a_i\),否则跳过这一位。

ull get_max() {
    ull ans = 0;
    for (int i = 50; ~i; i--) {
        if (!(ans >> i & 1)) ans ^= a[i];
    }
    return ans;
}

对于第二种方法构造出的线性基,对于线性基能构造出的每一位 \(i\),由于只有一个数 \(b_i\) 能令这一位为 \(1\),并且 \(b_i\) 不会影响到更高位,因此只需求出线性基中所有元素的异或和,即为答案。

ull get_max() {
    ull ans = 0;
    for (auto b: B) ans ^= b;
    return ans;
}

查询异或 \(k\) 小值

查询异或 \(k\) 小值就需要用到第二种方法所构造的线性基。

对于第一种线性基,假设为 \(\{10110,00111,00001\}\),那么它们负责第 \(4\) 位(\(2^4\) 对应的位)、第 \(2\) 位、第 \(0\) 位。但是,\(10110\oplus00111=10001\)\(10110\) 要小,而 \(10110\oplus00001=10111\)\(10110\) 大,这样就导致了我们无法确定各个线性组合的大小顺序。

然而,由于第二种线性基的良好性质,假设为 \(\{00001,00110,10000\}\)(为了方便,我们按从小到大排列),设最高有效位为 \(i\) 的元素为 \(b_i\),那么包含 \(b_i\) 的任一组合一定大于 \(b_0,\cdots,b_{i-1}\)(如果存在的话)的所有组合,这是因为包含 \(b_i\) 的组合的第 \(i\) 位一定为 \(1\),而用 \(b_0,\cdots,b_{i-1}\) 无论如何也组合不出来第 \(i\)\(1\)

同时,如果线性基包含 \(l\) 个元素,那么一共有 \(2^l\) 种线性组合。因此,设 \(k\) 的二进制第 \(d_1,\cdots,d_s\) 位为 \(1\),那么第 \(k\) 小值即为线性基中第 \(d_1,\cdots,d_s\) 个元素的组合。

例如,对于线性基 \(B=\{00001,00110,10000\}=\{\beta_0,\beta_1,\beta_2\}\),其能组合出的所有值按从小到大排列为

\[00000,00001,00110,00111,10000,10001,10110,10111. \]

分别设为 \(\alpha_0,\cdots,\alpha_7\)。现求第 \(k=6\) 小值,如果从 \(0\) 开始数就是第 \(5\) 小值。由于 \(5=(101)_2\) 的第 \(0\) 和第 \(2\) 位为 \(1\),所以 \(B\) 能组合出的第 \(5\) 小值为 \(\alpha_5=\beta_0\oplus\beta_2=10001\)

例题

下面都采用第二种线性基。

HDU-3949 XOR

vjudge

对于 \(a_1,\cdots,a_n\),选择其中若干个(不可以不选)进行异或,求可能的结果中的第 \(k\) 小值。

\(T\) 组数据,每组数据给定 \(a_1,\cdots,a_n\)\(q\) 次询问 \(k\)

\(1\le T\le30\)\(1\le n,q\le10000\)\(1\le a_i,k\le10^{18}\)

虽然不能不选,但是选集合中的多个元素仍然可能组合出 \(0\)

注意到,如果线性基 \(B\) 的大小 \(|B|\) 小于原集合的大小 \(n\),那么说明原集合中有 \(x\notin B\) 可以被 \(B\) 中元素 \(b_{p_1},\cdots,b_{p_k}\) 表示,进而 \(b_{p_1}\oplus\cdots\oplus b_{p_k}\oplus x=0\),即可以选择若干个数,异或和为 \(0\)

因此,如果 \(|B|=n\),则可能的结果中不包含 \(0\);如果 \(|B|<n\),则包含 \(0\)

void solve() {
    int n; cin >> n;
    vector<ull> B;
    for (int i = 1; i <= n; i++) {
        ull x; cin >> x;
        ins(x, B);
    }
    sort(B.begin(), B.end());
    int q; cin >> q;
    while (q--) {
        ull k; cin >> k;
        if (B.size() < n) --k; // 原集合选若干个可以异或出 0
        ull ans = 0;
        for (auto b: B) {
            if (k & 1) ans ^= b;
            k >>= 1;
        }
        if (!k) cout << ans << '\n';
        else cout << "-1\n"; // 总组合数不够 k
    }
    return;
}

洛谷-P4869 albus就是要第一个出场

上辈子写的题解 改了一下。

给定一个长度为 \(n\) 的序列 \(A\),设 可重 集合 \(S=\left\{\operatorname{xor}_{i=1}^nA_ix_i\mid x_i\in\{0,1\}\right\}\),即 \(S\)\(A\) 的所有子集的异或和构成的集合。

给定一个数 \(k\),求 \(k\)\(S\) 中的排名。如果 \(S\) 中有多个 \(k\),取最小的排名。

\(1\le n\le10^5\),其他输入不超过 \(10^9\)

首先构造 \(A\) 的线性基 \(B\),设 \(\operatorname{span}(B)=\operatorname{span}(A)=S\)。由于可以一个数都不选,所以 \(0\in S\)

如果集合 \(S\) 不可重,给定一个数 \(k\),如何求出它在 \(S\) 中的排名?保证 \(k\)\(S\) 中出现。

我们从低到高考虑每一位。如果 \(B_i\) 不为空,并且 \(k\) 的第 \(i\) 位为 \(1\),说明我们需要取出 \(B_i\) 以构造出 \(k\)。根据线性基的性质,\(B_i\) 的高位为 \(0\),且 \(B\) 中其他数的第 \(i\) 位为 \(0\),所以 必须取出 \(B_i\),且取出 \(B_i\) 不影响之前和之后的操作。至于其他没考虑到的位,由于保证可以构造出 \(k\),因此可以不用管。

求排名 \(rk\) 的具体代码如下:

int rk = 0, pw = 1;
f(i, 0, 30) if (B[i]) {
    if (k >> i & 1) rk += pw;
    pw <<= 1;
}

取出线性基的第 \(i\) 个元素,对应的贡献是 \(2^{i-1}\)。可以对照一个例子:

  • 线性基为 \(\{00100,01001,10011\}\),能构造出的数的集合为 \(\{00100,01001,01101,10011,10111,11010,11110\}\)

现在考虑 集合 \(S\) 可重 的情况。设 \(f(s)=\operatorname{xor}_{s}x\),其中 \(s\) 为某个集合。

考虑插入线性基的过程,出现了重复即是插入线性基失败。

设一个插入线性基失败的数为 \(x\),那么存在 \(B'\subseteq B\) 使得 \(f(B')\operatorname{xor}x=0\)。设 \(x\) 对应的 \(B'\)\(B_x\)

根据异或的性质,如果用线性基中的数能构造出 \(y\),设 \(y=f(Y)\)\(Y\subseteq A\)),那么 \(y\) 也等于 \(f(Y)\operatorname{xor}f(B_x)\operatorname{xor}x\),其中 \(x\) 是某个插入线性基失败的数。

对于每个 \(y\),用线性基中的数构造的方案是唯一的(否则不符合线性基的定义);而 \(x\) 对应的 \(B_x\) 也是唯一的。因此要构造 \(y\),对于每个不在线性基中的 \(x\) 都可以用或不用,所以每个 \(y\) 的总方案数都要乘以 \(2^{n-|B|}\)

于是答案为 \(rk\times2^{n-|B|}+1\)

实现细节:

  • \(2^{n-|B|}\) 可以在插入线性基失败的同时计算出来,这样就不需要用到快速幂了。
  • \(rk<2^{31}\),所以可以最后再取模。
#include <bits/stdc++.h>
#define f(x, y, z) for (int x = (y); x <= (z); ++x)
#define g(x, y, z) for (int x = (y); x >= (z); --x)
using namespace std;
int constexpr MOD = 10086;
int n, rk, pw = 1, B[32], c = 1;

void ins(int x) {
    f(i, 0, 30) x = min(x, B[i] ^ x);
    if (!x) return (c <<= 1) %= MOD, void();
    int pos = -1;
    g(i, 30, 0) {
        if (B[i]) B[i] = min(B[i], B[i] ^ x);
        else if (!(~pos) && (x >> i & 1)) pos = i;
    }
    B[pos] = x;
    return;
}

signed main() {
    cin >> n;
    int x; 
    f(i, 1, n) cin >> x, ins(x);
    cin >> x;
    f(i, 0, 30) if (B[i]) {
        if (x >> i & 1) rk += pw;
        pw <<= 1;
    }
    rk %= MOD;
    cout << (rk * c + 1) % MOD << '\n';
    return 0;
}

todo

参考资料

posted @ 2025-07-22 13:00  f2021ljh  阅读(69)  评论(0)    收藏  举报