Core Logic and Mathematical Principles
Heavy Path Decomposition is an advanced technique for losslessly transforming tree topologies into linear structures. The core motivation is to break the discreteness of tree branches, allowing any two node paths or subtree intervals in the tree to be isomorphically mapped to at most $O(\log N)$ contiguous memory intervals, thus enabling dynamic maintenance at $O(\log N)$ level using segment trees or binary indexed trees.
1. Core Definitions
For any non-leaf node $u$ in the tree:
- Heavy Son: Among all its child nodes $v \in \text{adj}[u]$, the node with the largest subtree size (i.e., the largest $\text{siz}[v]$), denoted as $\text{son}[u]$. If there are multiple child nodes with the same subtree size, any one can be chosen.
- Light Son: All other child nodes except the heavy son.
- Heavy Edge: The edge connecting node $u$ to its heavy son $\text{son}[u]$.
- Light Edge: The edge connecting node $u$ to its light sons.
- Heavy Chain: A maximal chain formed by connecting multiple heavy edges end to end. The entire tree structure is strictly divided into several mutually exclusive heavy chains. All leaf nodes that do not have a heavy son form their own independent heavy chains.
2. Mathematical Theorems and Complexity Proof
Theorem: The number of light edges traversed and the number of heavy chain disconnections when moving from any node $u$ towards the root node is strictly no more than $\log_2 N$. Proof: According to the definition of heavy son, when moving from node $u$ along a light edge to its parent node $f$, it must hold that $\text{siz}[u] \le \frac{1}{2} \text{siz}[f]$. This means that for every light edge traversed, the size of the current subtree at least doubles. Since the total number of nodes in the tree is $N$, this doubling operation can occur at most $\log_2 N$ times. Thus, it can be proven that any path in the tree can be decomposed into at most $O(\log N)$ segments of heavy chains.
State Design and Algorithm Derivation
Implementing heavy path decomposition requires two independent DFS processes.
1. First DFS (Preprocessing Topological Properties)
Define the state arrays:
- $\text{fa}[u]$: The parent node of $u$.
- $\text{dep}[u]$: The topological depth of $u$.
- $\text{siz}[u]$: The total number of nodes in the subtree rooted at $u$.
- $\text{son}[u]$: The index of the heavy son of $u$.
State Transition Equations:
$$\text{siz}[u] = 1 + \sum_{v \in \text{adj}[u], v \neq \text{fa}[u]} \text{siz}[v]$$
$$\text{son}[u] = \arg\max_{v} \{\text{siz}[v] \mid v \in \text{adj}[u], v \neq \text{fa}[u]\}$$
2. Second DFS (Decomposing Chains and Determining Linear Sequences)
Define the state arrays:
- $\text{top}[u]$: The top node of the heavy chain that $u$ belongs to (the node with the smallest depth).
- $\text{dfn}[u]$: The new timestamp of $u$ after decomposition (i.e., its index in the linear sequence).
Decomposition Strategy:
- Priority of Heavy Sons: When recursively traversing downwards, always first perform a deep traversal on the heavy son $\text{son}[u]$. This ensures that all nodes on the same heavy chain occupy a contiguous memory address in the $\text{dfn}$ sequence.
- State Transfer:
- For the heavy son $\text{son}[u]$, its chain top directly inherits: $\text{top}[\text{son}[u]] = \text{top}[u]$.
- For any light son $v$, it forms a new chain starting point, with its top being itself: $\text{top}[v] = v$.
C++ Standard Source Code
#include <iostream>
#include <vector>
#include <algorithm>
const int MAXN = 200005;
std::vector<int> adj[MAXN];
int fa[MAXN], dep[MAXN], siz[MAXN], son[MAXN];
int top[MAXN], dfn[MAXN], rnki[MAXN], tim;
// First DFS: Calculate fa, dep, siz, son
void dfs1(int u, int p) {
fa[u] = p;
dep[u] = dep[p] + 1;
siz[u] = 1;
son[u] = 0; // Initialize without heavy son
int max_siz = 0;
for (size_t i = 0; i < adj[u].size(); ++i) {
int v = adj[u][i];
if (v == p) continue;
dfs1(v, u);
siz[u] += siz[v];
if (siz[v] > max_siz) {
max_siz = siz[v];
son[u] = v; // Update heavy son
}
}
}
// Second DFS: Calculate top, dfn, note the priority of heavy son
void dfs2(int u, int t) {
top[u] = t;
tim++;
dfn[u] = tim;
rnki[tim] = u; // Mapping reverse lookup: original tree node corresponding to linear index
if (!son[u]) return; // Leaf node breakpoint
// Fatal pitfall: must and can only first recursively process the heavy son to ensure continuity of the heavy chain sequence
dfs2(son[u], t);
for (size_t i = 0; i < adj[u].size(); ++i) {
int v = adj[u][i];
if (v == fa[u] || v == son[u]) continue;
dfs2(v, v); // Light son forms a new chain, top is itself
}
}
// Jump heavy chain to find LCA (industry standard, very low constant, beats doubling)
int lca_by_heavy_edge(int u, int v) {
while (top[u] != top[v]) {
if (dep[top[u]] < dep[top[v]]) {
std::swap(u, v);
}
u = fa[top[u]]; // Jump the deeper node at the chain top to its parent node
}
return dep[u] < dep[v] ? u : v;
}
int main() {
std::ios_base::sync_with_stdio(false);
std::cin.tie(NULL);
int n, root;
if (!(std::cin >> n >> root)) return 0;
for (int i = 1; i < n; ++i) {
int u, v;
std::cin >> u >> v;
adj[u].push_back(v);
adj[v].push_back(u);
}
dep[0] = 0;
tim = 0;
dfs1(root, 0);
dfs2(root, root);
// Output the linear sequence positions after flattening for each point
for (int i = 1; i <= n; ++i) {
std::cout << "Node " << i << " Map to Segment-Tree Index: " << dfn[i] << "\n";
}
return 0;
}
NOIP Practical Pitfall Guide
- Confusion in Heavy Son Determination Conditions in Second DFS:
When traversing the adjacency list in
dfs2, it is easy to missif (v == son[u]) continue;, leading to a second deep traversal on the heavy son. This not only completely destroys the linear physical address continuity of the heavy chain but also renders subsequent segment tree interval queries completely ineffective, potentially causing aSIGSEGVdue to infinite loops. - Uninitialized
timorsonArrays for Multiple Data Sets: Thesonarray stores heavy son pointers. If there are multiple test data sets and thesonarray is not completely cleared, when encountering a leaf node that originally has no heavy son, it may read residual dirty data from the previous set, causingdfs2to continue calling non-existent nodes, leading to illegal memory access.
Classic NOIP/Luogu Problems
1. Luogu P2590 [ZJOI2008] Tree Statistics
- Problem Description: Given a tree with $N$ nodes, each node has its weight. Three operations need to be implemented: modify the weight of a certain node; query the maximum weight of all nodes on the path from node $u$ to $v$; query the total weight of all nodes on the path from node $u$ to $v$.
- Essence and Core Idea of the Problem:
Standard template application of heavy path decomposition. The core idea is to flatten the tree using
dfs1anddfs2. Since the tree decomposition guarantees the continuity of heavy chains in the $\text{dfn}$ sequence, when querying the path from $u$ to $v$, iftop[u] != top[v], the deeper node at the chain top (assumed to be $u$) performs a maximum or sum query on the continuous interval $[\text{dfn}[\text{top}[u]], \text{dfn}[u]]$ using a segment tree, and then let $u = \text{fa}[\text{top}[u]]$ to jump out of that chain. When both are on the same chain, perform a segment tree operation on the continuous interval $[\min(\text{dfn}[u], \text{dfn}[v]), \max(\text{dfn}[u], \text{dfn}[v])]$ to finalize the result.
2. Luogu P2146 [NOI2015] Package Manager
- Problem Description: The dependency relationships of software packages form a tree structure, and uninstalling or installing a software will affect a series of nodes. Specifically, when installing software $x$, all uninstalled software on the path from $x$ to the root node must be installed; when uninstalling software $x$, all installed software within its subtree must be uninstalled. Determine the number of software that changes state with each operation.
- Essence and Core Idea of the Problem: Heavy path decomposition provides a comprehensive mapping of paths and subtrees. The install operation essentially sets all nodes on the path from $x$ to the root node to 1 (installed), which requires utilizing the tree decomposition to cover multiple continuous linear intervals with a segment tree in $O(\log^2 N)$ time. The uninstall operation sets the entire subtree rooted at $x$ to 0, and due to the properties of heavy path decomposition, any subtree is also a completely contiguous closed interval $[\text{dfn}[x], \text{dfn}[x] + \text{siz}[x] - 1]$ in the $\text{dfn}$ sequence. This transforms the subtree uninstall operation into a single segment tree interval cover, showcasing the dual capabilities of tree decomposition in governing both paths and subtrees.