DFS — Worked Examples
Scope — The problem set for dfs.md: the DFS problems its templates solve, each as one problem card filed under the template it reduces to, plus pointers to the sheet that works a problem when it lives elsewhere. See also — parent sheet: dfs.md — the ten core templates and the pattern-selection flowchart, which is where the technique is explained; dfs_advanced.md — the rare/hard DFS templates and their examples. Neighbouring sheets: bst.md, tree_codec.md and tree_lca_distance.md own the tree problems pointed to here; bfs_examples.md — the breadth-first problem set; union_find.md — the alternative engine for the connectivity problems.
LeetCode Problem Lists
Overview
The DFS problems that are not a template’s own anchor problem, grouped by the dfs.md template they reduce to, in that sheet’s order. dfs.md holds the templates; this sheet is the problems. Each card states the one step the problem adds to its template. A problem worked in full on another sheet appears here as a pointer row, so every problem has exactly one home. A template with no problem of its own here (Template 9, Template 10) has no section.
Index
| LC | Problem | Reduces to | Difficulty |
|---|---|---|---|
| 655 | Print Binary Tree | Template 1: Tree Traversal | Medium |
| 695 | Max Area of Island | Template 2: Graph / Grid DFS | Medium |
| 529 | Minesweeper | Template 2: Graph / Grid DFS | Medium |
| 737 | Sentence Similarity II | Template 2: Graph / Grid DFS | Medium |
| 990 | Satisfiability of Equality Equations | Template 2: Graph / Grid DFS | Medium |
| 472 | Concatenated Words | Template 4: Backtracking | Hard |
| 663 | Equal Tree Partition | Template 6: Bottom-up DFS | Medium |
| 1339 | Maximum Product of Splitted Binary Tree | Template 6: Bottom-up DFS | Medium |
| 3319 | K-th Largest Perfect Subtree Size in Binary Tree | Template 6: Bottom-up DFS | Medium |
| 417 | Pacific Atlantic Water Flow | Template 7: 2-Pass DFS (Boundary) | Medium |
Tree Traversal Examples
Print Binary Tree — LC 655
Key point — the grid’s size depends only on the height, so compute the height first and the DFS only has to carry
(row, col)down. Each level down, the horizontal offset to a child halves:2^(height - row - 1), which is what keeps subtrees from overlapping.
Reduces to: dfs § Template 1 — a pre-order walk that passes the node’s position down instead of returning anything up.
# python
# LC 655 - Print Binary Tree
# IDEA: height first; then pre-order DFS places each node and halves the column offset per level
# time = O(h * 2^h), space = O(h * 2^h) the output grid is (h + 1) x (2^(h+1) - 1)
def printTree(root):
def height(node):
if not node:
return -1 # a lone leaf has height 0
return 1 + max(height(node.left), height(node.right))
h = height(root)
rows, cols = h + 1, 2 ** (h + 1) - 1
grid = [[""] * cols for _ in range(rows)]
def place(node, row, col):
if not node:
return
grid[row][col] = str(node.val)
if row == h:
return
offset = 2 ** (h - row - 1) # halves on every level down
place(node.left, row + 1, col - offset)
place(node.right, row + 1, col + offset)
place(root, 0, (cols - 1) // 2) # root in the middle of the top row
return grid
Traps
- Height must be 0 for a single node (null returns
-1). Returning0for null makes the grid one row too tall:[1]would give two rows instead of one.
Worked elsewhere
| LC | Problem | Home |
|---|---|---|
| 94 | Binary Tree Inorder Traversal | dfs § Template 1 (anchor) |
| 100 | Same Tree | binary_tree § Template 12 (anchor) |
| 98 | Validate Binary Search Tree | bst.md — validate with bounds |
| 700 | Search in a Binary Search Tree | bst.md |
| 297 | Serialize and Deserialize Binary Tree | tree_codec.md |
| 449 | Serialize and Deserialize BST | bst_examples.md |
| 662 | Maximum Width of Binary Tree | bfs_examples.md — heap index per level |
Graph / Grid DFS Examples
Max Area of Island — LC 695
Key point — the flood fill that counts islands in LC 200 also sizes them: let the sink return
1 + the sizes of its four neighbours, and the launch loop keeps the maximum instead of a count.
Reduces to: dfs § Template 2 — the same flood fill, returning a size instead of nothing.
# python
# LC 695 - Max Area of Island
# IDEA: each 1 still standing starts a sink that returns the island's size; keep the largest
# time = O(m * n), space = O(m * n) the recursion is as deep as the largest island
def maxAreaOfIsland(grid):
rows, cols = len(grid), len(grid[0])
def sink(r, c):
if r < 0 or r >= rows or c < 0 or c >= cols or grid[r][c] != 1:
return 0
grid[r][c] = 0 # sink on entry: never counted twice
return 1 + sink(r + 1, c) + sink(r - 1, c) + sink(r, c + 1) + sink(r, c - 1)
best = 0
for r in range(rows):
for c in range(cols):
if grid[r][c] == 1:
best = max(best, sink(r, c))
return best
// java
// LC 695 - Max Area of Island
// IDEA: flood fill that returns the size of the island it sinks; keep the largest
// time = O(m * n), space = O(m * n)
class Solution {
public int maxAreaOfIsland(int[][] grid) {
int best = 0;
for (int r = 0; r < grid.length; r++) {
for (int c = 0; c < grid[0].length; c++) {
best = Math.max(best, area(grid, r, c));
}
}
return best;
}
private int area(int[][] grid, int r, int c) {
if (r < 0 || r >= grid.length || c < 0 || c >= grid[0].length || grid[r][c] == 0) {
return 0;
}
grid[r][c] = 0; // sink it so it is counted once
return 1 + area(grid, r + 1, c) + area(grid, r - 1, c)
+ area(grid, r, c + 1) + area(grid, r, c - 1);
}
}
Traps
- Mark the cell before recursing. Marking after the four calls lets two neighbours count each other, and the recursion never ends on a 2×2 block of land.
Minesweeper — LC 529
Key point — the flood fill stops at a number. A revealed cell with an adjacent mine becomes its count and does not expand; only a cell with zero adjacent mines becomes
'B'and recurses into all eight neighbours. Recursing only into'E'cells is the visited check.
Reduces to: dfs § Template 2 — eight directions, and a cell decides from its neighbours whether the flood continues.
# python
# LC 529 - Minesweeper
# IDEA: a mine click is game over; otherwise flood from the click, stopping at cells next to a mine
# time = O(m * n), space = O(m * n) each cell is revealed once; recursion as deep as the blank region
import sys
sys.setrecursionlimit(10**6) # the default 1,000 is too low: a 50 x 50 blank board recurses 2,500 deep
def updateBoard(board, click):
rows, cols = len(board), len(board[0])
r, c = click
if board[r][c] == "M":
board[r][c] = "X"
return board
dirs = [(-1, -1), (-1, 0), (-1, 1), (0, -1), (0, 1), (1, -1), (1, 0), (1, 1)]
def reveal(r, c):
if r < 0 or r >= rows or c < 0 or c >= cols or board[r][c] != "E":
return # only an unrevealed empty cell expands
mines = 0
for dr, dc in dirs:
nr, nc = r + dr, c + dc
if 0 <= nr < rows and 0 <= nc < cols and board[nr][nc] == "M":
mines += 1
if mines > 0:
board[r][c] = str(mines) # a number stops the flood
return
board[r][c] = "B"
for dr, dc in dirs:
reveal(r + dr, c + dc)
reveal(r, c)
return board
Traps
- The guard is
board[r][c] != "E", not “is it a mine”.'B'and digit cells are already revealed; without the guard two blank cells recurse into each other forever.
Sentence Similarity II — LC 737
Key point — similarity is transitive, so two words are similar exactly when they are in the same connected component of the graph whose edges are the similar pairs. Each aligned word pair is one reachability query. (LC 734, which is not transitive, is a set lookup.)
Reduces to: dfs § Template 2 — on an explicit word graph, run once per query.
# python
# LC 737 - Sentence Similarity II
# IDEA: undirected graph of similar pairs; each differing word pair must be reachable
# time = O(n * (V + E)), space = O(V + E) one DFS per aligned pair, n = sentence length
from collections import defaultdict
def areSentencesSimilarTwo(sentence1, sentence2, similarPairs):
if len(sentence1) != len(sentence2):
return False
graph = defaultdict(set)
for a, b in similarPairs:
graph[a].add(b)
graph[b].add(a) # similarity is symmetric: both directions
def connected(src, dst):
stack, seen = [src], {src}
while stack:
word = stack.pop()
if word == dst:
return True
for nxt in graph[word]:
if nxt not in seen:
seen.add(nxt)
stack.append(nxt)
return False
for w1, w2 in zip(sentence1, sentence2):
if w1 != w2 and not connected(w1, w2):
return False
return True
Traps
- A word is similar to itself even when it appears in no pair: check
w1 != w2before searching. - With many queries, label components once (as in LC 990 below) or use union-find; a DFS per query repeats the same walk.
Satisfiability of Equality Equations — LC 990
Key point —
==is an equivalence: the==edges split the variables into components, and the equations are satisfiable unless some!=joins two variables of the same component. So label every component once, then each!=is an O(1) check. All==must be read before any!=.
Reduces to: dfs § Template 2 — flood fill that writes a component id instead of sinking cells.
# python
# LC 990 - Satisfiability of Equality Equations
# IDEA: DFS labels each == component; a != inside one component is a contradiction
# time = O(n), space = O(1) n = equations; at most 26 variables
from collections import defaultdict
def equationsPossible(equations):
graph = defaultdict(list)
for eq in equations:
if eq[1] == "=":
graph[eq[0]].append(eq[3])
graph[eq[3]].append(eq[0]) # == is symmetric: store both directions
group = {}
def label(var, gid):
group[var] = gid
for nxt in graph[var]:
if nxt not in group:
label(nxt, gid)
for var in list(graph):
if var not in group:
label(var, var) # one DFS per component
for eq in equations:
if eq[1] == "!":
a, b = eq[0], eq[3]
if a == b or (a in group and group.get(b) == group[a]):
return False
return True
Traps
a!=ais unsatisfiable on its own, andamay appear in no==. Testa == bfirst.- A one-directional graph breaks transitivity checks: from
a==b, b==c, a search fromcfinds no edge. Add both directions. - Union-find is the other common answer, same O(n): union every
==, then reject any!=whose two letters share a root. This card is its home;union_find_examples.mdpoints here.
Worked elsewhere
| LC | Problem | Home |
|---|---|---|
| 200 | Number of Islands | dfs § Template 2 (anchor) |
| 133 | Clone Graph | graph_examples.md |
Path Finding Examples
Every root-to-leaf path problem in this set is worked on another sheet.
| LC | Problem | Home |
|---|---|---|
| 112 | Path Sum | dfs § Template 3 (anchor) |
| 113 | Path Sum II | dfs § Template 3 — collect every path |
| 129 | Sum Root to Leaf Numbers | tree_lca_distance.md — root-to-leaf paths |
| 257 | Binary Tree Paths | tree_lca_distance.md — root-to-leaf paths |
| 1448 | Count Good Nodes in Binary Tree | tree_examples.md |
Backtracking Examples
Concatenated Words — LC 472
Key point — a word qualifies when it splits into two or more dictionary words. Try every dictionary prefix and recurse on the rest; memoising “can this suffix be split” turns the exponential search into word-break DP, because the same suffix is reached by many cuts.
Reduces to: dfs § Template 4 — choose a prefix, recurse on the remainder; plus a memo, because the choices overlap.
# python
# LC 472 - Concatenated Words
# IDEA: a word counts if a proper dictionary prefix leaves a suffix that splits into dictionary words
# time = O(n * L^3), space = O(n * L) n words of length L: L suffixes, L cuts each, O(L) slicing
def findAllConcatenatedWordsInADict(words):
word_set = set(words)
memo = {}
def splits(word):
# can `word` be cut into one or more dictionary words?
if word in memo:
return memo[word]
for i in range(1, len(word) + 1):
prefix = word[:i]
if prefix in word_set and (i == len(word) or splits(word[i:])):
memo[word] = True
return True
memo[word] = False
return False
result = []
for word in words:
if not word:
continue
# two or more pieces: the first piece is a PROPER prefix
for i in range(1, len(word)):
if word[:i] in word_set and splits(word[i:]):
result.append(word)
break
return result
Traps
- A word is in its own dictionary. Requiring a proper first prefix (
i < len(word)) is what stops every word from matching itself. - Without the memo,
"aaaa…ab"against["a", "aa", "aaa"]explores every composition: exponential.
Tree Modification Examples
Every tree-modification problem in this set is worked on another sheet.
| LC | Problem | Home |
|---|---|---|
| 450 | Delete Node in a BST | dfs § Template 5 (anchor) |
| 701 | Insert into a Binary Search Tree | bst.md |
| 669 | Trim a Binary Search Tree | bst.md |
| 538 | Convert BST to Greater Tree | bst_examples.md — reverse in-order |
| 776 | Split BST | bst_examples.md |
| 623 | Add One Row to Tree | bfs_examples.md — level BFS, with the DFS countdown beside it |
Bottom-up (Post-Order) DFS Examples
Equal Tree Partition — LC 663
Key point — removing one edge leaves the subtree below it and the rest. Collect every subtree sum in one post-order pass; the tree splits evenly exactly when some subtree other than the whole tree sums to
total / 2.
Reduces to: dfs § Template 6 — each node returns its subtree sum upward and records it.
# python
# LC 663 - Equal Tree Partition
# IDEA: post-order collects every subtree sum; look for total / 2 among the proper subtrees
# time = O(n), space = O(n)
def checkEqualTree(root):
sums = []
def subtree_sum(node):
if not node:
return 0
s = node.val + subtree_sum(node.left) + subtree_sum(node.right)
sums.append(s)
return s
total = subtree_sum(root)
sums.pop() # the whole tree is the last sum: not a cut
return total % 2 == 0 and total // 2 in sums
Traps
- Drop the root’s own sum before searching. For
[0, -1, 1]the total is 0 and the whole tree sums to 0, but no edge removal gives two halves of 0. - An odd total can never split; check it before
//.
Maximum Product of Splitted Binary Tree — LC 1339
Key point — the same subtree-sum pass as LC 663: cutting above a subtree with sum
sgivess * (total - s). One pass for everys, then the best product.
Reduces to: dfs § Template 6 — subtree sums returned upward, then scored against the total.
# python
# LC 1339 - Maximum Product of Splitted Binary Tree
# IDEA: post-order records every subtree sum s; each is one cut worth s * (total - s)
# time = O(n), space = O(n)
def maxProduct(root):
sums = []
def subtree_sum(node):
if not node:
return 0
s = node.val + subtree_sum(node.left) + subtree_sum(node.right)
sums.append(s)
return s
total = subtree_sum(root)
best = 0
for s in sums:
best = max(best, s * (total - s))
return best % (10 ** 9 + 7) # take the max BEFORE the modulo
Traps
- Apply the modulo once, to the maximum. Comparing products already reduced mod 10⁹+7 picks the wrong cut.
K-th Largest Perfect Subtree Size in Binary Tree — LC 3319
Key point — a subtree is perfect exactly when both children are perfect and have the same height. Return
(height, is_perfect)upward; a perfect subtree of heighthhas2^h − 1nodes, so its size needs no separate count.
Reduces to: dfs § Template 6 — the node decides from its children’s returned pair.
# python
# LC 3319 - K-th Largest Perfect Subtree Size in Binary Tree
# IDEA: post-order returns (height, is_perfect); record 2^height - 1 for every perfect subtree
# time = O(n log n), space = O(n) the sort of the recorded sizes dominates
import sys
sys.setrecursionlimit(10**6) # the default 1,000 is too low: a 2,000-node chain recurses 2,000 deep
def kthLargestPerfectSubtree(root, k):
sizes = []
def dfs(node):
if not node:
return 0, True
left_h, left_ok = dfs(node.left)
right_h, right_ok = dfs(node.right)
if left_ok and right_ok and left_h == right_h:
sizes.append(2 ** (left_h + 1) - 1)
return left_h + 1, True
return max(left_h, right_h) + 1, False
dfs(root)
if len(sizes) < k:
return -1
sizes.sort(reverse=True)
return sizes[k - 1]
Traps
- “Both children perfect” is not enough: a node whose left child is a leaf and whose right child is a perfect subtree of height 2 is not perfect. The heights must match.
Worked elsewhere
| LC | Problem | Home |
|---|---|---|
| 543 | Diameter of Binary Tree | dfs § Template 6 (anchor) |
| 236 | Lowest Common Ancestor of a Binary Tree | tree_lca_distance.md |
| 508 | Most Frequent Subtree Sum | hashing.md — subtree sums counted in a hash map |
2-Pass DFS (Boundary Elimination) Examples
Pacific Atlantic Water Flow — LC 417
Key point — asking “can water from this cell reach the ocean?” for every cell is O((mn)²). Reverse it: start at each ocean’s border and climb uphill (
next >= current). The cells one flood reaches can drain to that ocean; the answer is the cells both floods reach.
Reduces to: dfs § Template 7 — the flood starts from the border, but it is run twice (one per ocean) and the results are intersected rather than subtracted.
# python
# LC 417 - Pacific Atlantic Water Flow
# IDEA: flood uphill from each ocean's border; answer = cells reached by both floods
# time = O(m * n), space = O(m * n) each flood visits a cell at most once
import sys
sys.setrecursionlimit(10**6) # the default 1,000 is too low: a flat 200 x 200 grid recurses 40,000 deep
def pacificAtlantic(heights):
rows, cols = len(heights), len(heights[0])
pacific, atlantic = set(), set()
def climb(r, c, reached):
reached.add((r, c))
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 reached
and heights[nr][nc] >= heights[r][c]): # uphill: its water flows down to us
climb(nr, nc, reached)
for r in range(rows):
climb(r, 0, pacific) # left edge
climb(r, cols - 1, atlantic) # right edge
for c in range(cols):
climb(0, c, pacific) # top edge
climb(rows - 1, c, atlantic) # bottom edge
result = []
for r in range(rows):
for c in range(cols):
if (r, c) in pacific and (r, c) in atlantic:
result.append([r, c])
return result
Traps
- The comparison is reversed: the flood moves to a neighbour that is higher or equal, because
water would flow from it down to the current cell. Using
<finds where water flows to. - The reached set doubles as the visited set; without it, two equal-height cells recurse forever.
Worked elsewhere
| LC | Problem | Home |
|---|---|---|
| 1254 | Number of Closed Islands | dfs § Template 7 (anchor) |
Path Signature Examples
| LC | Problem | Home |
|---|---|---|
| 694 | Number of Distinct Islands | dfs § Template 8 (anchor) |
| 652 | Find Duplicate Subtrees | tree_codec.md — post-order subtree signature + hash map count |