← Back to DSA Course | Chapter 14: Dynamic Programming | Lesson 12 of 12

DP on Trees

Tree DP is like asking every branch of a family tree to report an answer to its parent, so the top person can combine them all.
Syntax
markup
def dfs(node):
    if node is None:
        return (0, 0)    # (skip, take)
    left = dfs(node.left)
    right = dfs(node.right)
    take = node.value + left[0] + right[0]
    skip = max(left) + max(right)
    return (skip, take)

Tree DP Idea

Tree DP solves a problem defined over a tree by computing an answer for each subtree, using the answers already computed for that subtree's children — so information flows upward from the leaves toward the root.

Example: Tree DP Idea

#include <iostream>
using namespace std;
int main() {
	cout << "Tree DP: compute an answer for each subtree, information flows upward from leaves to root";
	return 0;
}
public class Main {
	public static void main(String[] args) {
		System.out.println("Tree DP: compute an answer for each subtree, information flows upward from leaves to root");
	}
}
print("Tree DP: compute an answer for each subtree, information flows upward from leaves to root")
#include <stdio.h>
int main() {
	printf("Tree DP: compute an answer for each subtree, information flows upward from leaves to root");
	return 0;
}

Postorder DP

Because a node's answer typically depends on its children's answers, a postorder traversal (fully process every child before returning to the parent) is the natural way to compute tree DP: by the time you're ready to combine results at a node, every child result is already available.

Example: Postorder DP

#include <iostream>
#include <vector>
using namespace std;
vector<vector<int>> children = {{1,2},{},{}};
int solve(int node) {
	int total = 0;
	for (int c : children[node]) total += solve(c);
	return total + 1;
}
int main() { cout << "Subtree size at root (children fully processed first): " << solve(0); return 0; }
import java.util.*;
public class Main {
	static List<List<Integer>> children = Arrays.asList(Arrays.asList(1,2), Arrays.asList(), Arrays.asList());
	static int solve(int node) {
		int total = 0;
		for (int c : children.get(node)) total += solve(c);
		return total + 1;
	}
	public static void main(String[] args) { System.out.println("Subtree size at root (children fully processed first): " + solve(0)); }
}
children = [[1,2],[],[]]
def solve(node):
    return sum(solve(c) for c in children[node]) + 1
print("Subtree size at root (children fully processed first):", solve(0))
#include <stdio.h>
int children[3][2] = {{1,2},{-1,-1},{-1,-1}};
int solve(int node) {
	int total = 0;
	for (int i = 0; i < 2; i++) if (children[node][i] != -1) total += solve(children[node][i]);
	return total + 1;
}
int main() { printf("Subtree size at root (children fully processed first): %d", solve(0)); return 0; }

Choose or Skip

Some tree DP problems, like the tree version of House Robber, must decide at each node whether to take that node (and therefore skip its immediate children) or skip it (allowing children to be freely taken), and the DP tracks both possibilities separately.

Example: Choose or Skip

#include <iostream>
#include <vector>
using namespace std;
vector<vector<int>> children = {{1,2},{},{}};
int val[3] = {5, 3, 4};
int solveTaken(int node);
int solveSkipped(int node);
int solveTaken(int node) {
	int sum = val[node];
	for (int c : children[node]) sum += solveSkipped(c);
	return sum;
}
int solveSkipped(int node) {
	int sum = 0;
	for (int c : children[node]) sum += max(solveTaken(c), solveSkipped(c));
	return sum;
}
int main() { cout << "House Robber on a tree, best at root: " << max(solveTaken(0), solveSkipped(0)); return 0; }
import java.util.*;
public class Main {
	static List<List<Integer>> children = Arrays.asList(Arrays.asList(1,2), Arrays.asList(), Arrays.asList());
	static int[] val = {5, 3, 4};
	static int solveTaken(int node) {
		int sum = val[node];
		for (int c : children.get(node)) sum += solveSkipped(c);
		return sum;
	}
	static int solveSkipped(int node) {
		int sum = 0;
		for (int c : children.get(node)) sum += Math.max(solveTaken(c), solveSkipped(c));
		return sum;
	}
	public static void main(String[] args) { System.out.println("House Robber on a tree, best at root: " + Math.max(solveTaken(0), solveSkipped(0))); }
}
children = [[1,2],[],[]]
val = [5, 3, 4]
def solve_taken(node):
    return val[node] + sum(solve_skipped(c) for c in children[node])
def solve_skipped(node):
    return sum(max(solve_taken(c), solve_skipped(c)) for c in children[node])
print("House Robber on a tree, best at root:", max(solve_taken(0), solve_skipped(0)))
#include <stdio.h>
int children[3][2] = {{1,2},{-1,-1},{-1,-1}};
int val[3] = {5, 3, 4};
int solveTaken(int node);
int solveSkipped(int node);
int solveTaken(int node) {
	int sum = val[node];
	for (int i = 0; i < 2; i++) if (children[node][i] != -1) sum += solveSkipped(children[node][i]);
	return sum;
}
int solveSkipped(int node) {
	int sum = 0;
	for (int i = 0; i < 2; i++) if (children[node][i] != -1) {
		int c = children[node][i];
		int t = solveTaken(c), s = solveSkipped(c);
		sum += t > s ? t : s;
	}
	return sum;
}
int main() {
	int t = solveTaken(0), s = solveSkipped(0);
	printf("House Robber on a tree, best at root: %d", t > s ? t : s);
	return 0;
}

Tree States

Because a node's best answer can differ depending on whether it was selected or not, many tree DP problems store two values per node instead of one — for example dp[node][0] for 'not taken' and dp[node][1] for taken — and combine them differently when merging into the parent.

Example: Tree States

#include <iostream>
using namespace std;
int main() {
	int dpTaken = 5, dpNotTaken = 7;
	cout << "Two values stored per node: dp[node][0]=not taken=" << dpNotTaken << ", dp[node][1]=taken=" << dpTaken;
	return 0;
}
public class Main {
	public static void main(String[] args) {
		int dpTaken = 5, dpNotTaken = 7;
		System.out.println("Two values stored per node: dp[node][0]=not taken=" + dpNotTaken + ", dp[node][1]=taken=" + dpTaken);
	}
}
dp_taken, dp_not_taken = 5, 7
print(f"Two values stored per node: dp[node][0]=not taken={dp_not_taken}, dp[node][1]=taken={dp_taken}")
#include <stdio.h>
int main() {
	int dpTaken = 5, dpNotTaken = 7;
	printf("Two values stored per node: dp[node][0]=not taken=%d, dp[node][1]=taken=%d", dpNotTaken, dpTaken);
	return 0;
}

Practice

Tree DP combines three ideas at once: recursion to walk the structure, subtree boundaries to define each subproblem, and stored per-node states to avoid recomputing the same subtree's answer more than once.

Example: Practice

#include <iostream>
using namespace std;
int main() {
	cout << "Tree DP = recursion + subtree boundaries + stored per-node states to avoid recomputation";
	return 0;
}
public class Main {
	public static void main(String[] args) {
		System.out.println("Tree DP = recursion + subtree boundaries + stored per-node states to avoid recomputation");
	}
}
print("Tree DP = recursion + subtree boundaries + stored per-node states to avoid recomputation")
#include <stdio.h>
int main() {
	printf("Tree DP = recursion + subtree boundaries + stored per-node states to avoid recomputation");
	return 0;
}
Related Topics
{# common_mistakes/chapter_summary/browser_support: on Hindi pages the view already swaps in the hi_ translation fields (or blanks these out if untranslated), so this renders correctly for both languages without a lang_code check here. #}
Common Mistakes
  1. Processing a node before its children, so its answer uses values that are not computed yet (it needs a postorder traversal).
  2. Not tracking the parent in a general tree stored as an adjacency list, so the DFS goes back up and never ends.
  3. Storing only one value per node when the answer differs for taken and not-taken states.
Chapter Summary
  • Dynamic programming solves problems by storing results of subproblems, using memoization or tabulation.
  • Classic problems include Fibonacci, knapsack, longest common subsequence, longest increasing subsequence, edit distance, and coin change.
  • DP also applies to matrix chain multiplication, grids, and trees.

Login to run this code

C/C++/Java/PHP execution requires a free account. Your code is saved — you'll land right back in the editor after logging in.