← 全部文章

树上启发式合并 学习笔记

本文目录(7 节)
  1. 引入
  2. dsu on tree
  3. 证明
  4. 【例题】CF600E Lomsat gelral
  5. 【练习】CF741D Arpa’s letter-marked tree and Mehrdad’s Dokhtar-kosh paths
  6. 【练习】CF570D Tree Requests
  7. 参考资料

树上启发式合并 (dsu on tree) 可以在较优秀的复杂度内解决某些树上离线问题。

引入

考虑这样一个问题:

  • 给一棵 nn 个点的有根树,求每棵子树的颜色种类数。

考虑暴力,当我们要求 xx 的子树的答案时,我们可以用一个 cc 数组记录每个颜色出现的次数,将 xx 子树遍历一遍后,cc 数组中不为 00 的位置的个数就是 xx 子树的颜色种类数,然后清空 cc 数组。

显然这样总复杂度是 O(n2)\mathcal O(n^2) 的,有没有办法优化一下呢?

dsu on tree

当我们求完 xx 子树的答案后,如果我们立刻要求 faxfa_x 子树的答案,那么我们就不必清空 cc 数组,同时遍历 faxfa_x 子树时也不需要遍历 xx 子树。

看起来这样似乎并没有优化多少?

那么,如果我们对于每个点 xx,在求完其重儿子 sonxson_x 的答案后,立刻按照上述方法求 xx 的答案,你可以惊奇的发现时间复杂度被优化到了 O(nlog⁡n)\mathcal O(n \log n)!

具体实现见代码:

const int N = 1e5 + 7;
int n, m, a[N], s[N], son[N], dfn[N], num, c[N], now, ans[N];
vi e[N];

void dfs(int x, int f) {
	s[x] = 1, dfn[x] = ++num;
	for (auto y : e[x]) if (y != f) dfs(y, x), s[x] += s[y], son[x] = s[y] > s[son[x]] ? y : son[x];
}

inline void add(int x) {
	now += !c[x]++;
}

void dfs(int x, int f, bool k) {
	for (auto y : e[x]) if (y != f && y != son[x]) dfs(y, x, 0);
	if (son[x]) dfs(son[x], x, 1);
	for (auto y : e[x]) if (y != f && y != son[x]) for (int i = 0; i < s[y]; i++) add(a[dfn[y]+i]);
	add(a[dfn[x]]), ans[x] = now;
	if (!k) for (int i = now = 0; i < s[x]; i++) c[a[dfn[x]+i]] = 0;
}

int main() {
	rd(n);
	for (int i = 1, x, y; i < n; i++) rd(x), rd(y), e[x].pb(y), e[y].pb(x);
	dfs(1, 0);
	for (int i = 1; i <= n; i++) rd(a[dfn[i]]);
	dfs(1, 0, 1), rd(m);
	for (int i = 1, x; i <= m; i++) rd(x), print(ans[x]);
	return 0;
}

证明

首先我们知道一个点到根节点上有 O(log⁡n)\mathcal O(\log n) 条重链和 O(log⁡n)\mathcal O(\log n) 条轻边。

注意到每条轻边会导致下面的子树被多遍历一次,因此每个点最多被遍历 O(log⁡n)\mathcal O(\log n) 次。

因此总时间复杂度为 O(nlog⁡n)\mathcal O(n \log n)。

【例题】CF600E Lomsat gelral

求子树众数和,就在上面代码的基础上改一下即可。

const int N = 1e5 + 7;
int n, a[N], b[N], s[N], son[N], dfn[N], num, c[N], mx;
ll now, ans[N];
vi e[N];

void dfs(int x, int f) {
	s[x] = 1, dfn[x] = ++num;
	for (auto y : e[x]) if (y != f) dfs(y, x), s[x] += s[y], son[x] = s[y] > s[son[x]] ? y : son[x];
}

inline void add(int x) {
	if (++c[x] > mx) mx = c[x], now = 0;
	if (c[x] == mx) now += x;
}

void dfs(int x, int f, bool k) {
	for (auto y : e[x]) if (y != f && y != son[x]) dfs(y, x, 0);
	if (son[x]) dfs(son[x], x, 1);
	for (auto y : e[x]) if (y != f && y != son[x]) for (int i = 0; i < s[y]; i++) add(a[dfn[y]+i]);
	add(a[dfn[x]]), ans[x] = now;
	if (!k) for (int i = mx = now = 0; i < s[x]; i++) c[a[dfn[x]+i]] = 0;
}

int main() {
	rd(n);
	for (int i = 1; i <= n; i++) rd(b[i]);
	for (int i = 1, x, y; i < n; i++) rd(x), rd(y), e[x].pb(y), e[y].pb(x);
	dfs(1, 0);
	for (int i = 1; i <= n; i++) a[dfn[i]] = b[i];
	dfs(1, 0, 1);
	for (int i = 1; i <= n; i++) print(ans[i], " \n"[i==n]);
	return 0;
}

【练习】CF741D Arpa’s letter-marked tree and Mehrdad’s Dokhtar-kosh paths

【练习】CF570D Tree Requests

参考资料

评论

    填写昵称即可留言。评论经站主审核后公开,请勿提交私人信息。

    正在连接评论服务…