【学习笔记】树上启发式合并
【学习笔记】树上启发式合并
简介
树上启发式合并 dsu on tree,是一种 离线 算法,通过钦定一个更好的询问顺序,来优化时间复杂度,其实本质上还是暴力,有点类似莫队。有关 dsu on tree 的问题,常常是询问 子树内 的颜色数啥玩意的。
实现
对于每一个节点 \(u\),按以下顺序遍历:
- 遍历 \(u\) 的 轻儿子 \(v\),计算它的答案,但并不保留它对答案数组的影响
- 遍历 \(u\) 的 重儿子 \(v\),计算它的答案,保留它对答案数组的影响(注意 判断一个点是否有儿子,不然可能死循环)
- 将所有 轻儿子 的贡献计入答案数组
注意,第一,三步不能合并,因为需要独立计算每棵字数内的答案,但是又不能对每个结点都开一个数组(空间上会炸)
时间复杂度证明:
引理:一个叶子节点到根的路径上,最多有 \(log_n\) 条 轻边。
证明:每经过一条轻边,都会将树的的大小除以 \(2\)。
对于一条重边,它的父亲节点不会遍历儿子节点(会直接继承),所以每个节点只会被遍历轻边个数 \(+1\)(还有它自己),因此最多时间复杂度 \(O(n\log n)\)。
例题 颜色平衡树
题解
设 \(cnt[x]\) 表示颜色 \(x\) 的出现次数,\(ccnt[x]\),表示出现次数为 \(x\) 的颜色有多少个,则判断 \(u\) 的子树是不是颜色平衡树,只需要判断 \(ccnt[cnt[c[u]]] \times cnt[c[u]]\) 是否等于 \(size_u\),然后 dsu on tree。
code
#include<bits/stdc++.h>
#define pb push_back
#define fo(a, b, c) for(int b = a; b <= c; b++)
#define Fo(a, b) for(auto a : b)
#define N 200010
using namespace std;
int n, c[N], f, son[N], sz[N], ans;
int cnt[N], ccnt[N];
vector<int>G[N];
void dfs(int u){
sz[u] = 1;
Fo(v, G[u]){
dfs(v, u);
sz[u] += sz[v];
if(sz[v] > sz[son[u]]) son[u] = v;
}
}
void mdf(int u, int x){
--ccnt[cnt[c[u]]], cnt[c[u]] += x, ++ccnt[cnt[c[u]]];
}
void add(int u, int x){
mdf(u, x);
Fo(v, G[u]) add(v, x);
}
void solve(int u, bool keep){
Fo(v, G[u]) if(v ^ son[u]) solve(v, 0);
if(son[u]) solve(son[u], 1);
mdf(u, 1);
Fo(v, G[u]) if(v ^ son[u]) add(v, 1);
if(cnt[c[u]] * ccnt[cnt[c[u]]] == sz[u]) ++ans;
if(!keep) add(u, - 1);
}
int main(){
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
cin >> n;
fo(1, i, n){
cin >> c[i] >> f;
if(f) G[f].pb(i);
}
dfs(1);
solve(1, 1);
cout << ans;
return 0;
}

浙公网安备 33010602011771号