Tree & Graph DP
Dynamic programming whose subproblems are subtrees (or DAG nodes), solved bottom-up with a DFS so each node is computed once from its children.
Instinct A node's best answer depends only on its children's answers - post-order DFS that returns a tiny tuple of states per node.
Memory hack Kids report up, parent decides - every subtree hands its parent a small report card, and the parent never re-reads the kids' homework.
At a glance
When to use it
- The input is a tree (or a DAG) and the answer is a best, count or minimum over the whole structure
- A node's choice only constrains its parent and children, such as take or skip, covered or not
- You need an answer for every node as the root and n DFS runs would be O(n^2)
- You want to count paths or ways through an acyclic graph
Key idea
Treat each subtree as a subproblem and let a post-order DFS return a tiny tuple of states per node, so every node is solved once from its children in O(n) instead of re-exploring subtrees exponentially; rerooting adds a second pass to get all-roots answers in O(n) rather than O(n^2).
Typical complexity
O(n) time, O(h) to O(n) space
| # | Common problems | Level | Our solution | Practice |
|---|---|---|---|---|
| 1 | Diameter of Binary Tree | Easy | Read solution | LeetCode ↗ |
| 2 | House Robber III | Medium | Read solution | LeetCode ↗ |
| 3 | Number of Ways to Arrive at Destination | Medium | Read solution | LeetCode ↗ |
| 4 | Binary Tree Maximum Path Sum | Hard | Read solution | LeetCode ↗ |
| 5 | Binary Tree Cameras | Hard | Read solution | LeetCode ↗ |
| 6 | Sum of Distances in Tree | Hard | Read solution | LeetCode ↗ |
The instinct
When a problem lives on a tree and asks for a best / count / minimum over the whole tree, the subtrees are your subproblems. Each subtree is independent of its siblings once you know what happens at its root, so a post-order DFS can compute a small answer per node and hand it upward. The trick is choosing what each node reports: usually one number per "situation" the parent cares about (taken / not taken, covered / uncovered, distance down / count of nodes).
On a general graph this only works when the dependency graph is acyclic - a DAG, or a DAG you build yourself, like the "shortest-path DAG" behind Number of Ways to Arrive at Destination.
Recognise it
| Signal in the problem | Why it points here |
|---|---|
| "Binary tree" + "maximum sum / minimum number of ..." with an adjacency rule | Choices at a node constrain only its parent and children - classic subtree DP |
| "Adjacent nodes cannot both be chosen" (House Robber III) | Two states per node: chosen or not chosen |
| "Every node must be covered / monitored" (Binary Tree Cameras) | Several coverage states per node, resolved bottom-up |
| "For every node, return the sum / max distance to all others" (Sum of Distances in Tree) | Rerooting: one DFS down, one DFS to push parent info into children |
| "Number of shortest paths" / "count ways from source to target" | DP over a DAG (topological or Dijkstra order) |
| "Generate / count all BSTs on values lo..hi" (Unique Binary Search Trees II) | Subproblem = a range that becomes a subtree; combine left and right results |
n up to 10^5 nodes and an O(n^2) brute force over roots |
Hints that rerooting can turn n DFS runs into two |
Not this pattern if...
- The tree question only needs one value per node with no competing choices (height, size) - that is plain tree DFS.
- The graph has cycles you cannot order (use shortest path / BFS instead of DP).
- You only need a single root-to-leaf path and no aggregation over subtrees - plain recursion or BFS is enough.
Memory hack
Kids report up, parent decides. Every child returns a short report card - one number per state. The parent reads the cards, makes its own choice, and writes its own card. Nobody ever re-visits a grandchild.
Analogy: a company org chart closing its quarterly numbers. Each team lead sends one summary to their manager ("best result if I attend the offsite / if I skip it"). The CEO decides using only the direct reports' summaries.
DP framing:
- State -
dp[node][situation], e.g.situation= robbed / not robbed. - Choice - what the node does (take, skip, place a camera) given what children report.
- Base case - a
nullchild or a leaf returns the neutral report (0 profit, "already covered"). - Order - post-order: children strictly before parent. For rerooting, a second pre-order pass runs parent before children.
Say it out loud in the interview:
- "What does each subtree need to tell its parent?" - name the states.
- "How does a parent combine its children's states?" - write the transition.
- "What does an empty subtree return, and where is the answer - the root, or a global max?"
How it works
Run a DFS. When it returns from a node, all children already have their tuples, so the node combines them in O(children) time. For "answer for every node as the root", do rerooting: first compute down[v] (facts about v's own subtree), then walk from the root downward and convert the parent's full answer into the child's full answer in O(1) per edge.
flowchart TD
A["Pass 1: post-order DFS"] --> B["Leaves return base tuple"]
B --> C["Parent combines children tuples"]
C --> D["Root tuple holds the answer"]
D --> E{"Need answer for every root?"}
E -->|"no"| F["Done: read root or global best"]
E -->|"yes"| G["Pass 2: pre-order DFS"]
G --> H["child answer = parent answer adjusted by child subtree"]
Worked rerooting idea for Sum of Distances in Tree: when the root moves from u to its child v, the size[v] nodes in v's subtree each get 1 closer and the other n - size[v] nodes each get 1 farther, so ans[v] = ans[u] - size[v] + (n - size[v]).
Java template
import java.util.*; class TreeDp { static class TreeNode { int val; TreeNode left, right; TreeNode(int v) { val = v; } } // Post-order tree DP: return one value per state of `node`. // Here state 0 = node not used, state 1 = node used (House Robber III shape). static long[] dfs(TreeNode node) { if (node == null) return new long[]{0, 0}; // base case: empty subtree long[] l = dfs(node.left), r = dfs(node.right); // children first long notUsed = Math.max(l[0], l[1]) + Math.max(r[0], r[1]); // kids free to choose long used = node.val + l[0] + r[0]; // problem rule: kids must be unused return new long[]{notUsed, used}; } } class Rerooting { private List<Integer>[] adj; private int[] size; private long[] down, ans; private int n; // Answer "sum of distances from every node" with two DFS passes. @SuppressWarnings("unchecked") long[] solve(int n, int[][] edges) { this.n = n; adj = new List[n]; for (int i = 0; i < n; i++) adj[i] = new ArrayList<>(); for (int[] e : edges) { adj[e[0]].add(e[1]); adj[e[1]].add(e[0]); } size = new int[n]; down = new long[n]; ans = new long[n]; collect(0, -1); // pass 1: facts about each subtree ans[0] = down[0]; distribute(0, -1); // pass 2: push the full answer to children return ans; } private void collect(int u, int parent) { size[u] = 1; for (int v : adj[u]) { if (v == parent) continue; collect(v, u); size[u] += size[v]; down[u] += down[v] + size[v]; // problem-specific merge } } private void distribute(int u, int parent) { for (int v : adj[u]) { if (v == parent) continue; ans[v] = ans[u] - size[v] + (n - size[v]); // problem-specific shift distribute(v, u); } } }
Complexity
| Variant | Time | Space |
|---|---|---|
Subtree DP with S states |
O(n · S) (O(n · S²) if states pair up) | O(h) recursion + O(n · S) if stored |
| Rerooting | O(n) for two passes | O(n) |
| DAG DP in topological order | O(V + E) | O(V) |
| Counting shortest paths with Dijkstra | O(E log V) | O(V + E) |
| Generating all BSTs (Catalan many) | Exponential output size; memo on (lo, hi) avoids recomputation | Output-bound |
Variations
- Take / skip states - two numbers per node; parent's choice restricts children. House Robber III.
- Multi-state coverage - three states such as "has camera", "covered without camera", "needs cover". Binary Tree Cameras (a greedy leaf-up view of the same states also works).
- Return one, record another - return the best downward chain, but update a global answer with the path that bends through the node. Binary Tree Maximum Path Sum, Diameter of Binary Tree.
- Rerooting / all-roots DP -
downpass thenuppass. Sum of Distances in Tree. - DAG path counting - process nodes in an order where predecessors finish first; for shortest-path counts, Dijkstra pop order is that order. Number of Ways to Arrive at Destination.
- Range-as-subtree - pick a root inside
[lo, hi], recurse on both sides and combine. Unique Binary Search Trees II. - Tree knapsack - each node merges children's "best using k nodes" arrays. Choose k nodes / budgeted subtree problems.
Common mistakes
- Returning a single number when the parent needs to know how the child got it - add a state instead of a global flag.
- In undirected graphs, forgetting the
parentargument (or a visited set), causing infinite recursion. - Recomputing the whole DFS from every root (O(n²)) when one rerooting pass would do.
- Counting paths modulo 1e9+7 but comparing distances as
int- shortest-path totals overflow, so uselong. - Assuming deep trees are safe: a 10^5-node chain can overflow Java's default stack; use an iterative order or a bigger thread stack.
DP needs an acyclic dependency order. On a graph with cycles, "memoised DFS" silently returns wrong answers from half-finished states - first prove the graph is a tree or DAG, or build one (e.g. keep only edges on shortest paths).
All practice problems
5 problems on this site use this pattern, easiest first. Solve them in order and say the instinct out loud before you code.