Coding Track

Blind 75 Graphs, Trees, And DP Deep Dive

Practice the patterns that most often decide advanced engineering coding rounds: state search, cycle detection, recursive invariants, memoization, and dynamic programming transitions.

Interview Method

Say The State Before The Code

For graph, tree, and DP questions, a strong answer starts by naming the state, invariant, transition, termination condition, and tests. Code becomes much easier once those five pieces are explicit.

  1. State: what node, index, amount, or subtree does this call or table entry represent?
  2. Invariant: what is true after every pop, visit, return, or table update?
  3. Transition: how does the solution move to smaller or adjacent states?
  4. Termination: what base case, visited set, or queue exhaustion ends the search?
  5. Tests: empty input, one node, cycle, disconnected graph, duplicate values, and impossible state.
Question: What is the fastest way to recover when stuck?

Write the recursive or iterative state in English, then trace a tiny input. For DP, draw three cells or calls and decide which earlier answers they need. For graphs, mark exactly when a node becomes visited so duplicates and cycles cannot explode the search.

Graph Patterns

Reachability, Cycles, And Shortest Paths

Worked Graph 1: Clone Graph

Given a connected undirected graph node, return a deep copy of the graph. Each node has a value and a list of neighbors.

Hidden answer: invariant, common mistake, and Python solution

Invariant: once an original node is in copies, every future edge that reaches that original should reuse the same clone. The common mistake is cloning neighbors before memoizing the current node, which recurses forever on cycles.

def clone_graph(node):
    if node is None:
        return None

    copies = {}

    def dfs(original):
        if original in copies:
            return copies[original]

        clone = Node(original.val)
        copies[original] = clone
        clone.neighbors = [dfs(neighbor) for neighbor in original.neighbors]
        return clone

    return dfs(node)

Worked Graph 2: Pacific Atlantic Water Flow

Given heights, return cells where water can flow to both the Pacific and Atlantic borders. Water can flow from a cell to a neighbor of lower or equal height.

Hidden answer: invariant, test cases, and Python solution

Reverse the flow. Invariant: a cell reached from an ocean border by moving only to equal-or-higher neighbors can drain back to that ocean in the original direction. Test one row, one column, flat terrain, and a high ridge surrounded by lower cells.

def pacific_atlantic(heights):
    if not heights or not heights[0]:
        return []

    rows, cols = len(heights), len(heights[0])

    def flood(starts):
        seen = set(starts)
        stack = list(starts)
        while stack:
            r, c = stack.pop()
            for dr, dc in ((1, 0), (-1, 0), (0, 1), (0, -1)):
                nr, nc = r + dr, c + dc
                if (
                    0 <= nr < rows
                    and 0 <= nc < cols
                    and (nr, nc) not in seen
                    and heights[nr][nc] >= heights[r][c]
                ):
                    seen.add((nr, nc))
                    stack.append((nr, nc))
        return seen

    pacific = flood([(0, c) for c in range(cols)] + [(r, 0) for r in range(rows)])
    atlantic = flood([(rows - 1, c) for c in range(cols)] + [(r, cols - 1) for r in range(rows)])
    return [[r, c] for r, c in sorted(pacific & atlantic)]

Tree Patterns

Return One Thing, Update Another

Many tree questions return a local value to the parent while updating a best answer outside the recursion. Be explicit about which is which.

Worked Tree 1: Binary Tree Maximum Path Sum

Return the maximum path sum in a binary tree. A path can start and end at any nodes, but it must follow parent-child links.

Hidden answer: invariant, common mistake, and Python solution

Invariant: the recursive return value is the best one-sided path that can extend to the parent. The global best can use both children through the current node. The common mistake is returning a forked path upward, which is not a valid path for the parent to extend.

def max_path_sum(root):
    best = -10**18

    def one_sided(node):
        nonlocal best
        if node is None:
            return 0

        left = max(0, one_sided(node.left))
        right = max(0, one_sided(node.right))
        best = max(best, node.val + left + right)
        return node.val + max(left, right)

    one_sided(root)
    return best

Worked Tree 2: Serialize And Deserialize Binary Tree

Design functions to serialize a binary tree to a string and deserialize the string back to the same tree structure.

Hidden answer: invariant, tests, and Python solution

Preorder with null markers is enough because each token stream can be consumed from left to right. Test an empty tree, a one-node tree, a left-only chain, and duplicate values. Values alone are not enough without null structure markers.

class Codec:
    def serialize(self, root):
        tokens = []

        def walk(node):
            if node is None:
                tokens.append("#")
                return
            tokens.append(str(node.val))
            walk(node.left)
            walk(node.right)

        walk(root)
        return ",".join(tokens)

    def deserialize(self, data):
        tokens = iter(data.split(","))

        def build():
            token = next(tokens)
            if token == "#":
                return None
            node = TreeNode(int(token))
            node.left = build()
            node.right = build()
            return node

        return build()

Dynamic Programming

Choose The Table Meaning First

Worked DP 1: Decode Ways

Given a digit string, return how many ways it can be decoded where "1" maps to "A" and "26" maps to "Z".

Hidden answer: invariant, common mistake, and Python solution

Invariant: dp[i] is the number of decodings for the prefix ending before index i. A zero is valid only as part of 10 or 20. The common mistake is treating every two-digit number ending in zero as valid.

def num_decodings(s):
    if not s:
        return 0

    dp = [0] * (len(s) + 1)
    dp[0] = 1
    dp[1] = 0 if s[0] == "0" else 1

    for i in range(2, len(s) + 1):
        one = int(s[i - 1:i])
        two = int(s[i - 2:i])
        if 1 <= one <= 9:
            dp[i] += dp[i - 1]
        if 10 <= two <= 26:
            dp[i] += dp[i - 2]
    return dp[-1]

Worked DP 2: House Robber

Given non-negative house values in a line, return the maximum amount that can be robbed without robbing adjacent houses.

Hidden answer: invariant, tests, and Python solution

Invariant: after each house, prev1 is the best value through the current house and prev2 is the best value through the previous previous house. Test empty input, one house, two houses, all zeros, and a large value surrounded by small values.

def rob(nums):
    prev2 = 0
    prev1 = 0
    for value in nums:
        prev2, prev1 = prev1, max(prev1, prev2 + value)
    return prev1

Timed Drill

Prompts To Practice Out Loud

Prompt 1: Course Schedule Follow-Up

After solving Course Schedule, the interviewer asks for one valid course order and then asks how your answer changes if prerequisites arrive as a stream.

Hidden answer

Return the Kahn traversal order when all nodes are processed. For streaming prerequisites, clarify whether queries must be answered online. A simple batch answer rebuilds indegrees; an online answer needs incremental cycle detection or versioned graph snapshots, depending on latency and update frequency.

Prompt 2: DP Debugging

Your Word Break solution passes short examples but times out on a long string with many repeated prefixes. How do you debug and improve it?

Hidden answer

First confirm the state and count repeated substring checks. Use a word set, cap candidate lengths by the longest dictionary word, and avoid unnecessary slicing where possible. If using recursion, add memoization by start index and test with repeated characters plus a dictionary that nearly matches but fails at the end.