线性基
学线性基之前,先了解下线性基可以解决哪些问题:
线性相关与线性无关:对于 \(\forall 向量a_{1},a_{2},...,a_{n}\),若存在一组 \(\{k_{1},k_{2},...,k_{n}\}\) 使得 \(\sum_{i=1}^{n} k_{i}a_{i} = 0\),则称向量 \(a_{1},a_{2},...,a_{n}\) 线性相关,否则线性无关。
线性基定义
(!!注意)异或空间线性基 只能表示 原集合的子集的非0异或空间 \(\Rightarrow\) 异或线性基并不能反映原集合能否异或出0 \(\Rightarrow\) 需要单独标记
线性基的性质
- 关于每一个二进制位均有一个线性基,故线性基的大小是 \(O(\log)\) 的。
- 线性基的任意两个不同的非空子集,异或值一定不同
线性基中任意非空子集的异或和不为 0 的证明:反证法,假设存在线性基的某个非空子集异或和 \(b_{1} \oplus b_{2} \oplus ... \oplus b_{k} = 0\),则说明 \(b_{1} = b_{2} \oplus ... \oplus b_{k}\),则 \(b_{1}\) 就可以用 \(b_{2} \oplus ... \oplus b_{k}\) 表示出来,没有必要放入线性基中。
构造异或线性基的两种方法
高斯消元法
// 高斯消元法 构造线性基
int n;
int k; // k表示当前构造线性基的个数
// 注意高斯消元法涉及到a内元素位置的交换,最终构造出的线性基数组就是a
ll a[55]; // index0
void solve()
{
cin >> n;
for(int i = 0; i < n; i ++){
cin >> a[i];
}
for(int c = 50; c >= 0; c --){
for(int i = k; i < n; i ++){
if(a[i] >> c & 1){
swap(a[i], a[k]);
break;
}
}
if(!(a[k] >> c & 1)) continue;
for(int i = 0; i < n; i ++){
if(i != k && (a[i] >> c & 1)){
a[i] ^= a[k];
}
}
k ++; if(k == n) break;
}
}
贪心法
// 贪心法 构造线性基
int n;
ll a[55];
ll basis[55]; // 与高斯消元不同,可以单开一个数组记录线性基
bool insert(ll x){
for(int c = 50; c >= 0; c --){
if(x >> c & 1){
if(basis[c] == 0){
basis[c] = x;
return true;
}
else{
x ^= basis[c];
}
}
}
return false;
}
void solve()
{
memset(basis, 0, sizeof basis);
cin >> n;
for(int i = 0; i < n; i ++){
cin >> a[i];
insert(a[i]); // 依次插入
}
}
两种构造方法的区别之处:
// 贪心法重新构造线性基,让每一个二进制高位都有唯一的1,以处理求子集异或和第k小问题
void reconstruct(){
for(int i = 60; i >= 0; i --){
for(int j = i - 1; j >= 0; j --){
if(basis[i] && basis[j]){
assert(basis[i] > basis[j]);
if(basis[i] >> j & 1){
basis[i] ^= basis[j];
}
}
}
}
}
恢复构造线性基的原数集方案
有时,我们需要确定对于给定异或和,可以由原数集中的哪些数异或得到,即确定一种构造方案。
在判断是否可以得到该异或和的过程中,我们能很容易确定关于线性基的方案,即选择线性基中的哪些向量来得到该异或和。因此,我们只需要确定每个线性基向量可以由原数集中的哪些数异或得到,这可以结合 bitset 快速计算,即每个线性基向量对应一个总位数为原数集大小的 bitset。
vector<int> basis(31);
vector<bitset<N>> f(31, bitset<N>(0));
auto insert = [&](int x, int id){
bitset<N> now(0);
now.set(id); // 当前的 x 正是 a[id],因此只有第 id 个低二进制位为 1
vector<bool> used(31, false);
for(int c = 30; c >= 0; c --){
if(x >> c & 1){
if(!basis[c]){
basis[c] = x;
// 只有在真正插入到线性基中时,才进行 bitset 操作,这样最多进行 logA * logA 次操作,复杂度 O(logA * logA * (n / w))
for(int k = 0; k < 30; k ++){
if(used[k]) now ^= f[k];
}
f[c] = now;
break;
}
else{
x ^= basis[c];
used[c] = true;
}
}
}
};
vector<int> a(n + 1); // 原数集,编号从0开始
for(int i = 0; i < n; i ++){
insert(a[i], i);
}
线性基的应用
判断某个数 \(x\) 是否能由某个数集的子集异或出来
两种解法:
- 对于数字 x,从高位到低位扫,若 x 在当前二进制位为 1,则异或上该位上的线性基;若最终 x = 0,则说明可以异或出来。
- 看 x 关于线性基的异或最小值是否为 0。
bool check(ll x){
for(int c = 60; c >= 0; c --){
if(x >> c & 1){
x ^= basis[c];
}
}
return x == 0;
}
求某个数集的所有子集的异或和第k小
// 利用线性基求某集合所有子集的第k小异或和:
// 首先要明确,原集合中所有子集的异或集合 与其 异或线性基中的所有子集的异或集合 是一样的
// 注意:必须用高斯消元法,因为用高斯消元法求得的线性基中的每个元素在其对应最高位上可以保证有唯一的1
// 这样可以发现选取线性基中的子集构造出的元素单调性 和 线性基中编号 呈现一种二进制上的单调性(细细品味)
// 因此,求第k小 就可以直接用 k的二进制表示得到线性基子集编号 对应的子集异或值
// 同时要注意,这种技巧得到的第k小只是非0的第k小(选取线性基中的任意非空子集,异或和一定不为0)
// 而实际上,原数组中的某个子集异或后的结果可能是0,0一定是最小的
// 因此还需要考虑原数组的子集是否还可以异或出来0:
// 直接看线性基大小是否等于原集合大小,若等于,则不能;否则能。(?未懂,待补)
int n;
int k;
ll a[10010];
int q;
// HDU 3943
void solve()
{
k = 0; // 多测,需要初始化线性基指针
cin >> n;
for(int i = 0; i < n; i ++){
cin >> a[i];
}
// 高斯消元法求线性基
for(int c = 61; c >= 0; c --){
for(int i = k; i < n; i ++){
if(a[i] >> c & 1){
swap(a[i], a[k]);
break;
}
}
if(!(a[k] >> c & 1)) continue;
for(int i = 0; i < n; i ++){
if(i != k && (a[i] >> c & 1)){
a[i] ^= a[k];
}
}
k ++; if(k == n) break;
}
cin >> q;
while(q --){
ll x; cin >> x; // 求异或和第x小
if(k < n){ // 原集合可以异或出0
x --;
}
if(x >= (1ll << k)){ // 线性基最多只能异或出(2^k - 1)个非0值
cout << -1 << endl;
continue;
}
ll ans = 0;
for(int i = k - 1; i >= 0; i --){
if(x >> i & 1){
ans ^= a[k - i - 1];
}
}
cout << ans << endl;
}
}
求一个集合的所有子集的异或和方案数:
线性基合并
对于两个集合 \(a, b\),均有各自的线性基 basis1, basis2。那么这两个集合合并后的线性基,等价于将这两个集合的线性基合并。而线性基的大小是 \(O(\log)\) 级别的,特别小,因此我们可以直接将其中一个线性基中的 \(\log A\) 个数依次插入到另一个线性基中,以实现线性基的合并,复杂度 \(O(\log^{2}A)\)。对于多个集合同理。
求区间线性基 —— 利用前缀线性基
!!!注意,前缀线性基只能解决区间异或线性基的异或最值问题,也就是说,只有求解的是区间异或最大值/最小值时才能使用前缀线性基。
// 前缀线性基
ll p[100005][65];
int pos[100005][65];
void insert(ll x, int id){
for(int c = 0; c <= 60; c ++){
p[id][c] = p[id - 1][c];
pos[id][c] = pos[id - 1][c];
}
int P = id;
for(int c = 60; c >= 0; c --){
if(x >> c & 1){
if(!p[id][c]){
p[id][c] = x;
pos[id][c] = P;
break;
}
else{
if(pos[id][c] < P){ // 贪心地让更高二进制位上的线性基的位置更靠后
swap(p[id][c], x);
swap(pos[id][c], P);
}
x ^= p[id][c];
}
}
}
}
string query(int l, int r, ll x){ // 求解区间 [l, r] 内的线性基关于 x 的答案(求区间异或最小值)
for(int c = 60; c >= 0; c --){
if(pos[r][c] >= l) x = min(x, x ^ p[r][c]);
}
return x;
}
线段树动态维护区间线性基
当 \(n\) 较小时,可以考虑使用线段树动态维护区间线性基(单点异或,区间查询)。
核心思想:对于线段树的每个结点,维护该结点表示区间的线性基。
- 对于单点修改,在修改叶节点的值后,向上 pushup 的过程中需要清空当前结点的线性基,并进行左右子节点的线性基合并然后赋值;但是直接暴力合并的话常数会非常大,建议采用下面 pushup 函数的方式来合并,可以避免调用更多的 insert。
void pushup(int p){
memset(treebasis[p], 0, sizeof treebasis[p]);
for(int c = 30; c >= 0; c --){
if(treebasis[lc][c]) treebasis[p][c] = treebasis[lc][c];
else if(treebasis[rc][c]) treebasis[p][c] = treebasis[rc][c];
}
for(int c = 30; c >= 0; c --){
if(treebasis[lc][c] && treebasis[rc][c]) insert(treebasis[p], treebasis[rc][c]);
}
}
- 对于区间查询,任意区间在线段树中一定会被拆分成 \(O(\log)\) 个子区间,相当于 \(O(\log)\) 个数集的线性基合并)
两种操作的复杂度均为 \(O(\log^{2}A)\)
可删除线性基
需要明确一点:已经插入线性基的元素是无法被实际删除或者抵消的。这里的“删除”指的是通过离线处理,避免从线性基中实际删除元素。
例题
P3265
向量空间线性基模板题。
P4839
线性基合并 + 线段树
Q:\(n\) 个位置,每个位置上有一个桶。初始时所有桶为空。维护两种操作:
- \(1\space k\space x\):往第 \(k\) 个桶放入数字 \(x\)
- \(2\space l\space r\):从桶 \([l\backsim r]\) 中任意拿数字,数字集合的最大异或和。
构建桶序列的一棵线段树,对于线段树的每个结点维护一个线性基(最多 \(32\) 个 \(int\),空间够用)。操作 \(1\) 相当于从根节点到相应桶所在的叶节点路径上的线性基均加入 \(x\);对于操作 \(2\),每个区间 \([l, r]\) 在线段树上可以拆分成 \(O(\log)\) 个子区间,取出这些子区间的线性基组合在一起,就是整个区间内所有桶中的数字的线性基。
总复杂度来自于操作 \(2\),因为每个查询需要找到 \(O(\log n)\) 个子区间,每个区间中的线性基中的 \(32\) 个数都要插入到当前线性基中,最终形成这些子区间的复合线性基,因此每次操作 \(2\) 的复杂度为 \(O(\log n * 32^{2})\)。
P3292
利用前缀线性基思想解决树上路径最大异或和问题。
Q:给定一棵大小为 \(n\) 的树,每个点上有数字。多次查询给定路径 \(path(a, b)\),在这条路径上任意选数字,能得到的最大异或和。
我们考虑对于每个结点 \(u\),构建从根结点到 \(u\) 的路径上所有元素的线性基(可以理解为前缀线性基),并且贪心地让线性基中每一个二进制位对应值在树中相应结点的深度尽可能地大。做路径查询时,将 \(path(a, b)\) 拆分成 \(path(a, lca(a,b))\) 和 \(path(b, lca(a, b))\) 两部分,并将这两个部分的线性基合并,可以得到整条路径的线性基。对于 get 到 \(path(a, lca(a,b))\) 这条纵链的线性基,我们观察线性基中每个值的相应深度是否 \(\geq lca(a, b)\) 的深度,若成立则可以将该值插入到正在合并的线性基中,否则不能。
这里只写一下我的感性理解,严谨的证明等以后再深究:我们欲从线性基中构造最大异或和,那么肯定是想让线性基中二进制高位能取到的可能性尽可能大。而当前结点的前缀线性基是从其父亲的前缀线性基 copy 过来,在插入自己当前结点的值获得的,且自身结点的深度也是线性基中所有值代表结点深度最大的。因此,我们需要贪心地让线性基中二进制更高位上的数字对应结点的深度尽可能大,之后在做关于以该结点为端点的路径查询时,让更多的路径能在二进制高位上构造出 \(1\),进而使结果更优。
P4151
Q:给定 \(n\) 个点,\(m\) 条边的无向连通图,每条边有权值。找到 \(1\) 到 \(n\) 的某一条路径,该路径可以重复经过某些点或边,多次经过某条边时异或也要计算多次,满足该路径的路径异或和最大。
本题的结论更重要一些,证明比较困难,因此先引出结论:\(1 \rightarrow n\) 的所有路径的最大异或和,等于 \(1 \rightarrow n\) 的某一条简单路径异或和 与 图中所有环的异或和 构成的线性基的最优解。当 \(1\) 到 \(n\) 的简单路径只有唯一一条时,如下图:

这个结论是显然成立的。
但是当 \(1\) 到 \(n\) 存在多条简单路径时,如下图:

存在 \(1 \rightarrow 5 \rightarrow 6 \rightarrow 10\) 以及 \(11 \rightarrow 12 \rightarrow 16\) 多条简单路径,我们无法保证先 dfs 的是哪条路径,也不可以全部都遍历一次取最优解,不然复杂度就不对了。 但是,我们从 \(1\) 出发整个 \(dfs\) 过程中,总是能找到所有环及其异或和(具体实现见 code),并还有 \(1\) 到 \(n\) 的某一条简单路径的异或和。而对于从 \(1\) 到 \(n\) 的任意两条不同简单路径,它们一定是构成环的。这个环一定已经被我们搜到过并且其异或和已经被插入到线性基中去了。那么假设最优路径是 \(a\),其他任何一条路径是 \(b\),这两条路径形成的环的异或和 \(xorsum[a] \oplus xorsum[b]\) 一定在线性基中,若我们先搜的不是最优路径 \(a\) 而是其他某条路径 \(b\),对于 \(b\) 得到的解,一定可以通过再异或上这个环的异或和得到 \(a\) 的最优解。至此得证上述结论。
最后要注意一下,\(pathxor[n]\) 是必选的,不能与环的异或和一样插入到线性基中。对于必须要异或上某个数 \(x\) 得到关于某个线性基的最大异或和,将初始答案 \(ans\) 设置为 \(x\) 再进行求解就行了。具体实现见 code。
拓展:如果是求从点 \(1\) 出发,回到点 \(1\) 的所有路径的最大异或和,就相当于从图中所有环组成的线性基中求最优解(所有路径上的边必然来回一次,对于异或运算不产生贡献)
P3733
可删除线性基
P11620
线段树 + 线性基合并
Q:维护一个序列 \(a\) 的 区间异或 与 区间内所有子集的最大异或和 的多查询问题。
对于区间异或,显然不能往区间中的每个叶节点表示数集均插入 \(x\)。由于异或运算具有差分性质,因此考虑利用差分将区间异或转化成两次单点异或。
设 \(b_{i} = a_{i} \oplus a_{i-1}\),考虑维护序列 \(b\) 的线性基,那么对于 \(a\) 的区间修改,就等价于 \(b\) 的两次单点修改。
而对于区间查询 \(a_{l\backsim r}\),我们会发现:\(a_{l\backsim r}\) 与 \(a_{l}\space\cup\space b_{l+1\backsim r}\) 的异或线性基是等价的(数集均为 \(a_{l\backsim r}\)),而我们可以动态维护 \(b_{l+1\backsim r}\) 的线性基,那么得到该线性基后,再单独插入 \(a_{l}\) 即为我们需要的线性基,贪心法查询子集异或最大值即可。
2026牛客寒假训练营3 I
需要得到线性基中每个向量可以由原数集里的哪些数异或得到。











浙公网安备 33010602011771号