Heavy-Light Decomposition
Heavy-Light Decomposition for efficient path queries on trees.
Heavy-Light Decomposition (HLD)
Heavy-Light Decomposition is a technique for dividing a tree into chains to efficiently answer path queries and updates, typically in or time.
Key Concepts
- Heavy edge: An edge to the child with the largest subtree.
- Light edge: All other edges from a node.
- Any path from a node to the root crosses at most light edges.
- When moving up the tree and crossing a light edge, the size of the subtree at least halves.
Implementation
class HLD {
int n, timer;
vector<vector<int>> adj;
vector<int> parent, depth, heavy, head, pos, subtreeSize;
int dfs(int u) {
subtreeSize[u] = 1;
int maxChildSize = 0;
for (int v : adj[u]) {
if (v != parent[u]) {
parent[v] = u;
depth[v] = depth[u] + 1;
int childSize = dfs(v);
subtreeSize[u] += childSize;
if (childSize > maxChildSize) {
maxChildSize = childSize;
heavy[u] = v;
}
}
}
return subtreeSize[u];
}
void decompose(int u, int h) {
head[u] = h;
pos[u] = timer++;
if (heavy[u] != -1) {
decompose(heavy[u], h);
}
for (int v : adj[u]) {
if (v != parent[u] && v != heavy[u]) {
decompose(v, v);
}
}
}
public:
HLD(int n) : n(n), timer(0), adj(n), parent(n, -1), depth(n, 0),
heavy(n, -1), head(n), pos(n), subtreeSize(n) {}
void addEdge(int u, int v) {
adj[u].push_back(v);
adj[v].push_back(u);
}
void build(int root = 0) {
dfs(root);
decompose(root, root);
}
// Returns pairs of [l, r] intervals on the pos array
vector<pair<int, int>> getPath(int u, int v) {
vector<pair<int, int>> result;
while (head[u] != head[v]) {
if (depth[head[u]] < depth[head[v]]) {
swap(u, v);
}
result.push_back({pos[head[u]], pos[u]});
u = parent[head[u]];
}
if (pos[u] > pos[v]) swap(u, v);
result.push_back({pos[u], pos[v]});
return result;
}
int lca(int u, int v) {
while (head[u] != head[v]) {
if (depth[head[u]] < depth[head[v]]) {
swap(u, v);
}
u = parent[head[u]];
}
return depth[u] < depth[v] ? u : v;
}
int getPos(int u) { return pos[u]; }
int getSubtreeSize(int u) { return subtreeSize[u]; }
};
Using with Segment Tree
Path Sum Query
class HLDWithSegTree {
HLD hld;
vector<long long> value;
vector<long long> tree; // Segment tree
int n;
void buildTree(int node, int l, int r) {
if (l == r) {
tree[node] = value[l];
return;
}
int mid = (l + r) / 2;
buildTree(2*node, l, mid);
buildTree(2*node+1, mid+1, r);
tree[node] = tree[2*node] + tree[2*node+1];
}
void update(int node, int l, int r, int idx, long long val) {
if (l == r) {
tree[node] = val;
return;
}
int mid = (l + r) / 2;
if (idx <= mid) update(2*node, l, mid, idx, val);
else update(2*node+1, mid+1, r, idx, val);
tree[node] = tree[2*node] + tree[2*node+1];
}
long long query(int node, int l, int r, int ql, int qr) {
if (qr < l || r < ql) return 0;
if (ql <= l && r <= qr) return tree[node];
int mid = (l + r) / 2;
return query(2*node, l, mid, ql, qr) +
query(2*node+1, mid+1, r, ql, qr);
}
public:
HLDWithSegTree(int n) : hld(n), n(n), value(n), tree(4*n) {}
void addEdge(int u, int v) { hld.addEdge(u, v); }
void setValue(int u, long long val) { value[u] = val; }
void build(int root = 0) {
hld.build(root);
vector<long long> reordered(n);
for (int i = 0; i < n; i++) {
reordered[hld.getPos(i)] = value[i];
}
value = reordered;
buildTree(1, 0, n-1);
}
void updateVertex(int u, long long val) {
update(1, 0, n-1, hld.getPos(u), val);
}
long long queryPath(int u, int v) {
long long result = 0;
auto intervals = hld.getPath(u, v);
for (auto& [l, r] : intervals) {
result += query(1, 0, n-1, l, r);
}
return result;
}
// Query on subtree
long long querySubtree(int u) {
int l = hld.getPos(u);
int r = l + hld.getSubtreeSize(u) - 1;
return query(1, 0, n-1, l, r);
}
};
Path Maximum Query
long long queryMax(int node, int l, int r, int ql, int qr) {
if (qr < l || r < ql) return LLONG_MIN;
if (ql <= l && r <= qr) return tree[node];
int mid = (l + r) / 2;
return max(queryMax(2*node, l, mid, ql, qr),
queryMax(2*node+1, mid+1, r, ql, qr));
}
long long queryPathMax(int u, int v) {
long long result = LLONG_MIN;
auto intervals = hld.getPath(u, v);
for (auto& [l, r] : intervals) {
result = max(result, queryMax(1, 0, n-1, l, r));
}
return result;
}
Edge Weights
To support edge weights, store the weight at the child node. When querying a path, exclude the value at the LCA if needed.
// Store edge weight at the lower node (child)
void addEdge(int u, int v, long long w) {
adj[u].push_back({v, w});
adj[v].push_back({u, w});
}
// During DFS, store weight at child
void dfs(int u, int p, long long w) {
edgeWeight[u] = w; // Weight of edge from parent to u
// ...
}
// Query: exclude LCA's value (it's the edge to its parent, not on path)
long long queryPath(int u, int v) {
long long result = 0;
auto intervals = hld.getPath(u, v);
// ...
}
Applications
- Path queries and updates (sum, max, etc.)
- Subtree queries
- Dynamic programming on trees
- Efficiently solving many tree problems in competitive programming.