ICPC Competitive Programming

Heavy-Light Decomposition

Heavy-Light Decomposition สำหรับ Path Queries บน Tree

Heavy-Light Decomposition (HLD)

Heavy-Light Decomposition เป็นเทคนิคที่แบ่ง tree เป็น chains เพื่อให้สามารถทำ path queries/updates ได้ใน O(log2n)O(\log^2 n) หรือ O(logn)O(\log n)

แนวคิด

แบ่ง edges เป็น 2 ประเภท:

  • Heavy edge: edge ไปยัง child ที่มี subtree ใหญ่ที่สุด
  • Light edge: edges อื่นๆ ทั้งหมด

คุณสมบัติสำคัญ

จาก node ใดๆ ไป root จะผ่าน at most O(logn)O(\log n) light edges

เพราะทุกครั้งที่ขึ้น light edge, subtree size จะ อย่างน้อยสองเท่า

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++;
        
        // First, go down heavy edge
        if (heavy[u] != -1) {
            decompose(heavy[u], h);
        }
        
        // Then, start new chains for light children
        for (int v : adj[u]) {
            if (v != parent[u] && v != heavy[u]) {
                decompose(v, v);  // v is head of new chain
            }
        }
    }
    
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);
            }
            
            // Add interval [pos[head[u]], pos[u]]
            result.push_back({pos[head[u]], pos[u]});
            u = parent[head[u]];
        }
        
        // Same chain now
        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]; }
};

การใช้งานกับ 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) {
            // Find which vertex has pos[v] = l
            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);
        
        // Reorder values according to pos
        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

สำหรับ tree ที่มี weights บน edges แทนที่จะเป็น nodes:

// 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);
    
    for (auto& [l, r] : intervals) {
        result += query(1, 0, n-1, l, r);
    }
    
    // Subtract LCA's edge weight (not part of u-v path)
    int l = hld.lca(u, v);
    result -= edgeWeight[l];
    
    return result;
}

Complexity

OperationTime
BuildO(n)O(n)
Path queryO(log2n)O(\log^2 n)
Path updateO(log2n)O(\log^2 n)
Subtree queryO(logn)O(\log n)
LCAO(logn)O(\log n)

เปรียบเทียบกับเทคนิคอื่น

TechniquePath QueryPath UpdateSubtreeImplementation
HLDO(log2n)O(\log^2 n)O(log2n)O(\log^2 n)O(logn)O(\log n)Medium
Link-Cut TreeO(logn)O(\log n) amortizedO(logn)O(\log n)HardHard
Euler Tour + Seg TreeO(n)O(n)O(n)O(n)O(logn)O(\log n)Easy

Tips

  1. Chain = contiguous interval ใน pos array - ใช้ segment tree ได้ตรงๆ
  2. pos array ทำให้ทั้ง path และ subtree queries เป็น contiguous ranges
  3. Heavy child first - ensure chain continues with heavy edge
  4. at most O(log n) chains บน path ใดๆ - guarantees efficiency