DP where the state is a subtree. The dependency order is post-order: a vertex is computed after all of its children.

The skeleton

void dfs(int u, int p) {
    dp[u] = base(u);
    for (int v : adj[u]) {
        if (v == p) continue;
        dfs(v, u);
        dp[u] = combine(dp[u], dp[v]);
    }
}

Everything else is choosing what dp[u] means and how combine works.

Canonical examples

Maximum independent set on a tree

dp[u][0] = best in ‘s subtree with not chosen; dp[u][1] = with chosen.

void dfs(int u, int p) {
    dp[u][0] = 0; dp[u][1] = w[u];
    for (int v : adj[u]) {
        if (v == p) continue;
        dfs(v, u);
        dp[u][0] += max(dp[v][0], dp[v][1]);
        dp[u][1] += dp[v][0];                  // v cannot be chosen
    }
}
// answer: max(dp[root][0], dp[root][1])

The [0/1] second dimension — “is this vertex used” — is the most common tree-DP pattern.

Longest path (diameter) through a vertex

Track the two deepest downward paths and combine them at each vertex. See Tree Diameter.

Counting subtrees / paths

dp[u] = number of ways within ‘s subtree; combine by multiplication or by the “knapsack on tree” merge below.

Tree knapsack — the size-bounded merge

When the state is dp[u][k] (“best using vertices from ‘s subtree”), the naive merge looks per vertex, but bounding the loops by the actual subtree sizes makes the total over the whole tree:

void dfs(int u, int p) {
    sz[u] = 1;
    dp[u][0] = 0; dp[u][1] = w[u];
    for (int v : adj[u]) {
        if (v == p) continue;
        dfs(v, u);
        for (int i = min(sz[u], K); i >= 0; i--)
            for (int j = min(sz[v], K - i); j >= 0; j--)
                tmp[i + j] = max(tmp[i + j], dp[u][i] + dp[v][j]);
        sz[u] += sz[v];
        copy back into dp[u];
    }
}

Why it is and not : every pair of vertices is “merged” exactly once, at their LCA. The nested loops therefore count pairs, and there are of them. Capping at improves it further to .

Getting the loop bounds right (min(sz[u], K), not K) is what makes this work — with fixed bounds it really is .

The patterns

ProblemState
Maximum independent setdp[u][0/1]
Minimum vertex coverdp[u][0/1]
Tree colouring with coloursdp[u][c]
Longest path in a subtreetwo deepest depths
Count paths of length dp[u][d] = vertices at depth in the subtree (merge small-to-large)
Choose exactly verticesdp[u][k] tree knapsack
Distances to all verticesrerooting
Matching on a treedp[u][0/1] = is matched to a child
Distinct colours per subtreeDSU on tree or Euler tour + offline
Delete edges to satisfy a constraintdp[u][state] with the constraint in the state

Small-to-large merging

When each vertex holds a set or map rather than a fixed-size array, merge the smaller structure into the larger:

if (mp[u].size() < mp[v].size()) swap(mp[u], mp[v]);
for (auto& [k, val] : mp[v]) mp[u][k] += val;
mp[v].clear();

Each element moves times, so the total is merges (times the map’s own ). This is how “count distinct values in each subtree” and similar problems get an efficient solution without any heavy machinery. See DSU on Tree.

Iterative tree DP

For deep trees, compute an order once and then process it in reverse:

vector<int> order; order.reserve(n);
stack<int> st; st.push(root); par[root] = -1;
while (!st.empty()) {
    int u = st.top(); st.pop(); order.push_back(u);
    for (int v : adj[u]) if (v != par[u]) { par[v] = u; st.push(v); }
}
for (int i = n - 1; i >= 0; i--) { int u = order[i]; /* combine children of u */ }

Reverse BFS/DFS order is post-order for the purposes of aggregation, and it never overflows the stack.

See also: Rerooting DP · DSU on Tree · Tree Fundamentals