DSA: Tree DP Explained

7 min read5.2k

Solution to Last Week's Challenge

Last week's problem was: DSA: Monotonic Queue Pattern

The optimal solution uses dsa: monotonic queue pattern pattern with careful state management.

Complexity Analysis:

  • Time Complexity: O(n)
  • Space Complexity: O(1)

Dynamic Programming (DP) is often the most feared topic in technical interviews, but when it is applied to trees, it takes on a surprisingly structured and predictable form. Tree DP is a technique used to solve optimization problems on hierarchical structures by breaking them down into subproblems defined by subtrees. In modern software engineering, this is not just an academic exercise; it is the fundamental logic behind rendering engines, organizational hierarchy calculations, and network routing protocols where data flows in a directed, acyclic manner.

For a senior engineer, mastering Tree DP is about moving beyond simple recursion and understanding how to manage state transitions across a graph. While standard DP often involves an iterative approach with a table, Tree DP almost exclusively uses a bottom-up recursive approach (Post-order Traversal). By the time the recursion returns to the root node, the optimal solution for the entire tree has been aggregated from its descendants. This pattern is a favorite for Tier-1 tech companies because it tests a candidate's ability to handle complex recursion and state management simultaneously.

Theory

The core of Tree DP lies in defining the state of a node based on the states of its children. Unlike linear DP where the state dp[i] depends on dp[i-1], in Tree DP, the state dp[u] depends on the results of dp[v1], dp[v2], ..., dp[vn] where v are children of u.

The general process involves:

  1. State Definition: What information do we need to pass up the tree? (e.g., dp[u][0] = max value excluding node u, dp[u][1] = max value including node u).
  2. Base Case: Defining values for leaf nodes.
  3. Recursive Transition: How to combine child states to form the parent state.
  4. Final Answer: Usually found at the root node after a full DFS traversal.

The time complexity is typically O(N * S) where N is the number of nodes and S is the number of states per node. Since we visit each node once, the efficiency is remarkably high compared to brute-force exponential approaches.

Pattern Visualization

To solve a Tree DP problem, you must visualize the flow of information. We start at the leaves and push "decisions" upward.

Implementation

Let’s look at a classic problem: House Robber III. You are given a binary tree where each node represents a house with a certain amount of money. If two directly-linked houses are broken into on the same night, the alarm sounds. We need the maximum money possible.

Brute Force (Exponential)

The naive approach recalculates subtrees multiple times, leading to O(2^N) in the worst case.

Optimized Tree DP (Linear)

We use a bottom-up approach where each node returns a pair: (max_if_robbed, max_if_not_robbed).

python
class TreeNode:
    def __init__(self, val=0, left=None, right=None):
        self.val = val
        self.left = left
        self.right = right

def solve_house_robber(root):
    # Returns [rob_this_node, skip_this_node]
    def dfs(node):
        if not node:
            return [0, 0]
        
        # Post-order traversal: visit children first
        left_res = dfs(node.left)
        right_res = dfs(node.right)
        
        # If we rob this node, we MUST skip its children
        rob_val = node.val + left_res[1] + right_res[1]
        
        # If we skip this node, we can either rob or skip children
        # We take the max of both options for each child
        skip_val = max(left_res) + max(right_res)
        
        return [rob_val, skip_val]

    # Time Complexity: O(N) - each node visited once
    # Space Complexity: O(H) - recursion stack depth
    return max(dfs(root))

Complexity Analysis Table

ApproachTime ComplexitySpace ComplexityDescription
Naive RecursionO(2^N)O(H)Recomputes identical subproblems multiple times.
Memoization (Top-Down)O(N)O(N)Stores results of dfs(node) in a hash map.
State Return (Bottom-Up)O(N)O(H)Passes state tuples up the tree; most memory efficient.

Common Patterns

1. Subtree Size/Sum

Used for problems like "Count nodes in each subtree" or "Delete nodes to balance a tree."

python
def get_size(node):
    if not node: return 0
    curr_size = 1 + get_size(node.left) + get_size(node.right)
    # Store or use curr_size here
    return curr_size

2. Diameter / Longest Path

Tracking the "best" path that passes through a node versus the "best" path that stays within a subtree.

python
def get_path(node):
    if not node: return 0
    l = get_path(node.left)
    r = get_path(node.right)
    self.ans = max(self.ans, l + r + node.val) # Global update
    return max(l, r) + node.val # Return to parent

Practice Problems

The difficulty of Tree DP problems usually scales with the number of states you need to track.

This Week's Interview Challenge

Problem: The Sentinel Surveillance

You are an engineer designing a security system for a corporate headquarters structured as a tree. You need to place cameras at nodes to monitor every edge in the tree. A camera at a node monitors all edges connected to that node. Find the minimum number of cameras needed to monitor all edges.

Examples:

  • Input: root = [0,1,null,2,3] (A line of 4 nodes)

  • Output: 2 (Placing cameras at node 1 and 3 covers all edges)

  • Input: root = [0,1,2] (A root with two children)

  • Output: 1 (Placing a camera at the root covers both edges)

Constraints:

  • The number of nodes is in the range [1, 10^4].
  • Each node value is unique.

Hints:

  1. Consider the state of a node: Does it have a camera? Is it being "covered" by a child? Is it "covered" by a parent?
  2. Use a bottom-up approach. A leaf node's edge must be covered by its parent to minimize cameras.
  3. Think about the three states: 0 (Node is a leaf/uncovered), 1 (Node has a camera), 2 (Node is covered by a child's camera).

Template:

python
def min_cameras_for_edges(root):
    def solve(node):
        # Your logic here
        pass
    
    # Submit your solution and check back next week for the detailed answer!
    return 0 

Conclusion

Tree DP is the ultimate test of your recursive thinking. The secret to mastering it is to focus entirely on the transition logic at a single node, assuming the children have already provided the correct values. In interviews, clearly define your state (e.g., "State 0 means...") before writing code. This clarity prevents logic errors and shows the interviewer you have a structured approach to complex system design.


References: