可持久化数据结构学习笔记
可持久化线段树
概述
又称主席树。当我们想要保存线段树上的一些历史版本信息时,直接给每一个版本都开一个线段树显然是吃不消的,考虑优化。以版本之间单点修改为例,容易发现插入一个点最多会有
这里给出一种基本结构的实现方式:
struct Node {
int lc, rc;
int sm;
} e[N * 100];
#define lc(i) e[i].lc
#define rc(i) e[i].rc
#define sm(i) e[i].sm
void push_up(inr p) {
sm(p) = sm(lc(p)) + sm(rc(p));
}
int update(int p, int l, int r, int x, int vl) {
int q = ++tot;
e[q] = e[p];
if (l == r) return sm(p) += vl, q;
int mid = (l + r) >> 1;
if (x <= mid) lc(q) = update(lc(q), l, mid, x, vl);
else rc(q) = update(rc(q), mid + 1, r, x, vl);
push_up(q);
return q;
}
区间修改
普通的线段树区间修改采用懒标记维护,但是主席树我们通常使用标记永久化的方式实现区间修改。这样修改是不需要 push_up
的。
int update(int p, int l, int r, int ql, int qr, int vl) {
int q = ++tot;
e[q] = e[p], sm(q) += (min(qr, r) - max(ql, l) + 1) * vl;
if (ql <= l && r <= qr) {
tg(q) += vl;
return q;
}
int mid = (l + r) >> 1;
if (ql <= mid) lc(q) = update(lc(q), l, mid, ql, qr, vl);
if (qr > mid) rc(q) = update(rc(q), mid + 1, r, ql, qr, vl);
return q;
}
int query(int p, int l, int r, int ql, int qr, int tg) {
if (!p || l > r || l > qr || ql > r) return 0;
if (ql <= l && r <= qr) return sm(p) + (r - l + 1) * tg;
int mid = (l + r) >> 1;
return query(lc(p), l, mid, ql, qr, tg + tg(p)) + query(rc(p), mid + 1, r, ql, qr, tg + tg(p));
}
二逼平衡树
我们知道线段树+平衡树可以实现二逼平衡树,让我们用树状数组+主席树再次解决这个问题。我们用树状数组维护外层下标,修改与查询时对内层主席树的
给出代码实现:
#include <bits/stdc++.h>
#define N 50005
#define LIM 100000000
using namespace std;
int n, m;
int a[N];
int rt[N];
struct Seg {
int tot = 0;
struct Node {
int lc, rc;
int sm;
} e[N * 1500];
#define lc(i) e[i].lc
#define rc(i) e[i].rc
#define sm(i) e[i].sm
void push_up(int p) {
sm(p) = sm(lc(p)) + sm(rc(p));
}
int update(int p, int l, int r, int x, int vl) {
if (x < l || x > r) return p;
int q = ++tot;
e[q] = e[p];
if (l == r) {
sm(q) = sm(p) + vl;
return q;
}
int mid = (l + r) >> 1;
if (x <= mid) lc(q) = update(lc(p), l, mid, x, vl);
else rc(q) = update(rc(p), mid + 1, r, x, vl);
push_up(q);
return q;
}
int rnk(vector<int>p, vector<int>q, int l, int r, int x) {
if (l == r) {
int ans = 0;
for (auto i : p) ans += sm(i);
for (auto i : q) ans -= sm(i);
return ans;
}
int mid = (l + r) >> 1;
vector<int>tp, tq;
if (x <= mid) {
for (auto i : p) tp.push_back(lc(i));
for (auto i : q) tq.push_back(lc(i));
return rnk(tp, tq, l, mid, x);
}
else {
int ans = 0;
for (auto i : p) tp.push_back(rc(i)), ans += sm(lc(i));
for (auto i : q) tq.push_back(rc(i)), ans -= sm(lc(i));
return rnk(tp, tq, mid + 1, r, x) + ans;
}
}
int kth(vector<int>p, vector<int>q, int l, int r, int k) {
if (l == r) return l;
int mid = (l + r) >> 1, sum = 0;
vector<int>tp, tq;
for (auto i : p) sum += sm(lc(i));
for (auto i : q) sum -= sm(lc(i));
if (k <= sum) {
for (auto i : p) tp.push_back(lc(i));
for (auto i : q) tq.push_back(lc(i));
return kth(tp, tq, l, mid, k);
}
else {
for (auto i : p) tp.push_back(rc(i));
for (auto i : q) tq.push_back(rc(i));
return kth(tp, tq, mid + 1, r, k - sum);
}
}
} S;
struct BIT {
int lbt(int x) {
return x & (-x);
}
int tr[N];
void update(int x, int vl, int fg) {
for (int i = x; i <= n; i += lbt(i)) {
if (fg) rt[i] = S.update(rt[i], -LIM, LIM, a[x], -1);
rt[i] = S.update(rt[i], -LIM, LIM, vl, 1);
}
a[x] = vl;
}
int kth(int k, int l, int r) {
--l;
vector<int>p, q;
for (int i = r; i; i -= lbt(i)) p.push_back(rt[i]);
for (int i = l; i; i -= lbt(i)) q.push_back(rt[i]);
return S.kth(p, q, -LIM, LIM, k);
}
int rnk(int x, int l, int r) {
--l;
vector<int>p, q;
for (int i = r; i; i -= lbt(i)) p.push_back(rt[i]);
for (int i = l; i; i -= lbt(i)) q.push_back(rt[i]);
return S.rnk(p, q, -LIM, LIM, x - 1) + 1;
}
int pre(int x, int l, int r) {
int rk = rnk(x, l, r);
if (rk <= 1) return -2147483647;
return kth(rk - 1, l, r);
}
int nxt(int x, int l, int r) {
int rk = rnk(x + 1, l, r);
int sum = 0;
for (int i = r; i; i -= lbt(i)) sum += S.e[rt[i]].sm;
for (int i = l - 1; i; i -= lbt(i)) sum -= S.e[rt[i]].sm;
if (rk > sum) return 2147483647;
return kth(rk, l, r);
}
} B;
int main() {
ios::sync_with_stdio(0);
cin.tie(0);
cin >> n >> m;
for (int i = 1; i <= n; i++) {
int x;
cin >> x;
B.update(i, x, 0);
}
while (m--) {
int o;
cin >> o;
if (o == 1) {
int l, r, k;
cin >> l >> r >> k;
cout << B.rnk(k, l, r) << "\n";
}
else if (o == 2) {
int l, r, k;
cin >> l >> r >> k;
cout << B.kth(k, l, r) << '\n';
}
else if (o == 3) {
int x, k;
cin >> x >> k;
B.update(x, k, 1);
}
else if (o == 4) {
int l, r, k;
cin >> l >> r >> k;
cout << B.pre(k, l, r) << '\n';
}
else {
int l, r, k;
cin >> l >> r >> k;
cout << B.nxt(k, l, r) << '\n';
}
}
return 0;
}
应用
主席树可以应用于维护历史版本信息,以及解决一类二维偏序问题,应用场景较为广泛。具体地,其经典的应用有静态区间
可持久化平衡树
概述
我们一般使用 FHQ-Treap 来进行可持久化。使用它的原因是时间复杂度每次都是严格的
考虑可持久化的实际上是 Split 和 Merge 操作,考虑 Split 操作分裂出来的树和原树形态差别不大,复制走过的节点即可,容易知道每次只会有
需要留意的是合并操作在 Treap 中没有权值相同的节点时并不需要新建操作,但有重复的权值时必须新建节点。
给出可持久化平衡树模板题目的代码:
#include <bits/stdc++.h>
#define N 500005
#define inf 0x7f7f7f7f
using namespace std;
int n;
int rt[N], tot;
mt19937 wdz(time(0));
struct Node {
int lc, rc;
int sm, rd;
int vl;
} e[N * 80];
#define lc(i) e[i].lc
#define rc(i) e[i].rc
#define sm(i) e[i].sm
#define rd(i) e[i].rd
#define vl(i) e[i].vl
int psup(int p) {
sm(p) = sm(lc(p)) + sm(rc(p)) + 1;
return p;
}
int nwde(int x) {
++tot;
sm(tot) = 1;
rd(tot) = wdz();
vl(tot) = x;
return tot;
}
void split(int p, int x, int &l, int &r) {
if (!p) return l = r = 0, void();
if (vl(p) <= x) {
l = ++tot;
e[l] = e[p];
split(rc(p), x, rc(l), r);
psup(l);
}
else {
r = ++tot;
e[r] = e[p];
split(lc(p), x, l, lc(r));
psup(r);
}
}
int mge(int l, int r) {
if (!l || !r) return l | r;
int t = ++tot;
if (rd(l) >= rd(r)) {
e[t] = e[l];
rc(t) = mge(rc(t), r);
}
else {
e[t] = e[r];
lc(t) = mge(l, lc(t));
}
return psup(t);
}
void ist(int p, int q, int x) {
int l, r;
split(rt[p], x, l, r);
rt[q] = mge(mge(l, nwde(x)), r);
}
void del(int p, int q, int x) {
int l, r, w;
split(rt[p], x, l, r);
split(l, x - 1, l, w);
w = mge(lc(w), rc(w));
rt[q] = mge(mge(l, w), r);
}
int rnk(int p, int x) {
int l, r;
split(p, x - 1, l, r);
int res = sm(l) + 1;
p = mge(l, r);
return res;
}
int kth(int p, int k) {
if (sm(lc(p)) + 1 == k) return vl(p);
if (sm(lc(p)) + 1 >= k) return kth(lc(p), k);
return kth(rc(p), k - 1 - sm(lc(p)));
}
int pre(int p, int x) {
int l, r;
split(p, x - 1, l, r);
int res = kth(l, sm(l));
p = mge(l, r);
if (!l) return -inf;
return res;
}
int nxt(int p, int x) {
int l, r;
split(p, x, l, r);
int res = kth(r, 1);
p = mge(l, r);
if (!r) return inf;
return res;
}
int main() {
ios::sync_with_stdio(0);
cin.tie(0);
cin >> n;
for (int i = 1; i <= n; i++) {
int o, p, x;
cin >> p >> o >> x;
if (o == 1) ist(p, i, x);
else if (o == 2) del(p, i, x);
else if (o == 3) cout << rnk(rt[p], x) << '\n';
else if (o == 4) cout << kth(rt[p], x) << '\n';
else if (o == 5) cout << pre(rt[p], x) << '\n';
else cout << nxt(rt[p], x) << '\n';
if (o > 2) rt[i] = rt[p];
}
return 0;
}
可持久化文艺平衡树
问题的关键是此时的懒标记如何处理。如果直接下放标记会影响共用同一个节点的树的信息,那么每次复制一下再交换左右儿子即可。
给出处理这部分的关键代码:
int nw(int x) {
e[++tot] = {0, 0, 1, x, wdz(), x, 0};
return tot;
}
int copy(int p) {
int x = ++tot;
e[x] = e[p];
return x;
}
void psdn(int p) {
if (tg(p)) {
swap(lc(p), rc(p));
if (lc(p)) lc(p) = copy(lc(p)), tg(lc(p)) ^= 1;
if (rc(p)) rc(p) = copy(rc(p)), tg(rc(p)) ^= 1;
tg(p) = 0;
}
}
应用
客观来讲可持久化平衡树的运用场景不是十分广泛,当看到题目中需要运用平衡树且需要维护历史版本信息时使用就可以了。
可持久化字典树
概述
可持久化字典树和可持久化线段树的思想是类似的,每插入一个字符串都只改变
void insert(int w, int x) {
int p = rt[w - 1], q = rt[w] = ++tot;
for (int i = M - 1; ~i; --i) {
int ch = (x >> i) & 1;
tr[q][!ch] = tr[p][!ch];
tr[q][ch] = ++tot;
p = tr[p][ch], q = tr[q][ch];
siz[q] = siz[p] + 1;
}
}
应用
其实可持久化 01-Trie 就是用来解决区间最大异或和问题的。形式化地,给定一个序列
int query(int l, int r, int x) {
int p = rt[l - 1], q = rt[r], ans = 0;
for (int i = M - 1; ~i; --i) {
int ch = (x >> i) & 1;
if (siz[tr[q][!ch]] > siz[tr[p][!ch]]) {
p = tr[p][!ch];
q = tr[q][!ch];
ans |= 1 << i;
}
else {
p = tr[p][ch];
q = tr[q][ch];
}
}
return ans;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律