Why Linearize a Tree?
Range data structures are powerful because an array interval is easy to describe. A tree path is not: it bends upward toward a lowest common ancestor and then downward through another branch. Heavy-Light Decomposition (HLD) is the adapter between those worlds.
HLD does not answer sums, maxima, or updates by itself. It assigns each vertex a position so that every root-to-leaf route changes chains only logarithmically many times. A segment tree, Fenwick tree, sparse table, or another one-dimensional structure then handles the resulting intervals.
The three-layer mental model
Heavy vs. Light Edges
Root the tree and compute $size[u]$, the number of vertices in $u$'s subtree. For every non-leaf $u$, choose one child with maximum subtree size as heavy[u]; ties may be broken arbitrarily.
- The edge from $u$ to that chosen child is heavy.
- Every other child edge is light.
- Maximal runs of heavy edges form vertex-disjoint chains.
The chosen heavy child does not need more than half of $u$'s subtree. The half-size guarantee applies to every light child: if a non-chosen child had more than half, the chosen maximum child would be at least as large and the two could not both fit beneath $u$.
The name is about the complement
The proof does not require the heavy child itself to contain more than half the subtree. It requires every light child to contain at most half. Choosing a largest child heavy guarantees exactly that.
Working Principle and Construction
parent[u], depth[u], and a traversal order.size[u] = 1 + Σ size[child] and remember a largest child as heavy[u].base[pos[u]], then build the segment tree or Fenwick tree over base.Chains Become Array Ranges
The decomposition is useful only because it controls the layout. Every heavy chain occupies one consecutive array interval. With the usual heavy-first preorder, every subtree is also consecutive.
head[u]
The topmost vertex of $u$'s current heavy chain. Equal heads mean two vertices occupy one continuous chain interval.
pos[u]
The index where $u$'s value lives in the base array. A chain slice becomes [pos[top], pos[bottom]].
size[u]
The number of vertices below $u$, including $u$. Its subtree interval is [pos[u], pos[u]+size[u]-1].
parent[head[u]]
The jump that leaves one chain. It crosses exactly one light edge and moves strictly toward the root.
Path Queries and Updates
Keep endpoints $u$ and $v$. While their chain heads differ, process the deeper head's entire suffix, then jump that endpoint above the head. When the heads match, process the final interval between the endpoints.
depth[head[u]] < depth[head[v]], swap $u$ and $v$ so $u$ belongs to the deeper chain.[pos[head[u]], pos[u]].u = parent[head[u]] and repeat until both endpoints share a head.[pos[u], pos[v]].Vertex Values, Edge Values, and Operation Order
Vertex-valued path
Store value[u] at pos[u]. Include both ends of every processed interval. The final same-chain range includes the LCA exactly once.
The implementations below use this model with maximum as the aggregate.
Edge-valued path
Store edge $(parent[u],u)$ at the deeper endpoint pos[u]. Chain-jump intervals still include their heads, but the final same-chain interval begins at pos[ancestor]+1 to exclude the LCA's incoming edge.
Subtree interval
Heavy-first preorder makes every subtree contiguous: [pos[u], pos[u]+size[u]-1]. One range query answers a subtree aggregate in $O(\log N)$.
Non-commutative operations
Maximum and sum ignore interval order. Matrix products, strings, and function composition do not; preserve the $u\to LCA$ and $LCA\to v$ directions and reverse chain aggregates where necessary.
The classic off-by-one
For edge values, querying [pos[lca], pos[deeper]] accidentally includes the edge entering the LCA, which is not on the requested path. Use [pos[lca]+1, pos[deeper]] for the final chain segment.
Worked Query: Path 6 to 9
Using the decomposition in the SVGs, process the endpoint whose chain head is deeper. The exact order of the first two equal-depth choices may vary, but the resulting set of intervals is the same.
| State | Deeper chain | Array interval | Jump |
|---|---|---|---|
u=6, v=9 | head 4 | [pos[4],pos[6]] = [5,6] | u=parent[4]=1 |
u=1, v=9 | head 9 | [pos[9],pos[9]] = [9,9] | v=parent[9]=2 |
u=1, v=2 | head 2 | [pos[2],pos[2]] = [7,7] | v=parent[2]=0 |
u=1, v=0 | same head 0 | [pos[0],pos[1]] = [0,1] | finish |
The queried vertices are ${6,4,9,2,0,1}$, precisely the simple path $6\to4\to1\to0\to2\to9$. Each of four segment-tree queries costs $O(\log N)$.
Why the Decomposition and Query Are Correct
Only logarithmically many light edges
If $(u,v)$ is light and $v$ is a child, then $size[v]\le (size[u]-1)/2 < size[u]/2$. After crossing $k$ light edges downward, the remaining subtree contains fewer than $N/2^k$ vertices. It cannot contain fewer than one, so $k<\log_2 N+1$.
A path $u\leftrightarrow v$ consists of two ancestor paths meeting at their LCA. Each half crosses $O(\log N)$ light edges; their sum is still $O(\log N)$. Therefore the path intersects only $O(\log N)$ heavy chains.
The query covers exactly the path
When the chain heads differ, the deeper head cannot be an ancestor of the other endpoint. The chain suffix from that head to its endpoint is therefore wholly part of the desired path. Removing it and jumping to the head's parent preserves the unprocessed remainder. Once the heads match, the unique remaining path is exactly the array interval between the two positions.
The shortest mental model: heavy edges keep long movement inside an array interval; light edges are the rare moments when a path query must start another interval.
HLD's guarantee comes from counting chain changes, not from making every chain long.
Complexity Analysis
| Operation | HLD work | With segment tree | Notes |
|---|---|---|---|
| Preprocess decomposition | $O(N)$ | $O(N)$ | Parent/depth/order, sizes, heavy child, heads, and positions |
| Build range structure | — | $O(N)$ | Over the linearized base array |
| Point update | One position | $O(\log N)$ | Update pos[u] |
| Subtree query | One interval | $O(\log N)$ | [pos[u], pos[u]+size[u]-1] |
| Path query | $O(\log N)$ intervals | $O(\log^2 N)$ | Each interval costs $O(\log N)$ |
| Path range update | $O(\log N)$ intervals | $O(\log^2 N)$ | Requires lazy propagation for range updates |
When can path queries become $O(\log N)$?
For some static or prefix-friendly aggregates, extra per-chain preprocessing can avoid a full segment-tree query for every chain. The standard general-purpose bound remains $O(\log^2 N)$, which is the safest expectation for dynamic point updates plus path queries.
Implementation
This vertex-valued implementation uses maximum as the aggregate. Both preprocessing passes and the segment tree are iterative, avoiding recursion limits on path-shaped trees. The constructor also checks that the input is one connected, acyclic tree.
Real-World Applications
Rooted network backbones
When the topology is a fixed routing tree but capacities or health scores change, HLD can update one link or vertex and query the minimum, maximum, or sum along a route.
Hierarchy analytics
On a static organizational or directory tree, HLD supports aggregates along reporting or containment paths while individual scores or permissions change.
Game and dependency trees
Skill trees and fixed dependency hierarchies can ask for the strongest requirement, accumulated cost, or enabled count along the path between two states.
Algorithmic query engines
HLD is a common foundation when a problem mixes static tree topology with online point updates, subtree aggregates, and arbitrary path queries.
Exercises
Trace the decomposition
- Recompute every subtree size in the first SVG. Verify the heavy choice at vertices 0, 1, 2, 3, 4, and 5.
- Swap the tie choice at vertex 2 from child 7 to child 9. Write the new chains and base array; explain why correctness and complexity do not change.
- List the chain intervals generated for paths $8\leftrightarrow7$ and $3\leftrightarrow6$.
A different valid decomposition can yield different intervals without changing the represented tree path.
Move from vertices to edges
- Store each edge's weight at its deeper endpoint and implement
queryPathEdges. - Test the empty edge path from a vertex to itself. Decide which identity value your aggregate should return.
- Demonstrate the wrong result produced when the final interval includes
pos[lca].
All chain-jump ranges keep their head; only the final same-chain range skips the ancestor vertex.
Extend the range operations
- Replace maximum with sum. Identify the identity element and every line that must change.
- Add lazy propagation for “add $x$ to every vertex on path $u\leftrightarrow v$.”
- Challenge: support string concatenation along $u\to v$ while preserving direction through the LCA.
Limitations
| Situation | Why HLD may be awkward | Consider instead |
|---|---|---|
| Only subtree queries | A normal Euler preorder already makes each subtree one interval. | Euler tour plus one Fenwick or segment tree |
| Only LCA queries | The range structure and chain loop are unnecessary overhead. | Binary lifting or Euler-tour RMQ |
| Edges are linked or cut | Parents, sizes, heavy choices, and positions become stale. | A dynamic-tree structure such as a link-cut tree |
| Non-commutative aggregate | Naively combining chain results loses path direction. | Store forward/reverse aggregates and join them carefully |
| Path-shaped tree with recursive DFS | The traversal can overflow the language call stack. | Iterative preprocessing, as implemented above |
| Very simple or small input | HLD's implementation and constant factors may dominate. | Direct climbing or straightforward per-query traversal |
Static topology, dynamic data
Standard HLD is designed for a fixed tree whose vertex or edge values change. Updating values is cheap; changing which vertices are connected invalidates the decomposition.