A Bit of History
Heavy-Light Decomposition (HLD) was introduced by Daniel Sleator and Robert Tarjan in 1983 in their landmark paper on dynamic trees and link-cut trees. Their goal was to maintain forest structures under link and cut operations while answering path queries efficiently. The heavy-light path partitioning idea was later recognized as a standalone data structuring technique for static trees: by linearizing heavy paths in DFS order, a standard 1D Segment Tree or Fenwick Tree can execute path queries (e.g., path max, path sum, path update) on an arbitrary tree in \(O(\log^2 N)\) time.
Heavy vs. Light Edges
In a rooted tree with $N$ vertices, let $\text{subtree\_size}(u)$ be the number of nodes in the subtree rooted at $u$:
- Heavy Edge: An edge $(u, v)$ from parent $u$ to child $v$ is heavy if $\text{subtree\_size}(v) > \frac{1}{2} \cdot \text{subtree\_size}(u)$. Each node $u$ can have at most one heavy child.
- Light Edge: All other edges connecting $u$ to its children are light.
Connected heavy edges form contiguous heavy paths (chains). Light edges serve as bridges jumping from one heavy path to the top of another.
Working Principle & Construction
Constructing HLD requires two DFS passes:
- First DFS (Subtree Sizes & Heavy Children): Compute node depths, parent pointers, subtree sizes, and identify the heavy child for each node (the child with the maximum subtree size).
- Second DFS (Path Linearization & Head Assignment): Traverse the tree prioritizing heavy children first. Assign a 1D position (
pos[u]) to each node. Because heavy children are visited consecutively, nodes on the same heavy path receive contiguous 1D indices! Also assignhead[u], the top node of the heavy path containing $u$.
Key Insight
Because nodes along a heavy path have contiguous 1D position indices in the second DFS traversal, an entire segment of a heavy path corresponds to a contiguous subsegment [pos[top], pos[u]] in a 1D Segment Tree!
Path Queries & Updates
To query or update the path between two nodes $u$ and $v$:
- While $u$ and $v$ are on different heavy paths (i.e.,
head[u] != head[v]):- Pick the node whose head is deeper (say $u$).
- Query/update the contiguous 1D range
[pos[head[u]], pos[u]]in the Segment Tree. - Jump $u$ up to the parent of its chain's head:
u = parent[head[u]](crossing a light edge).
- Once both nodes share the same heavy path (
head[u] == head[v]), query/update the remaining range between them:[min(pos[u], pos[v]), max(pos[u], pos[v])].
Worked Example
Consider a tree rooted at 0 with 10 nodes. Suppose path from 0 to 8 consists of 2 heavy paths connected by 1 light edge:
- Querying path $0 \to 8$: Node 8 is on a heavy path starting at head 5. We query range
[pos[5], pos[8]]in $O(\log N)$ via Segment Tree. - Then jump node 8 up across the light edge:
8 -> parent[5] = 2. - Node 2 and Node 0 share the same heavy path (head 0). Query remaining range
[pos[0], pos[2]]in $O(\log N)$. - Total path query time: $2 \times O(\log N) = O(\log^2 N)$.
Why Any Path Has O(log N) Light Edges
Crucially, traversing a light edge $(u, v)$ means moving from parent $u$ to child $v$ where $\text{subtree\_size}(v) \le \frac{1}{2} \cdot \text{subtree\_size}(u)$. Therefore, moving down a light edge cuts the subtree size at least in half!
Starting from the root with $N$ nodes, you can traverse at most $\lfloor \log_2 N \rfloor$ light edges before reaching a leaf. Consequently, **any simple path in the tree crosses at most $O(\log N)$ light edges**, jumping between at most $O(\log N)$ distinct heavy paths.
Complexity Analysis
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| Decomposition Construction (2 DFS passes) | \(O(N)\) | \(O(N)\) |
| Segment Tree Construction | \(O(N)\) | \(O(N)\) |
| Path Query / Path Update | \(O(\log^2 N)\) | \(O(1)\) auxiliary |
| Subtree Query / Subtree Update | \(O(\log N)\) | \(O(1)\) auxiliary |
Implementation
Real-World Applications
Network Backbones & Dynamic Tree Querying
In telecommunication backbone networks and distributed routing trees, capacity or bottleneck metrics along network paths need real-time updates and queries. HLD allows network monitoring systems to update link capacities and query path bottlenecks in $O(\log^2 N)$ time.
Exercises
- Show how HLD can be modified to support Subtree Queries (e.g., sum of all nodes in $u$'s subtree) in $O(\log N)$ time.
- Extend the HLD implementation to support edge weights instead of vertex weights.
- Compare HLD with Centroid Decomposition for path queries and range updates.
- Challenge: Implement HLD with Lazy Propagation on the Segment Tree to support path range updates in $O(\log^2 N)$ time.
Limitations
Static Tree Constraint & Constant Factor
Standard HLD works only on static trees (fixed structure). If edges are dynamically inserted/deleted, Link-Cut Trees (Sleator & Tarjan) must be used instead. Additionally, HLD carries a notable constant factor due to 2 DFS passes and multiple Segment Tree range queries per path.