学习背景
在学习这块内容前,最好要先了解树的轻重链划分(了解过树剖)
题目特征
树上启发式合并,通常是在题目给定了根节点
r
o
o
t
root
root(通常
r
o
o
t
=
1
root=1
root=1),在离线情况下,解决查询某个子树下符合题目条件的答案,并且子树的信息是没法全部存下来的,如查询
u
u
u及其子树中不同颜色个数
下面还有很多例题,没明白意思可以先看下都是什么题
树上启发式合并思想
暴力就不用说了,直接搜那个点的子树,单次查询复杂度有
O
(
n
)
O(n)
O(n),
M
M
M次查询就到了
O
(
N
M
)
O(NM)
O(NM)
而树上启发式合并能够做到
d
f
s
dfs
dfs一遍整棵树即可得到所有答案(离线的询问),复杂度在
O
(
N
l
o
g
N
)
O(NlogN)
O(NlogN)
树上启发式合并非常暴力,在一次
d
f
s
dfs
dfs的过程中,统计两遍轻儿子,统计一遍重儿子就能实现
对于父亲节点
u
u
u来说,他的信息通过合并子节点
v
v
v的信息来得到,那么这么说我们记录每个点所有的信息不就完了吗?可是这类题目显然是不会给你这个机会的,一般需要记录的信息非常多,每次都记录一下根本实现不了,所以这里我们需要有取舍的保留子节点的信息!,即我们每次保存当前点最后一次搜索的儿子信息,然后再重新搜索其他儿子的信息并且合并上去
对于父亲节点来说,显然最后一次搜索子节点的信息是可以保留的,因为回来我们就要搜父亲的信息了,而这个点的信息不就是父亲的信息吗,所以最后一次搜索子节点非常关键!
而这里我们最后一次搜的子节点就是重儿子,为什么选重儿子呢?因为重儿子是所有子节点里面点的数量最多的,所以包含的信息也最多,我们如果要重新搜索他的话花费的时间要比轻儿子要更多
所以这里的实现过程就是
对于一个点
u
u
u,先统计完他所有轻儿子的答案,并且在最后回溯到
u
u
u的时候要删去保存的信息(否则会影响我们搜索重儿子的答案统计)
然后我们再搜重儿子,查询答案并且保存信息(为什么保留信息上面解释了)
最后我们再重新搜一遍轻儿子,保存信息即可(不需要更新答案)
复杂度分析
①一个点的
u
p
d
upd
upd只会经过轻边
②树剖中轻重链划分的性质——从根结点到任意结点的路所经过的轻重链的个数必定都小于
l
o
g
n
logn
logn
换一个角度来想,就是一个点最多有
l
o
g
N
logN
logN个祖先节点会需要他的信息,那么
N
N
N个点就是
O
(
N
l
o
g
N
)
O(NlogN)
O(NlogN)
模板
void dfs(int u, int fa) {//轻重链划分
siz[u] = 1;
for (auto &v: g[u])
if (v != fa) {
dfs(v, u);
siz[u] += siz[v];
if (!son[u] || siz[v] > siz[son[u]])
son[u] = v;
}
}
void upd(int u, int fa, int k) {//将信息加入/撤回
//k = 1的时候是加上信息,k = -1的时候是撤回信息
for (auto &v: g[u])
if (v != fa && !vis[v]) upd(v, u, k);
}
void dsu(int u, int fa, int keep) {//u为当前点, fa为父节点, keep为是否保留信息
for (auto &v: g[u])//先搜我们的轻儿子,过程中不保留信息
if (v != fa && v != son[u]) dsu(v, u, 0);
if (son[u]) dsu(son[u], u, 1), vis[son[u]] = 1;//然后搜我们的重儿子, 过程中保留信息
upd(u, fa, 1);//然后再查一遍我们的轻儿子,把信息更新上去
//这里用来统计答案
if (son[u]) vis[son[u]] = 0;
if (!keep) upd(u, fa, -1);//如果信息不保留,那我们就撤回
}
练习题
1. CF600E - Lomsat gelral
题意
N
N
N个节点以点
1
1
1为根的树,每一个节点有一个颜色
c
o
l
o
r
i
color_i
colori,对于一个以
u
u
u为根的子树来说,他的子树中的节点有多种颜色,现在求出出现次数最多的颜色(可能有多个颜色出现次数相同),并将出现次数求和
输出以所有点的答案
分析
以点1为根,我们需要保存的信息为当前点子树中颜色出现次数,并且在统计答案的时候找到颜色最多的
这里我们可以用一个桶
c
n
t
[
i
]
cnt[i]
cnt[i]来记录颜色
i
i
i出现的次数,然后用
m
x
mx
mx来记录当前次数最多的颜色,
s
u
m
sum
sum记录次数最多的颜色的次数之和
这样当某种颜色出现次数
c
n
t
>
m
x
cnt>mx
cnt>mx的时候,我们将
m
x
mx
mx更新,并且
s
u
m
=
c
n
t
sum=cnt
sum=cnt (为什么不是加上答案呢,因为我们最大值已经更新了,所以实际上是
s
u
m
=
0
,
s
u
m
=
s
u
m
+
c
n
t
sum=0, sum = sum + cnt
sum=0,sum=sum+cnt)
当
c
n
t
=
m
x
cnt=mx
cnt=mx的时候,
m
x
mx
mx不必更新,
s
u
m
+
=
c
n
t
sum+=cnt
sum+=cnt
当
c
n
t
<
m
x
cnt<mx
cnt<mx的时候,啥都不用做
代码
部分代码下面, 全部代码这里
int cnt[MAX], vis[MAX], mx;//mx记录当前
ll ans[MAX], sum;
void upd(int u, int fa, int k) {
cnt[color[u]] += k;
if (k == 1 && cnt[color[u]] >= mx) {
if (cnt[color[u]] > mx) mx = cnt[color[u]], sum = 0;
sum += color[u];
}
for (auto &v: g[u])
if (v != fa && !vis[v]) upd(v, u, k);
}
void dsu(int u, int fa, int keep) {
for (auto &v: g[u])
if (v != fa && v != son[u]) dsu(v, u, 0);
if (son[u]) dsu(son[u], u, 1), vis[son[u]] = 1;
upd(u, fa, 1);
ans[u] = sum;
if (son[u]) vis[son[u]] = 0;
if (!keep) upd(u, fa, -1), mx = sum = 0;
}