在算法竞赛和工程实践中,我们常常需要对一棵树进行频繁的路径查询或修改操作,比如求两点间路径上的最大值、最小值、和值,或者将某条路径上的所有点权加上一个值。如果每次操作都暴力遍历整条路径,时间复杂度会高达 O(n),在大数据量下效率极低。
这时,C++树链剖分(Heavy-Light Decomposition, HLD)就派上了用场!它是一种将树结构“线性化”的高级技巧,配合线段树或树状数组,可将单次路径操作的时间复杂度优化到 O(log²n)。
树链剖分的核心思想是:将一棵树拆分成若干条“重链”(heavy chains),使得任意两点间的路径最多被拆成 O(log n) 条连续的链段。这样,原本在树上的复杂路径操作,就可以转化为在线性序列上的区间操作。

通过这种划分,任意两点 u 和 v 之间的路径可以被拆分为若干段,每一段都是某条重链的连续子区间。这正是我们能使用线段树处理的关键!
树链剖分通常需要两次 DFS:
void dfs1(int u, int father) { fa[u] = father; dep[u] = dep[father] + 1; size[u] = 1; son[u] = -1; // 初始化无重儿子 for (int v : adj[u]) { if (v == father) continue; dfs1(v, u); size[u] += size[v]; if (son[u] == -1 || size[v] > size[son[u]]) son[u] = v; // 更新重儿子 }}
int cnt = 0; // 全局 dfn 计数器void dfs2(int u, int t) { top[u] = t; dfn[u] = ++cnt; a[cnt] = val[u]; // val[u] 是原树中 u 的点权 if (son[u] != -1) dfs2(son[u], t); // 优先遍历重儿子,保持链连续 for (int v : adj[u]) { if (v == fa[u] || v == son[u]) continue; dfs2(v, v); // 轻儿子新开一条链 }}
核心思想:不断将 u 和 v 所在链中较深的那个跳到其链顶的父节点,同时处理当前链段。
void update_path(int u, int v, int delta) { while (top[u] != top[v]) { if (dep[top[u]] < dep[top[v]]) swap(u, v); seg_tree.update(dfn[top[u]], dfn[u], delta); // 线段树区间更新 u = fa[top[u]]; } if (dep[u] > dep[v]) swap(u, v); seg_tree.update(dfn[u], dfn[v], delta);}long long query_path(int u, int v) { long long res = 0; while (top[u] != top[v]) { if (dep[top[u]] < dep[top[v]]) swap(u, v); res += seg_tree.query(dfn[top[u]], dfn[u]); u = fa[top[u]]; } if (dep[u] > dep[v]) swap(u, v); res += seg_tree.query(dfn[u], dfn[v]); return res;}
int main() { // 输入建图... dfs1(1, 0); // 以1为根 dfs2(1, 1); seg_tree.build(1, 1, n); // 用 a[1..n] 构建线段树 // 处理查询/更新... return 0;}
树链剖分是C++图论算法中处理树上路径问题的利器。虽然代码较长,但只要理解了“重链”和“DFS序”的思想,就能轻松掌握。它常用于解决以下问题:
通过本教程,相信你已经对树链剖分算法详解有了清晰的认识。记住:树链剖分的本质是“树转序列”,再用数据结构维护序列。多练习几道题(如洛谷 P3384),你就能熟练运用这一强大工具!
关键词回顾:C++树链剖分、树链剖分算法详解、树上路径查询、C++图论算法。
本文由主机测评网于2025-12-19发表在主机测评网_免费VPS_免费云服务器_免费独立服务器,如有疑问,请联系我们。
本文链接:https://www.vpshk.cn/20251210117.html