Heavy-Light Decomposition
Heavy-Light Decomposition สำหรับ Path Queries บน Tree
Heavy-Light Decomposition (HLD)
Heavy-Light Decomposition เป็นเทคนิคที่แบ่ง tree เป็น chains เพื่อให้สามารถทำ path queries/updates ได้ใน หรือ
แนวคิด
แบ่ง edges เป็น 2 ประเภท:
- Heavy edge: edge ไปยัง child ที่มี subtree ใหญ่ที่สุด
- Light edge: edges อื่นๆ ทั้งหมด
คุณสมบัติสำคัญ
จาก node ใดๆ ไป root จะผ่าน at most 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
| Operation | Time |
|---|---|
| Build | |
| Path query | |
| Path update | |
| Subtree query | |
| LCA |
เปรียบเทียบกับเทคนิคอื่น
| Technique | Path Query | Path Update | Subtree | Implementation |
|---|---|---|---|---|
| HLD | Medium | |||
| Link-Cut Tree | amortized | Hard | Hard | |
| Euler Tour + Seg Tree | Easy |
Tips
- Chain = contiguous interval ใน pos array - ใช้ segment tree ได้ตรงๆ
- pos array ทำให้ทั้ง path และ subtree queries เป็น contiguous ranges
- Heavy child first - ensure chain continues with heavy edge
- at most O(log n) chains บน path ใดๆ - guarantees efficiency