30分钟掌握树链剖分:从路径查询到换根操作的完全指南

30分钟掌握树链剖分:从路径查询到换根操作的完全指南

【免费下载链接】OI-wiki :star2: Wiki of OI / ICPC for everyone. (某大型游戏线上攻略,内含炫酷算术魔法) 【免费下载链接】OI-wiki 项目地址: https://gitcode.com/GitHub_Trending/oi/OI-wiki

你是否还在为树上路径查询问题烦恼?面对"修改两点间路径权值"或"查询子树和"等需求时无从下手?本文将带你系统掌握树链剖分(Heavy-Light Decomposition, HLD)技术,用最直观的方式理解这一处理树上路径问题的强大工具。读完本文后,你将能够独立实现树链剖分算法,解决包括路径修改、子树查询和换根操作在内的复杂树上问题。

什么是树链剖分?

树链剖分是一种将树结构分解为若干条链的预处理技术,通过重链剖分(最常用的树链剖分形式)可以将任意树上路径分解为不超过O(log n)条连续的链,每条链上的DFS序是连续的,从而可以使用线段树等数据结构高效维护路径信息。

树链剖分示意图

如上图所示,树链剖分通过以下定义实现对树的分解:

  • 重子结点:子树规模最大的子结点
  • 轻子结点:除重子结点外的其他子结点
  • 重边:连接结点与其重子结点的边
  • 轻边:连接结点与其轻子结点的边
  • 重链:由重边连接而成的连续路径

树链剖分的核心价值在于:树上任意路径都可分解为O(log n)条重链,且每条重链的DFS序连续,这为使用线段树等区间数据结构处理树上路径问题提供了可能。

实现树链剖分的两个关键DFS

树链剖分的实现需要两次深度优先搜索(DFS),完整代码实现可参考树链剖分核心代码

第一次DFS:计算基础信息

void dfs1(int u, int f) {
  fa[u] = f, dep[u] = dep[f] + 1, siz[u] = 1;
  for (auto v : G[u]) {
    if (v == f) continue;
    dfs1(v, u);
    siz[u] += siz[v];
    if (siz[v] > siz[son[u]]) son[u] = v;
  }
}

这次DFS主要计算五个数组:

  • fa[u]:结点u的父结点
  • dep[u]:结点u的深度
  • siz[u]:以u为根的子树大小
  • son[u]:u的重子结点(子树最大的子结点)

第二次DFS:划分重链并分配DFS序

void dfs2(int u, int ftop) {
  top[u] = ftop, dfn[u] = ++idx, rnk[idx] = u;
  if (son[u]) dfs2(son[u], ftop);
  for (auto v : G[u])
    if (v != son[u] && v != fa[u]) dfs2(v, v);
}

这次DFS完成三个关键任务:

  • top[u]:记录u所在重链的顶端结点
  • dfn[u]:分配DFS序(保证重链内连续)
  • rnk[idx]:DFS序到结点的映射

通过这两次DFS,我们就完成了对树的剖分,为后续处理路径问题奠定了基础。

树链剖分的核心应用

路径查询与修改

树链剖分最经典的应用是处理树上两点间路径的查询和修改操作。其核心思想是将路径分解为多条重链,对每条重链使用线段树进行操作。

路径查询的伪代码如下:

function TREE-PATH-SUM(u, v):
    tot = 0
    while top[u] != top[v]:
        if depth[top[u]] < depth[top[v]]:
            swap(u, v)
        tot += sum(dfn[top[u]] to dfn[u])
        u = fa[top[u]]
    tot += sum(min(dfn[u], dfn[v]) to max(dfn[u], dfn[v]))
    return tot

这个过程中,我们不断将深度较大的链顶端向上跳,直到两个结点位于同一条重链上。每次跳跃处理一段连续的DFS区间,时间复杂度为O(log²n)。

子树查询与修改

由于子树的DFS序是连续的,我们可以通过记录每个结点子树的DFS区间来处理子树操作。具体实现时,只需查询或修改[dfn[u], dfn[u] + siz[u] - 1]区间即可。

最近公共祖先(LCA)查询

树链剖分还可以高效求解LCA问题:

int lca(int u, int v) {
  while (top[u] != top[v]) {
    if (dep[top[u]] > dep[top[v]])
      u = fa[top[u]];
    else
      v = fa[top[v]];
  }
  return dep[u] > dep[v] ? v : u;
}

通过不断将较深的链顶端上移,最终两个结点会汇合到同一条重链上,深度较小的结点即为LCA。

高级应用:处理换根操作

当树需要支持换根操作时,子树查询变得复杂。我们需要根据新根与原根的相对位置关系分类讨论:

  1. 新根就是当前结点:操作对象为整棵树
  2. 当前结点是新根的祖先:需要排除新根所在的子树
  3. 其他情况:正常处理子树区间[dfn[u], dfn[u] + siz[u] - 1]

详细实现可参考带换根操作的树链剖分例题,其中关键是找到新根在原树上的祖先的特定子结点,将子树操作转换为两个区间操作。

长链剖分:另一种剖分方式

除重链剖分外,长链剖分是另一种重要的树剖分技术,它以子树深度而非大小来定义重子结点。长链剖分在优化某些动态规划问题上有奇效,如求解k级祖先问题可达到O(1)查询复杂度,或优化深度相关的DP转移。

长链剖分示意图

长链剖分的典型应用是优化树上深度相关的动态规划,如Codeforces 1009 F. Dominant Indices问题,通过继承重儿子的DP数组实现O(n)时间复杂度的求解。

实战例题与代码实现

ZJOI2008树的统计

这是一道树链剖分的经典模板题,要求支持单点修改、路径最大值查询和路径和查询。完整代码实现见树链剖分模板代码

LOJ 139. 树链剖分

这道题在基础树链剖分上增加了换根操作,需要处理复杂的子树查询问题。关键是根据新根与原根的位置关系分类讨论,完整代码见带换根操作的树链剖分代码

练习题推荐

通过这些练习,你将逐步掌握树链剖分的各种应用场景,从基础路径查询到复杂的换根操作和动态规划优化。

总结

树链剖分作为处理树上路径问题的强大工具,通过将树分解为若干重链,成功将复杂的树形结构转化为线性结构,使得线段树等区间数据结构能够应用于树上问题。其核心优势在于:

  1. 路径分解:任意路径可分解为O(log n)条重链
  2. 高效查询:每次操作复杂度为O(log²n)
  3. 灵活扩展:支持路径修改、子树查询、换根等复杂操作

掌握树链剖分技术,将为你解决各类树上问题提供有力武器。建议结合本文提供的代码实现和练习题,亲手实现这一算法,深入理解其原理和应用场景。

希望本文能帮助你彻底掌握树链剖分技术。如有任何疑问,欢迎查阅OI Wiki树链剖分完整文档或参与社区讨论。最后,别忘了点赞收藏本文,以便日后复习!

【免费下载链接】OI-wiki :star2: Wiki of OI / ICPC for everyone. (某大型游戏线上攻略,内含炫酷算术魔法) 【免费下载链接】OI-wiki 项目地址: https://gitcode.com/GitHub_Trending/oi/OI-wiki

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值