1. Bottom-Up Subtree DP & The $O(N^2)$ Tree Knapsack Theorem
By rooting an undirected tree arbitrarily at vertex $0$, every vertex $u$ defines a subproblem on its subtree $T_u$, solved in DFS post-order:
- Maximum Weight Independent Set on a Tree (House Robber III): Maintain two states per node: $dp[u][0]$ (max weight in $T_u$ if $u$ is excluded) and $dp[u][1]$ (if $u$ is included): $$dp[u][0] = \sum_{v \in \text{children}(u)} \max(dp[v][0], dp[v][1]), \qquad dp[u][1] = w_u + \sum_{v \in \text{children}(u)} dp[v][0]$$
- Subtree Dependency Knapsack (Selecting $K$ Connected Nodes): Suppose at node $u$ we merge a knapsack table of size $\text{sz}[u]$ with child $v$'s table of size $\text{sz}[v]$. A naive bound where both loops run up to $N$ gives $O(N^3)$. However, if we bound the loop ranges strictly by the current accumulated subtree size $\text{sz}[u]$ and child size $\text{sz}[v]$ before updating $\text{sz}[u] \mathrel{+}= \text{sz}[v]$, what is the true complexity?
Core Invariant — The $O(N^2)$ (and $O(N \cdot K)$) Tree Knapsack Complexity Proof
When merging child $v$ into parent $u$'s partial subtree via nested loops of lengths $\text{sz}_{\text{cur}}[u]$ and $\text{sz}[v]$, the number of inner-loop operations performed at that merge step is $\text{sz}_{\text{cur}}[u] \times \text{sz}[v]$.
Combinatorial interpretation: $\text{sz}_{\text{cur}}[u] \times \text{sz}[v]$ equals the exact number of pairs of vertices $(x, y)$ such that $x$ belongs to a previously merged branch of $u$, $y \in T_v$, and $\text{LCA}(x, y) = u$! Since every pair of vertices $(x, y)$ in the tree has a unique Lowest Common Ancestor $u$, summing over the entire tree counts each pair $(x, y)$ at most once:
If capacities are further capped at $K$, the complexity is strictly $O(N \cdot K)$!
2. The All-Roots Rerooting Technique (Up-Down / In-Out DP)
What if we need the tree DP answer when every vertex $u \in \{0..N-1\}$ is chosen as the root (e.g., Sum of Distances in Tree, Minimum Height Trees, or Maximum Path from Every Node)? Running $N$ separate DFS traversals takes $O(N^2)$. Instead, Two-Pass Rerooting solves all $N$ roots in $O(N)$:
- Pass 1 (Post-Order Downward DP
down[u]): Root at $0$ and computedown[u]for every subtree $T_u$. - Pass 2 (Pre-Order Upward DP
up[v]): As we step from parent $u$ to child $v$, the "complement subtree" $T \setminus T_v$ seen by $v$ consists of $u$'s upward contributionup[u]merged with all siblings $w \in \text{children}(u) \setminus \{v\}$. To avoid division (which fails on $\max/\min$ or $\bmod p$ zeroes), precompute prefix and suffix merges of $u$'s children!
of siblings != v_2"] C1b["Sibling v_1: down[v_1]"] --> Combine Combine --> UpV2["up[v_2] passed into Child v_2"] end
Hardware Reality — Why Bound-Capped Tree Knapsack Must Update sz[u] += sz[v] AFTER the Merge Loop
A single-line bug turns an $O(N^2)$ Tree Knapsack into an $O(N^3)$ TLE: if you increment sz[u] += sz[v] before the merge loop (or loop up to the precomputed total subtree size instead of the running accumulated size), the first child merge at the root iterates $N \times \text{sz}[v_1]$ instead of $1 \times \text{sz}[v_1]$, destroying the LCA pair-counting invariant!
3. Complexity Matrix
Step through upper-triangular Interval DP diagonal wavefronts dp[i][j] by length L = 2..N, or trace Two-Pass Tree Rerooting (bottom-up subtree down[u] + top-down prefix/suffix up[u]).
Invariant & State
| Tree DP Pattern | Naive Approach | Optimal Tree DP Time | Auxiliary Space | Key Mechanism |
|---|---|---|---|---|
| Max Independent Set / Vertex Cover | O(2^N) | O(N) | O(N) | 2-state post-order $dp[u][0/1]$ |
| Tree Diameter / Max Path Sum | O(N^2) | O(N) | O(N) | Combine top-2 child depths at each LCA |
| Subtree Knapsack (Cap $K \le N$) | O(N * K^2) | O(N * K) | O(N * K) | Cap loops at $\min(K, \text{sz}_{\text{cur}}[u])$ |
| All-Roots Queries (Rerooting DP) | O(N^2) | O(N) | O(N) | Downward post-order + Upward prefix/suffix |
4. Reference C++20 Implementation
#include <algorithm>
#include <cstdint>
#include <vector>
// 1. All-Roots Rerooting DP: Compute sum of distances from EVERY node u to all other nodes in O(N)
std::vector<long long> sum_of_distances_in_tree(int n, const std::vector<std::pair<int, int>>& edges) {
std::vector<std::vector<int>> adj(n);
for (auto [u, v] : edges) {
adj[u].push_back(v);
adj[v].push_back(u);
}
std::vector<int> sz(n, 1);
std::vector<long long> ans(n, 0);
// Pass 1 (Bottom-Up): Compute subtree sizes and ans[0] = sum of depths from root 0
auto dfs_down = [&](auto& self, int u, int p) -> void {
for (int v : adj[u]) {
if (v == p) continue;
self(self, v, u);
sz[u] += sz[v];
ans[0] += ans[v] + sz[v]; // momentarily use ans[u] to accumulate subtree dist sum
}
if (u != 0) {
long long sub_sum = 0;
for (int v : adj[u]) if (v != p) sub_sum += ans[v] + sz[v];
ans[u] = sub_sum;
}
};
dfs_down(dfs_down, 0, -1);
// Pass 2 (Top-Down Rerooting): Shift root from u to child v in O(1):
// Nodes in T_v (sz[v] of them) get 1 step closer; nodes outside T_v (n - sz[v]) get 1 step farther!
auto dfs_up = [&](auto& self, int u, int p) -> void {
for (int v : adj[u]) {
if (v == p) continue;
ans[v] = ans[u] - sz[v] + (n - sz[v]);
self(self, v, u);
}
};
dfs_up(dfs_up, 0, -1);
return ans;
}
// 2. O(N * K) Tree Dependency Knapsack (Must pick parent u to pick any node in T_u)
std::vector<long long> tree_knapsack_Exact_ONK(
int n, int k, const std::vector<int>& parent, const std::vector<long long>& profit) {
std::vector<std::vector<int>> children(n);
int root = 0;
for (int i = 0; i < n; ++i) {
if (parent[i] == -1) root = i;
else children[parent[i]].push_back(i);
}
constexpr long long NEG_INF = -(1LL << 60);
std::vector<std::vector<long long>> dp(n);
std::vector<int> sz(n, 0);
auto dfs = [&](auto& self, int u) -> void {
sz[u] = 1;
dp[u] = { 0LL, profit[u] }; // Size 0 -> 0, Size 1 (just u) -> profit[u]
for (int v : children[u]) {
self(self, v);
int new_cap = std::min(k, sz[u] + sz[v]);
std::vector<long long> next_dp(new_cap + 1, NEG_INF);
next_dp[0] = 0;
// CRITICAL: Bound i by current sz[u] and j by sz[v] BEFORE adding sz[v] to sz[u]!
for (int i = 1; i <= std::min(k, sz[u]); ++i) {
for (int j = 0; j <= std::min(k - i, sz[v]); ++j) {
next_dp[i + j] = std::max(next_dp[i + j], dp[u][i] + dp[v][j]);
}
}
dp[u] = std::move(next_dp);
sz[u] += sz[v]; // Update sz[u] AFTER the merge!
}
};
dfs(dfs, root);
return dp[root];
}
Production Pitfall — Using Inverse Operations (Subtraction / Division) in Rerooting When Merging $\max$ or $\min$
For additive objectives like Sum of Distances, removing child $v$'s contribution from $u$ is a simple subtraction. However, when the DP transition uses a non-invertible operator like $\max$ (e.g., Eccentricity / Longest Path from Every Node) or multiplication $\bmod M$ (where a child factor could be $0 \pmod M$), you cannot divide or subtract! Instead, for a node $u$ with $d$ children $v_0, \dots, v_{d-1}$, precompute prefix merges $P[i] = \bigoplus_{j < i} f(v_j)$ and suffix merges $S[i] = \bigoplus_{j > i} f(v_j)$ in $O(d)$ time so child $v_i$ receives $P[i] \oplus S[i] \oplus \text{up}[u]$ in $O(1)$ without inversion.