一种区间线性基维护方式的证明
区间线性基问题,即已知序列
使用下面的一种被称作“前缀线性基”的维护方式,这个问题可以做到
- 对线性基中的每个数
,同时维护一个下标 ,表示它存在的后缀。 - 在插入一个数
时(将 拓展到 ),按如下方式插入数对 :- 对线性基中每个数
,若当前 bit 存在:- 若
对应的下标 ,将 替换为 ,并继续向下插入 ; - 否则,继续向下插入
。
- 若
- 若不存在,直接在该位置插入
。
- 对线性基中每个数
- 对查询
,要求的线性基就是前缀 组成的线性基中,所有在查询区间内存在的,即 的数。
时间复杂度显然是
可以看到比起一般的线性基插入,这里只在位长相同时增加了下标的交换操作,并未改变线性基本身的正确性。
下面我们证明它对区间线性基查询的正确性。
另一种解法与一些启发
我们不妨首先看一种复杂度略差的维护方式:离线扫描右端点,我们实时维护对所有
正因为大小越来越大且存在包含关系,插入
但我们同时发现,上升的起始位置是有限的,也就是阶梯的
事实上,最上面的做法中的
正确性证明
首先显然查询得到的线性空间包含于
还需要证明查询得到的线性空间的大小是正确的。首先注意到一个位置
模板实现
template <class T, size_t B> struct LinearBase { array<T, B> base; LinearBase() { base.fill(0); } void Insert(T x) { for (int i = B - 1; i >= 0; i--) { if (x >> i & 1) { if (!base[i]) { base[i] = x; return; } else { x ^= base[i]; } } } } auto QueryMax() -> T { T res = 0; for (int i = B - 1; i >= 0; i--) { res = max(res, res ^ base[i]); } return res; } auto Contains(T x) -> bool { for (int i = B - 1; i >= 0; i--) { if (x >> i & 1) { x ^= base[i]; } } return x == 0; } auto operator+(const auto &o) -> LinearBase<T, B> { auto res = *this; for (int i = B - 1; i >= 0; i--) { res.Insert(o.base[i]); } return res; } }; template <class T, size_t B> struct PrefixLinearBase { array<T, B> base; array<int, B> pos; PrefixLinearBase() { base.fill(0); pos.fill(-1); } void InsertWithPos(T x, int p) { for (int i = B - 1; i >= 0; i--) { if (x >> i & 1) { if (!base[i]) { base[i] = x, pos[i] = p; return; } else if (pos[i] < p) { swap(base[i], x); swap(pos[i], p); } x ^= base[i]; } } } auto QueryRange(int l) -> LinearBase<T, B> { LinearBase<T, B> res; for (int i = 0; i < static_cast<int>(B); i++) { if (pos[i] >= l) { res.base[i] = base[i]; } } return res; } };
使用例
模板题:CF1100F Ivan and Burgers,已知序列
只要求出区间异或线性基,在其上查询异或空间最大值即可。
代码实现:
void GraciousMisery() { int n; cin >> n; PrefixLinearBase<int, B> b; vector<PrefixLinearBase<int, B>> bases(n + 1); for (int i = 1; i <= n; i++) { int x; cin >> x; b.InsertWithPos(x, i); bases[i] = b; } int q; cin >> q; while (q--) { int l, r; cin >> l >> r; cout << bases[r].QueryRange(l).QueryMax() << '\n'; } }
搬到树上:CF1902F Trees and XOR Queries Again,已知一棵点带权的树,查询
现在我们只要求出每个节点到它的某个祖先路径上的线性空间。容易发现此时对每个节点就是以深度为下标的序列上的问题,直接套用上面的模板就在
代码实现:
struct BinaryLifting { vector<vector<int>> fa; vector<int> dep; int lg2; BinaryLifting(vector<int> &f, vector<int> &dep) : dep(dep), lg2(__lg(f.size())) { int n = f.size(); fa.resize(lg2 + 1); fa[0] = f; for (int b = 1; b <= lg2; b++) { fa[b].resize(n); for (int i = 0; i < n; i++) { fa[b][i] = fa[b - 1][fa[b - 1][i]]; } } } auto Ancestor(int u, int diff) -> int { for (int b = lg2; b >= 0; b--) { if (diff >> b & 1) u = fa[b][u]; } return u; } auto LCA(int u, int v) -> int { if (dep[u] < dep[v]) swap(u, v); u = Ancestor(u, dep[u] - dep[v]); if (u == v) return u; for (int b = lg2; b >= 0; b--) { if (fa[b][u] != fa[b][v]) { u = fa[b][u]; v = fa[b][v]; } } return fa[0][u]; } }; constexpr int B = 20; void GraciousMisery() { int n; cin >> n; vector<int> a(n); for (int i = 0; i < n; i++) { cin >> a[i]; } vector<vector<int>> adj(n); for (int ei = 1; ei < n; ei++) { int u, v; cin >> u >> v; u--, v--; adj[u].push_back(v); adj[v].push_back(u); } vector<int> fa(n), dep(n); vector<PrefixLinearBase<int, B>> bases(n); function<void(int)> dfs = [&](int u) { bases[u].InsertWithPos(a[u], dep[u]); for (int v : adj[u]) { if (v == fa[u]) continue; fa[v] = u; dep[v] = dep[u] + 1; bases[v] = bases[u]; dfs(v); } }; dfs(0); int q; cin >> q; auto bin_lift = BinaryLifting(fa, dep); while (q--) { int u, v, x; cin >> u >> v >> x; u--, v--; int w = bin_lift.LCA(u, v); auto space = bases[u].QueryRange(dep[w]) + bases[v].QueryRange(dep[w]); if (space.Contains(x)) { cout << "YES\n"; } else { cout << "NO\n"; } } }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 干货分享!MCP 实现原理,小白也能看懂
· 精选 4 款免费且实用的数据库管理工具,程序员必备!
· Cursor:一个让程序员“失业”的AI代码搭子
· MCP开发应用,使用python部署sse模式
· 慢查询解决思路