可持久化数据结构学习笔记

可持久化数据结构学习笔记

可持久化线段树

概述

又称主席树。当我们想要保存线段树上的一些历史版本信息时,直接给每一个版本都开一个线段树显然是吃不消的,考虑优化。以版本之间单点修改为例,容易发现插入一个点最多会有 logn 个节点受影响,于是我们可以对这 logn 个节点新开节点维护,其它节点沿用上一个版本的节点即可。查询的时候从两棵线段树的根查询,将答案相减即可,这事实上是前缀和思想的体现。这样每次插入的复杂度是 O(logn),总的复杂度是 O(nlogn),可以接受。

这里给出一种基本结构的实现方式:

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));
}

二逼平衡树

我们知道线段树+平衡树可以实现二逼平衡树,让我们用树状数组+主席树再次解决这个问题。我们用树状数组维护外层下标,修改与查询时对内层主席树的 logn 个节点一起查询即可。时空复杂度均为 O(nlog2n)

给出代码实现:

#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;
}

应用

主席树可以应用于维护历史版本信息,以及解决一类二维偏序问题,应用场景较为广泛。具体地,其经典的应用有静态区间 k 小值,区间数颜色等。

可持久化平衡树

概述

我们一般使用 FHQ-Treap 来进行可持久化。使用它的原因是时间复杂度每次都是严格的 O(logn),而没有因旋转导致的均摊复杂度使得难以持久化。

考虑可持久化的实际上是 Split 和 Merge 操作,考虑 Split 操作分裂出来的树和原树形态差别不大,复制走过的节点即可,容易知道每次只会有 logn 个节点被复制。Merge 操作是同理的,新建节点合并即可。

需要留意的是合并操作在 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;
	}
}

应用

客观来讲可持久化平衡树的运用场景不是十分广泛,当看到题目中需要运用平衡树且需要维护历史版本信息时使用就可以了。

可持久化字典树

概述

可持久化字典树和可持久化线段树的思想是类似的,每插入一个字符串都只改变 O(|S|) 个节点,因此其最主要的形式是可持久化 01-Trie。这里给出大体的一个模板实现:

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 就是用来解决区间最大异或和问题的。形式化地,给定一个序列 a 和一个数 x,求 max{aix}(lir)。我们仿照一般 01-Trie 解决这类问题的方式,在可持久化 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;
}
posted @   长安19路  阅读(5)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律
点击右上角即可分享
微信分享提示