高级数据结构笔记

树套树

顾名思义,就是一个树套一个树。。。

广义的树套树是指嵌套多层的数据结构。常见的有:线段树套线段树(二维线段树),线段树套平衡树("二逼平衡树"),分块套平衡树,树状数组套线段树(带修主席树)等等。

在这里,由于 set,map 等 STL 内部实现是平衡树,因此将这些 STL 的嵌套也算作树套树。

树套树解决偏序问题

树套树最典型的应用就是解决各种各样的偏序问题。

P3810 【模板】三维偏序(陌上花开)

经典解法是 CDQ 分治。这里使用树状数组套权值线段树解决。

首先第一维是经典的排序,第二维可以使用树状数组维护起来。树状数组的每个节点维护一棵动态开点线段树,维护这个节点范围内所有节点的第三维信息。

时间复杂度 \(O(n \log ^ 2 n)\),空间 \(O(n \log n)\)。

放一下主体部分代码:

cpp 复制代码
struct node {
	int ls, rs, s;
}tr[M];
#define lc tr[u].ls
#define rc tr[u].rs
#define mid (l + r >> 1)
void add(int &u, int l, int r, int x) {
	if (!u) u = ++ cnt; tr[u].s ++ ; 
	if (l == r) return;
	if (x <= mid) add(lc, l, mid, x);
	else add(rc, mid + 1, r, x);
}
void ADD(int x, int y) {
	for (int i = x; i <= m; i += (i & -i)) add(rt[i], 1, m, y);
}
int sum(int &u, int l, int r, int x) {
	if (!u or l > x) return 0;
	if (r <= x) return tr[u].s;
	return sum(lc, l, mid, x) + sum(rc, mid + 1, r, x);
}
int SUM(int x, int y, int s = 0) {
	for (int i = x; i; i -= (i & -i)) s += sum(rt[i], 1, m, y); return s;
}

P3157 CQOI2011 动态逆序对

将删点操作倒过来,就是逆序加点的过程。

将加点的顺序(即时间轴)看做第一维,将下标看做第二维,将权值看做第三维。

这就是典型的三维偏序问题。直接树套树带走。

提交记录

树套树解决二维数点问题

这里的二维数点定义比较广泛,包括点个数的计数,以及满足某些性质的点集查询等。

通常,二维数点问题有以下几种做法:

  • 将第一维分块,块内套 set 等数据结构维护第二维。同时对于每个 \(x\) 坐标建立一个 set,用于维护散块信息。

  • 对第一维建线段树,线段树节点里面套 set / 平衡树。

  • 对第一维建树状数组,树状数组每个节点里套 set / 平衡树。

对于第一种方法,直接对于整块 / 散块里的平衡树 lower_bound 即可。

对于第二种方法,定位到线段树上的 \(O(\log n)\) 个区间之后和第一种一样。

对于第三种方法,需要根据情况具体分析。有时需要维护树状数组后缀 \(\max\) 或者前缀 \(\max\),有时需要维护点数等等。优势是常数小。

CF19D Points

很好的一道题。但是由于题目丧心病狂的卡常,我至今没有用树状数组套 set 卡过去。

题目显然是二维数点,属于求满足某种条件的点集类型题目。条件是某个点右上方的最左下的点。

第一种思路是分块套 set。对于横纵坐标离散化之后,对于每个横坐标维护一个 set,记录横坐标为该值的所有纵坐标。同时对每个块维护一个类型为 pair 的 set,维护横坐标在该块内的所有点。

每次插入和查询复杂度都是 \(O(\log n)\)。查询复杂度需要查询 \(O(\sqrt n)\) 个整块,每个整块需要 \(O(\log n)\) 的复杂度。需要查询 \(O(\sqrt n)\) 个单点,单点复杂度 \(O(\log n)\)。因此总复杂度 \(O(m \sqrt n \log n)\)。轻松的跑过去了。

第二种思路是树状数组套 set。对于纵坐标用树状数组维护,内层套 set 维护横坐标。插入的时候只需要在右端点 \(< y\) 的节点的 set 内插点就可以了。对于查询操作,只需要在左端点 \(\ge y\) 的节点 set 内 lower_bound 即可。复杂度 \(O(m \log ^ 2 n)\)。

不知道为什么,常数和复杂度都小的树状数组套 set 没有卡过去/kk

分块套 set 代码

树状数组套 set :

cpp 复制代码
set<PII> s[N]; vector<int> p;
int n, lim;
struct Q { int op, x, y; }q[N];
void add(int x, int y) {
	for (int i = y; i; i -= (i & -i)) s[i].insert(mp(x, y));
}
void del(int x, int y) {
	for (int i = y; i; i -= (i & -i)) s[i].erase(mp(x, y));
}
PII ask(int x, int y) {
	PII ans = mp(INF, INF);
	for (int i = y; i <= lim; i += (i & -i))
		ans = min(ans, *s[i].lower_bound(mp(x, y)));
	return ans;
}
int main() {
	read(n);
	rep(i, 1, n) {
		char ch[7]; int x, y;
		scanf("%s", ch); read(x, y);
		if (*ch == 'a') q[i] = {0, x, y};
		if (*ch == 'r') q[i] = {1, x, y};
		if (*ch == 'f') q[i] = {2, ++ x, ++ y};
		p.push_back(y);
	} sort(all(p)); p.resize(unique(all(p)) - p.begin()); 
	lim = p.size();
	auto find = [](int x) -> int {
		return lower_bound(all(p), x) - p.begin() + 1;
	};
	rep(i, 1, n) q[i].y = find(q[i].y);
	rep(i, 1, lim) s[i].insert(mp(INF, INF));
	rep(i, 1, n) {
		if (q[i].op == 0) add(q[i].x, q[i].y);
		if (q[i].op == 1) del(q[i].x, q[i].y);
		if (q[i].op == 2) {
			register PII ans = ask(q[i].x, q[i].y);
			if (ans.first == INF) puts("-1");
			else write(' ', ans.first, p[ans.second - 1]), pc('\n');
		}
	} return 0;
}

Intersection of Permutations

比较套路的一道题。若有 \(b_x = a_y\),那么记 \(t_x = y\)。

问题转化成了:

  • 查询操作:查询在 \(l_b \sim r_b\) 中,有多少 \(t_i \in l_a, r_a \bigcup \mathbb{Z}\)。

  • 修改操作:交换 \(t_x, t_y\)。

这是一个带修的二维数点,直接树状数组套权值线段树带走。

注意要写空间回收,要不然会 MLE。

cpp 复制代码
struct node { int ls, rs, s; }tr[M];
#define ls tr[u].ls
#define rs tr[u].rs
#define mid (l + r >> 1)
int New() { return !top ? ++ cnt : stk[top -- ]; }
void add(int &u, int l, int r, int x, int v) {
	if (l > x or r < x) return;
	if (!u) u = New(); tr[u].s += v; if (l == r) return;
	add(ls, l, mid, x, v), add(rs, mid + 1, r, x, v);
	if (!tr[u].s) stk[ ++ top] = u, u = 0;
}
void ADD(int x, int p, int v) {
	for (int i = x; i <= n; i += (i & -i)) add(rt[i], 1, n, p, v);
}
int ask(int u, int l, int r, int L, int R) {
	if (!u or r < L or l > R) return 0;
	if (l >= L and r <= R) return tr[u].s;
	return ask(ls, l, mid, L, R) + ask(rs, mid + 1, r, L, R);
}
int ASK(int la, int ra, int lb, int rb, int s = 0) { lb -- ;
	for (int i = rb; i; i -= (i & -i)) s += ask(rt[i], 1, n, la, ra);
	for (int i = lb; i; i -= (i & -i)) s -= ask(rt[i], 1, n, la, ra); return s;
}
int main() {
	scanf("%d%d", &n, &m);
	rep(i, 1, n) scanf("%d", &a[i]), bin[a[i]] = i;
	rep(i, 1, n) scanf("%d", &b[i]);
	rep(i, 1, n) t[i] = bin[b[i]], ADD(i, t[i], 1);
	while (m -- ) {
		int op, la, ra, lb, rb, x, y;
		scanf("%d", &op);
		if (op & 1) {
			scanf("%d%d%d%d", &la, &ra, &lb, &rb);
			printf("%d\n", ASK(la, ra, lb, rb));
		} else {
			scanf("%d%d", &x, &y);
			ADD(x, t[x], -1); ADD(y, t[y], -1);
			swap(t[x], t[y]); ADD(x, t[x], 1); ADD(y, t[y], 1);
		}
	} return 0;
}

树套树解决带修区间第 \(k\) 大问题

不带修的区间第 \(k\) 大,正经解法就是主席树。

那么如果带修了呢?

可以外层一个树状数组维护下标,内层一个权值线段树维护这个区间的权值。

这样相当于把一棵主席树拆成了许多个动态开点线段树。

修改时,每次修改树状数组的 \(\log\) 个节点,每修改一个节点需要一个老哥,所以就是 \(O(\log ^ 2 n)\)。

查询就是前缀和的容斥。前缀和要在树状数组上做,所以是两个 \(\log\)。

经典例题:

P2617 Dynamic Rankings

放一下主体部分代码:

cpp 复制代码
struct node {
	int ls, rs, s;
}tr[M]; int rt[N];
vector<int> L, R;
int n, m, cnt, a[N];
#define lc tr[u].ls
#define rc tr[u].rs
#define mid (l + r >> 1)
void add(int &u, int l, int r, int x, int v) {
	if (l > x or r < x) return;
	if (!u) u = ++ cnt; tr[u].s += v;
	if (l == r) return;
	add(lc, l, mid, x, v); add(rc, mid + 1, r, x, v);
}
void ADD(int x, int v, int c) {
	for (int i = x; i <= n; i += (i & -i))
		add(rt[i], 0, V, v, c);
}
int ask(int l, int r, int k) {
	if (l == r) return r; int s = 0;
	for (auto &u : R) s += tr[lc].s;
	for (auto &u : L) s -= tr[lc].s;
	if (k <= s) {
		for (auto &u : L) u = lc;
		for (auto &u : R) u = lc;
		return ask(l, mid, k);
	}
	for (auto &u : L) u = rc;
	for (auto &u : R) u = rc;
	return ask(mid + 1, r, k - s);
}
int ASK(int l, int r, int k) {
	L.clear(); R.clear(); l -- ;
	for (int i = r; i; i -= (i & -i)) R.pb(rt[i]);
	for (int i = l; i; i -= (i & -i)) L.pb(rt[i]);
	return ask(0, V, k);
}

Summary: 在大部分时候,需要维护的信息具有左 / 右端点固定或者可差分性时,使用树状数组套数据结构是一个不错的选择。

当然,有时分块套数据结构可以获得意想不到的小常数。

相关推荐
于眠牧北2 小时前
雷电模拟器9/14搭配charles抓取自带浏览器接口9/14
经验分享·笔记·git
Answer1st9 小时前
【嵌入式模块】UART通信模块
笔记
和侧10 小时前
离子注入后为什么必须配套退火工艺?
笔记·学习
学逆向的11 小时前
C 循环结构的执行效率分析
c语言·开发语言·数据结构·网络安全
TheBlackHoleLab14 小时前
【Vulnhub系列】DarkHole1: 渗透测试笔记,没有踩渗透的坑,却踩到了虚拟机平台的坑,人生处处是大坑
笔记·vulnhub·easy·darkhole1
ACP广源盛1392462567314 小时前
硬件选型笔记:GSV2231@ACP,DP1.4 MST 一进三出多屏扩展方案评估
笔记·硬件架构·视频·mst·国产芯片
jeo陽15 小时前
04-循环实战:素数、最大公约数、级数求和与枚举法
c语言·开发语言·数据结构
Awh-15 小时前
ARM 裸机开发 DAY11 学习笔记
arm开发·笔记·学习
志尊宝15 小时前
Vue3 零基础每日笔记(087):Vue3 高频面试题(下)
前端·javascript·vue.js·笔记·ecmascript·#vue.js
叶子丶苏15 小时前
VibeCoding实战三个AI小项目-VibeCoding入门笔记02
大数据·人工智能·笔记