树上启发式合并

不是很重要但挺有用的小知识点,可以通过这个思路来优化复杂度。

1. 概念

树上启发式合并(dsu on tree),他跟并查集的关系也只有个启发式合并了

并查集的按秩合并就是让更小的连通块并到更大的连痛块里,如果把连通块的大小看作树的高度,那么就是让深度更小的树并到深度更大的树里。显然这可以使得find更快速地找到父亲(手画一下即可证明),这种合并方法就成为启发式合并。

2. 具体内容

树上启发式合并常用于树上数颜色相关问题。

例题:树上数颜色

思路

如果用暴力的思想去考虑这道题,那么复杂度将会达到 \(O(n^2)\) 。如果可以离线,那么查询就不是问题了,所以是要优化每个点的遍历次数的。

先引入一个小小的知识点:

根节点到树上任意节点的轻边数不超过 \(\log n\) 条。我们设根到该节点有 \(x\) 条轻边该节点的子树大小为 \(y\),显然轻边连接的子节点的子树大小小于父亲的一半(若大于一半就不是轻边了),则 \(y<n/2^x\),显然 \(n>2^x\),所以 \(x<\log n\)

对于当前节点 u,暴力想法是直接遍历每一个儿子,但如果考虑分开来求,即先把轻儿子遍历了,统计子树内节点答案(保留对cnt的影响),再单独遍历一遍重儿子,统计子树内节点答案,再统计u的答案,那么每个点最多会遍历 \(\log n+1\) 次。但是这样想就会出现一个很显然的问题:在遍历重儿子时需要把轻儿子对cnt数组的影响删去,否则答案是有问题的。所以结合一下这个思路,在遍历时用这个顺序:

  1. 先遍历 u 的轻(非重)儿子,并计算答案,但 不保留遍历后它对 cnt 数组的影响
  2. 遍历它的重儿子,保留它对 cnt 数组的影响
  3. 再次遍历 u 的轻儿子的子树结点,加入这些结点的贡献,以得到 u 的答案。

可能会觉得删除操作很奇怪,但是以暴力的思路来想,统计每一个节点时都要把数组清空才可以统计,这个删除操作也是这样的道理。

代码

直接放 OI-wiki 上的了。

点击查看代码
#include <cstdio>
#include <vector>
using namespace std;
constexpr int N = 2e5 + 5;
int n;
// g[u]: 存储与 u 相邻的结点
vector<int> g[N];
// sz: 子树大小
// big: 重儿子
// col: 结点颜色
// L[u]: 结点 u 的 DFS 序
// R[u]: 结点 u 子树中结点的 DFS 序的最大值
// Node[i]: DFS 序为 i 的结点
// ans: 存答案
// cnt[i]: 颜色为 i 的结点个数
// totColor: 目前出现过的颜色个数
int sz[N], big[N], col[N], L[N], R[N], Node[N], totdfn;
int ans[N], cnt[N], totColor;
void add(int u) {
	if (cnt[col[u]] == 0) ++totColor;
	cnt[col[u]]++;
}
void del(int u) {
	cnt[col[u]]--;
	if (cnt[col[u]] == 0) --totColor;
}
int getAns() { return totColor; }
void dfs0(int u, int p) {
	L[u] = ++totdfn;
	Node[totdfn] = u;
	sz[u] = 1;
	for (int v : g[u])
		if (v != p) {
			dfs0(v, u);
			sz[u] += sz[v];
			if (!big[u] || sz[big[u]] < sz[v]) big[u] = v;
		}
	R[u] = totdfn;
}
void dfs1(int u, int p, bool keep) {
	// 计算轻儿子的答案
	for (int v : g[u])
		if (v != p && v != big[u]) {
			dfs1(v, u, false);
		}
	// 计算重儿子答案并保留计算过程中的数据(用于继承)
	if (big[u]) {
		dfs1(big[u], u, true);
	}
	for (int v : g[u])
		if (v != p && v != big[u]) {
			// 子树结点的 DFS 序构成一段连续区间,可以直接遍历
			for (int i = L[v]; i <= R[v]; i++) {
				add(Node[i]);
			}
		}
	add(u);
	ans[u] = getAns();
	if (!keep) {
		for (int i = L[u]; i <= R[u]; i++) {
			del(Node[i]);
		}
	}
}
int main() {
	scanf("%d", &n);
	for (int i = 1; i <= n; i++) scanf("%d", &col[i]);
	for (int i = 1; i < n; i++) {
		int u, v;
		scanf("%d%d", &u, &v);
		g[u].push_back(v);
		g[v].push_back(u);
	}
	dfs0(1, 0);
	dfs1(1, 0, false);
	for (int i = 1; i <= n; i++) printf("%d%c", ans[i], " \n"[i == n]);
	return 0;
}

3. 例题

  • CF375D. Tree and Queries

板子题。记录下每一个节点需要查询哪些颜色,在dfs处理时记下即可。

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=1e5+5;
int n, m, c[maxn], idx, rk[maxn], son[maxn], siz[maxn], st[maxn], ed[maxn];
int head[maxn], edgenum, cnt[maxn], col[maxn], tot, d[maxn], ans[maxn];
vector<pair<int,int> > q[maxn];
struct edge{
    int next;
    int to;
}edge[maxn<<1];
void add(int from,int to)
{
    edge[++edgenum].next=head[from];
    edge[edgenum].to=to;
    head[from]=edgenum;
}
void dfs(int u,int fa)
{
	st[u]=++idx;
	rk[idx]=u;
	siz[u]=1;
	for(int i=head[u];i;i=edge[i].next)
	{
		int v=edge[i].to;
		if(v==fa) continue;
		dfs(v, u);
		siz[u]+=siz[v];
		if(siz[son[u]]<siz[v]) son[u]=v;
	}
	ed[u]=idx;
}
void add(int u)
{
	cnt[c[u]]++; 
	d[cnt[c[u]]]++;
}
void del(int u)
{
	d[cnt[c[u]]]--;
	cnt[c[u]]--;
}
void dfs1(int u,int fa,int opt)
{
	for(int i=head[u];i;i=edge[i].next)
	{
		int v=edge[i].to;
		if(v==fa||v==son[u]) continue;
		dfs1(v, u, 0);
	}
	if(son[u]) dfs1(son[u], u, 1);
	for(int i=head[u];i;i=edge[i].next)
	{
		int v=edge[i].to;
		if(v==fa||v==son[u]) continue;
		for(int j=st[v];j<=ed[v];j++)
		{
			add(rk[j]);
		}
	}
	add(u);
	for(int i=0;i<q[u].size();i++)
		ans[q[u][i].first]=d[q[u][i].second];
	if(!opt)
	{
		for(int i=st[u];i<=ed[u];i++)
		{
			del(rk[i]);
		}
	}
}
signed main()
{
	cin>>n>>m;
	for(int i=1;i<=n;i++)
	{
		cin>>c[i];
	}
	for(int i=1;i<n;i++)
	{
		int u, v;
		cin>>u>>v;
		add(u, v), add(v, u);
	}
	for(int i=1;i<=m;i++)
	{
		int u, k;
		cin>>u>>k; 
		q[u].push_back(make_pair(i, k));
	}
	dfs(1, 0);
	dfs1(1, 0, 0);
	for(int i=1;i<=m;i++) cout<<ans[i]<<endl;
	return 0;
}

墙裂推荐此博客(图示超详细!)和OI-wiki的(简明扼要清晰)

posted @ 2025-02-26 19:36  zhouyiran2011  阅读(80)  评论(0)    收藏  举报