树上启发式合并学习笔记
参考文献:
- OI Wiki
1 树上启发式合并简介
1.1 理论
先吐槽一句,这个算法只是名字很高级。
本算法可以在较优的时间复杂度内求出不少可以离线的树上问题。
我们不如先带入情景:
洛谷 CF600E Lomsat gelral
题目大意:
给定节点数量为
n 的树,编号为i 的结点的颜色为c_i 。求出每一颗子树内哪种颜色的结点最多。(1 \le n \le 2 \times 10^5 )
首先假设,我们拥有无限的空间,你数组开多大都没事。那么我们不妨定义
如果我们定义
-
对于任意一个节点(设其为
x ),我们先用cnt 统计其某一个儿子节点的答案。 -
接下来你需要暴力重置
cnt ,然后再去统计另一个儿子节点的答案。 -
重复如上操作。当你统计到最后一个儿子节点的答案时,你可以不重置
cnt ,再统计一遍之前统计过的儿子节点,求出x 的答案。
不难发现的是,以上算法时间复杂度会很高,而高就高在你每次统计下一棵子树时就要重置一次
总结:
对于当前节点
-
先遍历所有轻儿子的子树,求出这些轻儿子结点的答案,但是统计出来的东西不保留(即
cnt )。 -
遍历其重儿子,求出其答案,且统计出来的答案保留。
-
最后直接访问
i 的轻儿子,求出i 结点的答案。
轻/重儿子的定义参见数链剖分
1.2 实现
参考代码(部分):
const int N = 1e5 + 5;
int n, a[N], siz[N], son[N], cnt[N], ans[N], mx, mxsum, hson;
// siz[] 为子数大小,son[] 为重儿子是哪个,cnt[] 表示当前的答案,ans[] 是总答案。
vector<int> v[N];
void dfs1(int x, int fa) { // 先维护 siz[], son[]... 之类的东西。
siz[x] = 1, son[x] = -1;
for (int i : v[x]) {
if (i == fa) continue;
dfs1(i, x);
siz[x] += siz[i];
if (son[x] == -1 || siz[i] > siz[son[x]]) {
son[x] = i;
}
}
}
void cnt_ans(int x, int fa, int val) { // 统计答案。
cnt[a[x]] += val;
if (cnt[a[x]] > mx) {
mx = cnt[a[x]], mxsum = a[x];
} else if (cnt[a[x]] == mx)
mxsum += a[x];
for (int i : v[x]) {
if (i == fa || i == hson) continue;
cnt_ans(i, x, val);
}
}
void dfs2(int x, int fa, bool keep) { // 树上启发式合并的关键部分。
for (int i : v[x]) { // 先遍历其轻儿子。
if (i == fa || i == son[x]) continue;
dfs2(i, x, 0);
}
if (son[x] != -1) { // 如果这个节点有重儿子结点(也就是非叶子),便利其重儿子。
dfs2(son[x], x, 1), hson = son[x];
}
cnt_ans(x, fa, 1);
hson = 0, ans[x] = mxsum;
if (!keep) { // 暴力删除 cnt。
cnt_ans(x, fa, -1);
mxsum = 0, mx = 0;
}
}
signed main() {
cin >> n;
for (int i = 1; i <= n; i++) cin >> a[i];
for (int i = 1, x, y; i < n; i++) {
cin >> x >> y;
v[x].pb(y), v[y].pb(x);
}
dfs1(1, 0);
dfs2(1, 0, 0);
for (int i = 1; i <= n; i++) cout << ans[i] << " ";
return cout << endl, 0;
}
2 题目
2.1 例题 1
洛谷 CF741D Arpa’s letter-marked tree and Mehrdad’s Dokhtar-kosh paths
题目大意:
给定结点数量为
n 的树,每条边上有一个字符(a-v共 22 种小写字母),求出每棵子树中最长的 Dokhtar-kosh 路径长度。Dokhtar-kosh 路径的定义是一条简单路径,且路径上所有结点的字符经过重排列后,可以变成回文字符串。
思路:
不难发现,若经过重排列后能成为回文字符串,出现次数为奇数次的字符数量
我们维护一个
最后,我们再维护两个数组
参考代码(部分)
const int N = 5e5 + 5, M = N * 2, K = 1 << 23;
int n, a[N], siz[N], son[N], cnt[N], ans[N], dfn[2][N], depth[N], dis[N], id[N], idx, f[K], hson;
// dfn[] 记录的是某个结点的子树中,dfs[] 序最大的和最小的,id[] 记录 dfs 序为 i 的节点是多少。depth 是深度,dis、f、ans 见思路部分。
vector<pii> v[M];
void dfs1(int x, int fa) {
siz[x] = 1, son[x] = -1, depth[x] = depth[fa] + 1;
dfn[0][x] = ++idx, id[idx] = x;
for (pii i : v[x]) {
if (i.fir == fa) continue;
dis[i.fir] = dis[x] ^ i.sec;
dfs1(i.fir, x);
siz[x] += siz[i.fir];
if (son[x] == -1 || siz[i.fir] > siz[son[x]]) {
son[x] = i.fir;
}
}
dfn[1][x] = idx;
}
void cnt_ans(int x, int fa) {
if (f[dis[x]]) {
ans[x] = max(ans[x], f[dis[x]] - depth[x]);
}
for (int i = 0; i <= 21; i++) {
if (f[dis[x] ^ (1 << i)]) {
ans[x] = max(ans[x], f[dis[x] ^ (1 << i)] - depth[x]);
}
}
f[dis[x]] = max(f[dis[x]], depth[x]);
for (pii i : v[x]) {
if (i.fir == fa || i.fir == hson) {
continue;
}
for (int j = dfn[0][i.fir]; j <= dfn[1][i.fir]; j++) { // 因为一棵子树内的 dfn 是连续的,可以直接访问。
if (f[dis[id[j]]]) {
ans[x] = max(ans[x], f[dis[id[j]]] + (depth[id[j]] - 2 * depth[x]));
}
for (int k = 0; k <= 21; k++) {
if (f[dis[id[j]] ^ (1 << k)]) {
ans[x] = max(ans[x], f[dis[id[j]] ^ (1 << k)] + (depth[id[j]] - 2 * depth[x]));
}
}
}
for (int j = dfn[0][i.fir]; j <= dfn[1][i.fir]; j++) {
f[dis[id[j]]] = max(f[dis[id[j]]], depth[id[j]]);
}
}
}
void dfs2(int x, int fa, bool keep) {
for (pii i : v[x]) {
if (i.fir == fa || i.fir == son[x]) continue;
dfs2(i.fir, x, 0);
ans[x] = max(ans[x], ans[i.fir]);
}
if (son[x] != -1) {
dfs2(son[x], x, 1), hson = son[x];
ans[x] = max(ans[x], ans[son[x]]);
}
cnt_ans(x, fa);
hson = 0;
if (!keep) {
for (int i = dfn[0][x]; i <= dfn[1][x]; i++) {
f[dis[id[i]]] = 0;
}
}
}
signed main() {
cin >> n;
for (int i = 1, x; i < n; i++) {
char c;
cin >> x >> c;
v[x].pb({i + 1, 1 << (c - 'a')}), v[i + 1].pb({x, 1 << (c - 'a')});
}
dfs1(1, 0);
dfs2(1, 0, 0);
for (int i = 1; i <= n; i++) cout << ans[i] << " ";
return cout << endl, 0;
}
2.2 练习
洛谷 CF375D Tree and Queries
模板题,不讲。
洛谷 CF1009F Dominant Indices
类似于例题 1 的简化版,其实还是模板。
The End
最近写博客可能会暂时到这,所以不要问我我为什么咕咕咕,问就是教练的题单一个接一个(
Thanks
暂无