\qquad 树链剖分,顾名思义,是应用在树上的一种数据结构。一般用于处理动态维护路径信息、子树信息的问题,例如路径权值修改,路径查询权值和(最值),子树查询权值和(最值)等。树链剖分是将树剖析成一条条链,再利用 dfn \text{dfn} dfn 序上套线段树实现动态维护区间信息。
\qquad 回顾标题:树链剖分(重链剖分),为什么要在小括号里加个重链剖分 呢?
树链剖分(树剖/链剖)有多种形式,如重链剖分,长链剖分和用于 Link/cut Tree 的剖分(有时被称作「实链剖分」),大多数情况下(没有特别说明时),「树链剖分」都指「重链剖分」。—— from oi-wiki
\qquad 话说至此,什么又是重链剖分呢?在了解这之前,我们先来了解下什么是重儿子。
\qquad “重”儿子,为什么说它“重”呢?在树上,什么信息可以恰当,形象的说“轻重”呢?当然是 size \text{size} size (子树大小)啦!我们形式化的定义一下重儿子:假设 son x \text{son}_x sonx 表示点 x \text{x} x 的重儿子, to x \text{to}_x tox 表示 x \text{x} x 的儿子们, sze x \text{sze}_x szex 表示以 x \text{x} x 为根的子树的大小,那么 sze son x = max u ∈ to x sze u \large \text{sze}_{\text{son}_x}=\max_{u\in \text{to}_x}\text{sze}_u szesonx=maxu∈toxszeu。用语言描述就是:重儿子的子树大小是所有子节点中最大的。我们把剩余的子节点定义为轻儿子,点 x \text{x} x 向 son x \text{son}_x sonx 连的边定义为重边,剩余的边定义为轻边;同时,我们把由若干条(可以是 0 0 0)重边首尾拼接而成的链叫做重链,那么整棵树就可以被剖分成若干条重链,而且每一个点一定存在于一条重链中。这意味着我们可以用重链将树完全剖分。
\qquad 在介绍树剖时,曾提到过树剖是利用 dfn \text{dfn} dfn 序加线段树来完成子树修改、查询和路径修改、查询。对于子树的操作,我们都知道子树内的点 dfn \text{dfn} dfn 序永远是连续的,也就是说对于子树的操作是可以轻松完成的。那么对于路径操作,怎么搞呢?
\qquad 既然上面提到重链可以将树完全剖分,那么我们不妨在 dfs \text{dfs} dfs 的时候优先递归重儿子,然后再递归轻儿子,这样我们不仅可以保证子树内 dfn \text{dfn} dfn 序连续,还可以保证一条重链上的点的 dfn \text{dfn} dfn 序连续。有了这个重要性质,我们便可以轻松做到对重链的修改。想到了这一点,那么路径修改也就很简单了:假设一条路径是从 x \text{x} x 到 y \text{y} y,我们将路径拆成从 lca \text{lca} lca 到 x \text{x} x,从 lca \text{lca} lca 到 y \text{y} y 两条路径,然后我们分别从 x \text{x} x, y \text{y} y 开始往 lca \text{lca} lca 跳重链,对于一条重链,我们先跳到这条链的一个端点,在 Θ ( log n ) \Theta(\log n) Θ(logn) 的时间内修改,然后直接跳到这条链的另一个端点,接着往上跳别的重链,重复上述操作即可。这就是重链剖分操作的整体过程。
\qquad 上述操作看起来非常暴力,我们接下来来分析一下这一过程的时间复杂度。首先,这一过程的时间复杂度跟我们跳过的重链的条数有着密切关系,跟长度并无太大关系。我们不难发现,两条重链是会被一条轻边断开的。因此,我们只需分析出这一过程中会跳过多少轻边即可。这就要回到我们为什么要定义重儿子了。
\qquad 重儿子的子树大小是所有儿子中最大的,但是我们并不能得知重儿子子树大小的范围。对于一条链,重儿子子树大小可能达到 Θ ( n ) \Theta(n) Θ(n) 级别,但是对于菊花图,这一大小可能只有 1 1 1。但是,我们是可以得到轻儿子的子树大小范围的。轻儿子的子树大小一定不超过 sze x 2 \Large \frac{\text{sze}_x}{2} 2szex。这一点我们通过反证法很好证明。接着分析下去,我们发现每经过一条轻边,子树大小就会至少除以 2 2 2,那是不是就意味着我们最多只会经过 Θ ( log n ) \Theta(\log n) Θ(logn) 条轻边呢?那么我们跳链的次数也是 Θ ( log n ) \Theta(\log n) Θ(logn) 级别的,配上线段树,总体时间复杂度 Θ ( n log 2 n ) \Theta(n\log^2 n) Θ(nlog2n),就能轻松解决掉子树操作、路径操作的问题了。
void dfs_pre(int x, int fa) {
sze[x] = 1, dep[x] = dep[fa] + 1, fat[x] = fa;//需要记录每个点的父亲,跳链时会用
for(int i = head[x]; i; i = edge[i].lst) {
int To = edge[i].to;
if(To == fa) continue;
dfs_pre(To, x);
sze[x] += sze[To];
if(sze[To] > sze[son[x]]) son[x] = To;//更新重儿子
}
}
\qquad 这一段应该是树剖中最重要的一个 dfs \text{dfs} dfs 了。
void dfs(int x, int fa, int chain) {
bel[x] = chain;//记录当前点所在的链的链头
dfn[x] = ++ num;//记录dfn序
v[num] = x;//记录dfn序为num的点是哪一个
if(son[x]) dfs(son[x], x, chain);//重儿子跟自己在同一条链中
for(int i = head[x]; i; i = edge[i].lst) {
int To = edge[i].to;
if(To == fa || To == son[x]) continue;
dfs(To, x, To);//轻儿子跟自己不在一条链中,每个轻儿子都新开一条链
}
}
int lca(int x, int y) {
while(bel[x] != bel[y]) {//只要链头不同,就让 链头 深度更大的点往上跳
if(dep[bel[x]] > dep[bel[y]]) x = fat[bel[x]];
else y = fat[bel[y]];
}
return dep[x] > dep[y] ? y : x;
}
void C(int x, int y, int z) {//C:change
for(; bel[x] != bel[y]; x = fat[bel[x]]) change(1, dfn[bel[x]], dfn[x], z);
change(1, dfn[y] + 1, dfn[x], z);
}
//main 中
int Lca = lca(u, v);
C(u, Lca, w), C(v, Lca, w);//将路径拆成两条,分别修改
int Q(int x, int y) {
int maxx = 0;
for(; bel[x] != bel[y]; x = fat[bel[x]]) maxx = max(maxx, query(1, dfn[bel[x]], dfn[x]));
maxx = max(maxx, query(1, dfn[y] + 1, dfn[x]));
return maxx;
}
// main 中
int Lca = lca(u, v);
printf("%d\n", max(Q(u, Lca), Q(v, Lca)));
\qquad [ZJOI2008] 树的统计 板子题
\qquad [HAOI2015] 树上操作 板子题
\qquad [NOI2015] 软件包管理器 板子题
\qquad 月下“毛景树” 板子题,但是边权转点权,有点小细节
\qquad QTREE - Query on a tree 也是边权转点权
\qquad [国家集训队] 旅游 树剖板子,主要考查线段树操作
\qquad [SDOI2011] 染色 处理颜色段时有一点点小细节