Appearance
注:线性基应当属于线性代数范畴,但是其在算法竞赛中通常用于维护异或,所以将其放在数据结构中。
算法竞赛中的线性基一般只考虑异或线性基。
线性基简单来讲就是一个整数集合的异或意义下的极大线性无关组。
具体而言,对于给定集合
从另一个角度来看,线性基将原集合的
在具体实现上,将集合中的任一元素,视作一个 01 的向量;将原集合视作一个 01 矩阵。
假设第一列是最高位。
构建:
高斯消元:
从最高位开始,对该矩形进行高斯消元,化简成最简型矩阵。
去掉全零行,最终形成的
任一能通过选择原矩阵若干行求异或和得到的向量,都能通过选择最终矩阵的若干行异或得到。
使用 bitset 维护,时间复杂度:
点击展开代码
cpp
void build(int n, vector<u64> &t) {
a.resize(n);
num.resize(64);
this->n = n;
for (int i = 1; i <= n; i++) {
a[i - 1] = t[i];
}
int now = 63;
for (int i = 0; i < n; i++) {
for (int j = i; j < n; j++) {
if (a[j][now]) {
swap(a[j], a[i]);
break;
}
}
if (!a[i][now]) {
now--;
if (now < 0)
break;
i--;
} else {
num[i] = now;
for (int j = 0; j < n; j++) {
if (j == i)
continue;
if (a[j][now])
a[j] ^= a[i];
}
now--;
if (now < 0)
break;
}
}
}贪心法:
按
实际上和高斯消元本质相同,加入
时间复杂度:
与高斯消元相比的优势有:
- 在线
- 支持回退
- 空间复杂度:
点击展开代码
cpp
void insert(u64 v) {
for (int i = 63; i >= 0; i--) {
if ((v >> i) & 1) {
if (!a[i]) {
a[i] = v;
break;
} else {
v ^= a[i];
}
}
}
}虽然一般情况下,线性基会从最高位开始消元,但实际上,因为秩的大小不一定为
求子集异或和第
性质:
以下均讨论原序列的子集异或,并将相同的异或结果只计一次。
是否可表示
如果允许选空集,那么空集的异或和始终是
如果要求选择至少一个原序列元素,那么异或和能为
下面关于“最后一行”、“所有行异或”和“下标二进制位对应基向量”的结论,都要求线性基已化为高位主元的行最简形:每个主元位只在它所在的基向量中为 insert 通常只得到行阶梯形,查询前还需进一步消元。
最小值:
若允许空集,最小值始终是
最大值:
在上述规范基中,所有非零行的异或和是最大值。对普通行阶梯基,应从高位主元到低位主元扫描,只在 res ^ basis[i] > res 时更新 res;直接异或所有行不一定正确。
第 小/大
设线性基的秩为
此时
“把下标的二进制
下面的查询代码沿用“非零行按主元从高到低排列,全零行位于末尾”的存储约定,其中 query_min 和 query_kth 按“子集必须非空”处理
点击展开代码
cpp
u64 query_max() {
u64 res = 0;
for (int i = 0; i < n; i++)
res ^= a[i].to_ullong();
return res;
}
u64 query_min() { return a[n - 1].to_ullong(); }
u64 query_kth(u64 k) {
u64 cnt = 0;
bool flag = 1;
for (int i = 0; i < n; i++) {
if (a[i].count())
cnt++;
else
flag = 0;
}
u64 res = 0;
int m = n - 1;
for (int i = n - 1; i >= 0; i--)
if (a[i].count()) {
m = i;
break;
}
if (!flag) {
k--;
}
if (!k)
return 0;
for (int i = 0; i < n; i++) {
if ((k >> i) & 1)
res ^= a[m - i].to_ullong();
}
return res;
}解的构造
对于线性基找出的解,要还原出其在原集合中由哪些数组成。
set 维护
对于线性基中的每一个行向量,直接用一个 set 维护其由原序列中哪些下标异或得到。设最终线性基的秩为
点击展开代码
cpp
void insert(int x, u64 v) {
set<int> now;
now.insert(x);
for (int i = 63; i >= 0; i--) {
if ((v >> i) & 1) {
if (!a[i]) {
a[i] = v;
pos[i] = now;
break;
} else {
v ^= a[i];
for (auto u : pos[i]) {
if (now.count(u))
now.erase(u);
else
now.insert(u);
}
}
}
}
}这份增量实现只会把“使秩增加”的输入下标保存到 pos。被当前线性基约化为 pos[i],因此所有已保存的来源下标都属于至多 pos[i] 的大小为
内层循环计算 now 与 pos[i] 的集合对称差。一次 count、erase 或 insert 的代价是 pos[i] 需要
而不是
处理
所有 pos 中保存的来源下标总数为 set 对象的空间后为
bitset 维护
因为每个行向量由且仅有它前面的行向量异或得到,而行向量插入线性基后不会被覆盖,所以可以用一个 bitset 存储每一个行向量由当前哪些行向量异或得到,同时存下每个行向量插入线性基时初始对应的值。
还原元素时,把那些行向量的 bitset 异或起来,再把最终需要的行向量的初始值异或得到最后的解。
时间复杂度:
点击展开代码
cpp
void insert(int x, u64 v) {
bitset<64> now;
for (int i = 63; i >= 0; i--) {
if ((v >> i) & 1) {
if (!a[i]) {
a[i] = v;
mp[i] = x;
pos[i] = now;
pos[i].set(i);
break;
} else {
v ^= a[i];
now ^= pos[i];
}
}
}
}线性基的合并
线性基可以用来求两个子空间的交,或者将两个子空间合并为它们的子空间和。
子空间和 / 线性基合并
令
将
而不是集合并
时间复杂度:
线性基的交
构建增广矩阵:
其中
对于增广矩阵,进行高斯消元,最后得到行最简型矩阵。
系数矩阵中的零行对应的右侧行向量就是
时间复杂度:
点击展开代码
cpp
LinearBasis intersection_basis(LinearBasis &A, LinearBasis &B)
{
struct Node
{
ull x, tag;
};
Node q[w + 1];
for (int i = 0; i <= w; i++)
q[i] = {0, 0};
auto insert_aug = [&](ull x, ull tag, LinearBasis *ans)
{
for (int i = w; i >= 0; i--)
{
if (((x >> i) & 1) == 0)
continue;
if (!q[i].x)
{
q[i] = {x, tag};
return;
}
x ^= q[i].x;
tag ^= q[i].tag;
}
if (ans && tag)
ans->insert(tag);
};
for (ull x : A.vectors())
{
insert_aug(x, 0, nullptr);
}
LinearBasis ans;
for (ull x : B.vectors())
{
insert_aug(x, x, &ans);
}
return ans;
}不同的线性基构造
高位线性基
优先保证高位消元成主元。
点击展开代码
cpp
低位线性基
与高位线性基相对,优先消低位。
因为整数的大小比较从高位开始,低位线性基不能直接套用高位线性基的最大值贪心,但这不代表它无法回答与大小相关的查询。可以把其至多
因此,低位线性基只是不适合直接进行高位贪心,而不是丢失了原线性空间的信息。
任意主元顺序
更一般地,如果题目有相应的约束要求,可以按照任意顺序消主元。
最简线性基 / 规范线性基
前面朴素维护的高位/低位线性基,本质是行阶梯型的线性基,对于不同的插入顺序,得到的线性基也可能不同。
但是最简线性基唯一。
主要用于线性基去重 / 判断本质相同时使用。
本质是用高斯消元消成的行最简型。
可以由任一线性基通过高斯消元得到,时间复杂度:
也可以直接在插入时维护行最简型。
点击展开代码
cpp
区间线性基
线段树维护
线段树上每个节点维护一个线性基,push_up 合并两个线性基。
预处理时间复杂度:
查询合并
空间复杂度:
支持修改,时间复杂度:
点击展开代码
cpp
ST 表维护
每次需要合并两个线性基,预处理时间复杂度:
空间复杂度:
查询合并两个线性基,时间复杂度:
相交猫树没有时空优势,唯一优势是好写。
点击展开代码
cpp
猫树维护
因为猫树是向线性基中加入元素,所以单次操作是
猫树共有
查询合并两个线性基,时间复杂度:
共存
点击展开代码
cpp
前缀线性基
前缀的本质不同线性基只有
个。
维护
查询
预处理时间复杂度:
询问时间复杂度:
空间复杂度:
优化
实际上,可以发现如果新插入的元素改变了线性基,在不化成行最简型的情况下,只会改变一个行向量。
那么每一个行向量尽可能用最靠后的元素。
处理
如果是高位线性基,那么需要替换能够替换的最高位。否则得到的可能是一个任意主元线性基。
时间复杂度:
空间复杂度:
点击展开代码
cpp
分块维护
序列分块
维护
时间复杂度:
空间复杂度:
此时,空间复杂度为:
时间复杂度较劣,优势是空间优秀。
点击展开代码
cpp
在线莫队
预处理
询问时,加入
时间复杂度:
空间复杂度:
此时,空间复杂度为:
点击展开代码
cpp
实现对比
下表中,
| 实现 | 预处理时间 | 单次询问时间 | 空间复杂度 | 优点 | 缺点 / 适用场景 | |
|---|---|---|---|---|---|---|
| 线段树维护 | 通用性最好,支持单点修改,空间只多一个线段树常数。 | 静态询问下时间不占优,每次查询要合并 | ||||
| ST 表维护 | 思路和实现简单,两个重叠区间的线性基合并即可。 | 只支持静态序列;相比猫树,预处理更慢、空间相同、询问不更优。 | ||||
| 猫树维护 | 静态区间询问较适合,预处理只需要不断插入元素,比 ST 表少一个 | 空间较大;不支持修改;询问仍要合并两个线性基。 | ||||
| 前缀线性基 | 查询很快,利用前缀本质不同线性基只有 | 空间较大,实现需要维护每个前缀的多个本质不同线性基;只适合静态序列。 | ||||
| 前缀线性基优化 | 静态区间询问通常最优秀,时空都接近最优,实现好后常数也小。 | 需要维护“每个主元尽量靠后”的位置约束;不如线段树通用,不能直接支持修改。 | ||||
| 序列分块 | 空间最省,块内暴力加入元素即可;需要时可以重构单块支持修改。 | 查询较慢,块长需要调参;当 | ||||
| 在线莫队 | 查询不用删除元素,适合静态且询问较多的场景;可在线回答询问。 | 预处理和空间压力较大,块长需要平衡;当 |
处理线性基的不可删
线性基容易维护加入一个元素,但是不容易维护删除一个元素。
线段树直接维护
开一棵大小是
时间复杂度:
线段树分治
设时间轴上有
插入线性基时,普通行阶梯型实现只会修改至多一个主元位置,每次成功修改只需记录
说明操作是可以暴力撤销的。
普通线性基的一次插入需要
若在至多
普通行阶梯型实现的空间复杂度为
其中
