Heavy-Light Decomposition (HLD)

Introduction

Heavy-Light Decomposition is a technique for decomposing a tree into a set of disjoint paths that allows efficient path queries and updates. It's particularly useful for solving problems involving tree path operations.

Algorithm Overview

The main idea is to break the tree into chains (called heavy paths) such that:

  1. Each path from a node to the root passes through at most O(log n) different chains
  2. We can perform operations on these chains efficiently using data structures like segment trees

Mathematics Behind HLD

The decomposition relies on classifying edges as either heavy or light:

For each non-leaf node u, we select one child v where:

  • size(v) ≥ size(u)/2 (this is the heavy child)
  • The edge (u,v) is a heavy edge

All other edges from u to its children are light edges. The size of a node is the number of nodes in its subtree.

Key Properties:

  • The number of light edges on any path from root to leaf is O(log n)
  • Each heavy path is a single chain that can be processed efficiently
  • Any path in the tree can be broken into O(log n) heavy and light paths

Time and Space Complexity

  • Preprocessing: O(n) time to compute sizes and decompose the tree
  • Path Queries: O(log²n) per query when using segment trees
  • Space Complexity: O(n) to store the decomposition

Problem Examples

Easy Problems

1. Path Maximum Query (Codeforces EDU)

Given a tree with node values, answer queries about the maximum value on a path between two nodes.


#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

const int MAXN = 1e5 + 5;
vector<int> adj[MAXN];
int val[MAXN], parent[MAXN], depth[MAXN], size[MAXN];
int chain[MAXN], head[MAXN], pos[MAXN];
int seg[MAXN * 4];
int n, chain_num, pos_num;

void dfs_size(int u, int p) {
    size[u] = 1;
    parent[u] = p;
    for (int &v : adj[u]) {
        if (v != p) {
            depth[v] = depth[u] + 1;
            dfs_size(v, u);
            size[u] += size[v];
            if (size[v] > size[adj[u][0]] || adj[u][0] == p)
                swap(v, adj[u][0]);
        }
    }
}

void dfs_hld(int u, int p) {
    pos[u] = pos_num++;
    chain[u] = chain_num;
    for (int v : adj[u]) {
        if (v != p) {
            if (v == adj[u][0])
                head[v] = head[u];
            else {
                chain_num++;
                head[v] = v;
            }
            dfs_hld(v, u);
        }
    }
}

void build_seg(int node, int l, int r) {
    if (l == r) {
        seg[node] = val[l];
        return;
    }
    int mid = (l + r) / 2;
    build_seg(node * 2, l, mid);
    build_seg(node * 2 + 1, mid + 1, r);
    seg[node] = max(seg[node * 2], seg[node * 2 + 1]);
}

int query_seg(int node, int l, int r, int ql, int qr) {
    if (qr < l || r < ql) return -1;
    if (ql <= l && r <= qr) return seg[node];
    int mid = (l + r) / 2;
    return max(query_seg(node * 2, l, mid, ql, qr),
               query_seg(node * 2 + 1, mid + 1, r, ql, qr));
}

int query_path(int u, int v) {
    int res = -1;
    while (chain[u] != chain[v]) {
        if (depth[head[u]] > depth[head[v]]) swap(u, v);
        res = max(res, query_seg(1, 0, n-1, pos[head[v]], pos[v]));
        v = parent[head[v]];
    }
    if (depth[u] > depth[v]) swap(u, v);
    res = max(res, query_seg(1, 0, n-1, pos[u], pos[v]));
    return res;
}

int main() {
    cin >> n;
    for (int i = 1; i < n; i++) {
        int u, v; cin >> u >> v;
        adj[u].push_back(v);
        adj[v].push_back(u);
    }
    for (int i = 1; i <= n; i++) cin >> val[i];
    
    dfs_size(1, 0);
    head[1] = 1;
    dfs_hld(1, 0);
    build_seg(1, 0, n-1);
    
    int q; cin >> q;
    while (q--) {
        int u, v; cin >> u >> v;
        cout << query_path(u, v) << "\n";
    }
    return 0;
}

Input/Output Example

Input:
5
1 2
1 3
3 4
3 5
10 20 30 40 50
3
1 5
2 4
4 5

Output:
50
40
50

2. Subtree Sum Query

Given a tree with node values, answer queries about the sum of values in a subtree.

     
#include <iostream>
#include <vector>
using namespace std;

const int MAXN = 1e5 + 5;
vector<int> adj[MAXN];
int val[MAXN], in[MAXN], out[MAXN], flat[MAXN], seg[MAXN * 4];
int n, timer;

void dfs(int u, int p) {
    in[u] = timer++;
    flat[in[u]] = val[u];
    for (int v : adj[u]) {
        if (v != p) dfs(v, u);
    }
    out[u] = timer - 1;
}

void build(int node, int l, int r) {
    if (l == r) {
        seg[node] = flat[l];
        return;
    }
    int mid = (l + r) / 2;
    build(node * 2, l, mid);
    build(node * 2 + 1, mid + 1, r);
    seg[node] = seg[node * 2] + seg[node * 2 + 1];
}

int query(int node, int l, int r, int ql, int qr) {
    if (qr < l || r < ql) return 0;
    if (ql <= l && r <= qr) return seg[node];
    int mid = (l + r) / 2;
    return query(node * 2, l, mid, ql, qr) + 
           query(node * 2 + 1, mid + 1, r, ql, qr);
}

int main() {
    cin >> n;
    for (int i = 1; i < n; i++) {
        int u, v; cin >> u >> v;
        adj[u].push_back(v);
        adj[v].push_back(u);
    }
    for (int i = 1; i <= n; i++) cin >> val[i];
    
    dfs(1, 0);
    build(1, 0, n-1);
    
    int q; cin >> q;
    while (q--) {
        int u; cin >> u;
        cout << query(1, 0, n-1, in[u], out[u]) << "\n";
    }
    return 0;
}
 

Intermediate Problems

1. Dynamic Tree Connectivity (Codeforces 117E)

Maintain a tree that supports edge addition/removal and connectivity queries.

// This problem requires Link-Cut Trees which are more advanced
// Implementation would be too long for this example

2. Path XOR Query (Codeforces 888G)

Given a tree with node values, answer queries about XOR on paths between nodes.

    
#include <iostream>
#include <vector>
using namespace std;

const int MAXN = 1e5 + 5;
vector<int> adj[MAXN];
int val[MAXN], parent[MAXN], depth[MAXN], size[MAXN];
int chain[MAXN], head[MAXN], pos[MAXN], xor_up[MAXN];
int n, chain_num, pos_num;

void dfs_size(int u, int p) {
    size[u] = 1;
    parent[u] = p;
    for (int &v : adj[u]) {
        if (v != p) {
            depth[v] = depth[u] + 1;
            xor_up[v] = xor_up[u] ^ val[v];
            dfs_size(v, u);
            size[u] += size[v];
            if (size[v] > size[adj[u][0]] || adj[u][0] == p)
                swap(v, adj[u][0]);
        }
    }
}

void dfs_hld(int u, int p) {
    pos[u] = pos_num++;
    chain[u] = chain_num;
    for (int v : adj[u]) {
        if (v != p) {
            if (v == adj[u][0])
                head[v] = head[u];
            else {
                chain_num++;
                head[v] = v;
            }
            dfs_hld(v, u);
        }
    }
}

int lca(int u, int v) {
    while (chain[u] != chain[v]) {
        if (depth[head[u]] > depth[head[v]]) swap(u, v);
        v = parent[head[v]];
    }
    return depth[u] < depth[v] ? u : v;
}

int query_xor(int u, int v) {
    int ancestor = lca(u, v);
    return xor_up[u] ^ xor_up[v] ^ val[ancestor];
}

int main() {
    cin >> n;
    for (int i = 1; i < n; i++) {
        int u, v; cin >> u >> v;
        adj[u].push_back(v);
        adj[v].push_back(u);
    }
    for (int i = 1; i <= n; i++) cin >> val[i];
    
    xor_up[1] = val[1];
    dfs_size(1, 0);
    head[1] = 1;
    dfs_hld(1, 0);
    
    int q; cin >> q;
    while (q--) {
        int u, v; cin >> u >> v;
        cout << query_xor(u, v) << "\n";
    }
    return 0;
}

Hard Problem

1. Dynamic Tree Diameter (Codeforces 1192B)

Maintain a tree that supports edge weight updates and answers diameter queries.

#include <iostream>
    #include <vector>
    #include <algorithm>
    using namespace std;
    
    const int MAXN = 1e5 + 5;
    const int INF = 1e9;
    
    struct Edge {
        int u, v, w;
    };
    
    vector<pair<int, int>> adj[MAXN];
    vector<Edge> edges;
    int parent[MAXN], depth[MAXN], size[MAXN];
    int chain[MAXN], head[MAXN], pos[MAXN], edge_pos[MAXN];
    int base[MAXN], chain_num, pos_num;
    int n, q;
    long long W;
    
    struct Node {
        long long max_dist;
        long long left_max, right_max;
        long long diameter;
        Node() : max_dist(-INF), left_max(-INF), right_max(-INF), diameter(-INF) {}
        Node(long long d) : max_dist(d), left_max(d), right_max(d), diameter(0) {}
    };
    
    Node seg[MAXN * 4];
    long long lazy[MAXN * 4];
    
    Node combine(Node a, Node b) {
        Node res;
        res.max_dist = max(a.max_dist, b.max_dist);
        res.left_max = max(a.left_max, b.left_max);
        res.right_max = max(a.right_max, b.right_max);
        res.diameter = max({a.diameter, b.diameter, a.right_max + b.left_max});
        return res;
    }
    
    void build(int node, int l, int r) {
        if (l == r) {
            seg[node] = Node(base[l]);
            return;
        }
        int mid = (l + r) / 2;
        build(node * 2, l, mid);
        build(node * 2 + 1, mid + 1, r);
        seg[node] = combine(seg[node * 2], seg[node * 2 + 1]);
    }
    
    void push(int node, int l, int r) {
        if (lazy[node] == 0) return;
        seg[node].max_dist += lazy[node];
        seg[node].left_max += lazy[node];
        seg[node].right_max += lazy[node];
        if (l != r) {
            lazy[node * 2] += lazy[node];
            lazy[node * 2 + 1] += lazy[node];
        }
        lazy[node] = 0;
    }
    
    void update(int node, int l, int r, int ql, int qr, long long val) {
        push(node, l, r);
        if (qr < l || r < ql) return;
        if (ql <= l && r <= qr) {
            lazy[node] += val;
            push(node, l, r);
            return;
        }
        int mid = (l + r) / 2;
        update(node * 2, l, mid, ql, qr, val);
        update(node * 2 + 1, mid + 1, r, ql, qr, val);
        seg[node] = combine(seg[node * 2], seg[node * 2 + 1]);
    }
    
    Node query(int node, int l, int r, int ql, int qr) {
        push(node, l, r);
        if (qr < l || r < ql) return Node();
        if (ql <= l && r <= qr) return seg[node];
        int mid = (l + r) / 2;
        return combine(query(node * 2, l, mid, ql, qr),
                       query(node * 2 + 1, mid + 1, r, ql, qr));
    }
    
    void dfs_size(int u, int p) {
        size[u] = 1;
        parent[u] = p;
        for (auto &[v, idx] : adj[u]) {
            if (v != p) {
                depth[v] = depth[u] + 1;
                dfs_size(v, u);
                size[u] += size[v];
                if (size[v] > size[adj[u][0].first] || adj[u][0].first == p)
                    swap(v, adj[u][0].first), swap(idx, adj[u][0].second);
            }
        }
    }
    
    void dfs_hld(int u, int p, int edge_idx) {
        pos[u] = pos_num;
        if (edge_idx != -1) {
            edge_pos[edge_idx] = pos_num;
            base[pos_num++] = edges[edge_idx].w;
        } else {
            base[pos_num++] = 0;
        }
        chain[u] = chain_num;
        
        bool heavy = true;
        for (auto [v, idx] : adj[u]) {
            if (v != p) {
                if (heavy) {
                    head[v] = head[u];
                    dfs_hld(v, u, idx);
                    heavy = false;
                } else {
                    chain_num++;
                    head[v] = v;
                    dfs_hld(v, u, idx);
                }
            }
        }
    }
    
    void update_edge(int idx, long long new_w) {
        long long delta = new_w - edges[idx].w;
        edges[idx].w = new_w;
        int u = edges[idx].u, v = edges[idx].v;
        if (depth[u] > depth[v]) swap(u, v);
        update(1, 0, pos_num - 1, edge_pos[idx], edge_pos[idx], delta);
    }
    
    long long get_diameter() {
        Node res = query(1, 0, pos_num - 1, 0, pos_num - 1);
        return res.diameter;
    }
    
    int main() {
        ios_base::sync_with_stdio(false);
        cin.tie(nullptr);
        
        cin >> n >> q >> W;
        edges.resize(n - 1);
        for (int i = 0; i < n - 1; i++) {
            cin >> edges[i].u >> edges[i].v >> edges[i].w;
            adj[edges[i].u].emplace_back(edges[i].v, i);
            adj[edges[i].v].emplace_back(edges[i].u, i);
        }
        
        dfs_size(1, 0);
        head[1] = 1;
        dfs_hld(1, 0, -1);
        build(1, 0, pos_num - 1);
        
        long long last = 0;
        while (q--) {
            long long d, e;
            cin >> d >> e;
            d = (d + last) % (n - 1);
            e = (e + last) % W;
            update_edge(d, e);
            last = get_diameter();
            cout << last << '\n';
        }
        
        return 0;
    }
    

Explanation:

This solution maintains a tree that supports edge weight updates and answers diameter queries using HLD with a segment tree. Key components:

  1. HLD Decomposition: The tree is decomposed into heavy paths
  2. Segment Tree: Stores node information including maximum distances and diameter
  3. Updates: When an edge weight changes, we update the corresponding segment in the tree
  4. Queries: The diameter is maintained by combining information from all paths

Input/Output Example:

    Input:
    3 3 100
    1 2 10
    2 3 20
    1 40
    0 60
    1 80
    
    Output:
    60
    80
    100
    

The solution efficiently handles edge weight updates and diameter queries in O(log²n) time per operation.