树上启发式合并
不是很重要但挺有用的小知识点,可以通过这个思路来优化复杂度。
1. 概念
树上启发式合并(dsu on tree),他跟并查集的关系也只有个启发式合并了。
并查集的按秩合并就是让更小的连通块并到更大的连痛块里,如果把连通块的大小看作树的高度,那么就是让深度更小的树并到深度更大的树里。显然这可以使得find更快速地找到父亲(手画一下即可证明),这种合并方法就成为启发式合并。
2. 具体内容
树上启发式合并常用于树上数颜色相关问题。
例题:树上数颜色
思路
如果用暴力的思想去考虑这道题,那么复杂度将会达到
先引入一个小小的知识点:
根节点到树上任意节点的轻边数不超过
条。我们设根到该节点有 条轻边该节点的子树大小为 ,显然轻边连接的子节点的子树大小小于父亲的一半(若大于一半就不是轻边了),则 ,显然 ,所以 。
对于当前节点 u,暴力想法是直接遍历每一个儿子,但如果考虑分开来求,即先把轻儿子遍历了,统计子树内节点答案(保留对cnt的影响),再单独遍历一遍重儿子,统计子树内节点答案,再统计u的答案,那么每个点最多会遍历
- 先遍历 u 的轻(非重)儿子,并计算答案,但 不保留遍历后它对 cnt 数组的影响;
- 遍历它的重儿子,保留它对 cnt 数组的影响;
- 再次遍历 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; }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】