树上启发式合并

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

1. 概念

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

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

2. 具体内容

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

例题:树上数颜色

思路

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

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

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

对于当前节点 u,暴力想法是直接遍历每一个儿子,但如果考虑分开来求,即先把轻儿子遍历了,统计子树内节点答案(保留对cnt的影响),再单独遍历一遍重儿子,统计子树内节点答案,再统计u的答案,那么每个点最多会遍历 logn+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  阅读(10)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
点击右上角即可分享
微信分享提示