学习笔记:线性基
线性基是一种处理 异或 问题的工具。将每个二进制数看成一个向量,值域为 \(\{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\}\),其能组合出的所有值按从小到大排列为
分别设为 \(\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
对于 \(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;
}

浙公网安备 33010602011771号