Trees and Graphs Problem Set
Trees and graphs are both “follow the edges” problems — the difference is just how strict the shape is. This set starts with plain recursive tree traversals, moves into grid-as-graph problems, and finishes with two classic BFS-on-an-implicit-graph problems: Course Schedule and Word Ladder.
What you’ll learn
- The recursive DFS on a tree template: handle the base case (
NoneNone), then combine the answers fromnode.leftnode.leftandnode.rightnode.right. - BFS level order using a queue, one level at a time.
- Treating a 2D grid as an implicit graph for flood-fill counting (Number of Islands).
- Detecting a cycle in a directed graph (Course Schedule) and running BFS over an implicit word graph (Word Ladder).
How to use this set
Same loop as the other problem sets: read the problem, open the stub,
find the # TODO# TODO, press Run, and match the printed output to the
# expect# expect comments. Each stub includes small helper functions (build_treebuild_tree,
build_graphbuild_graph, and similar) so the block runs standalone — you only need
to fill in the one function under test. A collapsed Show solution
follows each problem with a complete, annotated answer and its complexity.
Problems
1. Maximum Depth of Binary Tree — LC 104 — Easy
Pattern: DFS (post-order combine) — see Depth First Search.
Problem. Given the root of a binary tree, return its maximum depth — the number of nodes along the longest path from the root down to the farthest leaf.
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
def build_tree(values):
if not values or values[0] is None:
return None
root = TreeNode(values[0])
queue = [root]
i = 1
while queue and i < len(values):
node = queue.pop(0)
if i < len(values):
if values[i] is not None:
node.left = TreeNode(values[i])
queue.append(node.left)
i += 1
if i < len(values):
if values[i] is not None:
node.right = TreeNode(values[i])
queue.append(node.right)
i += 1
return root
def max_depth(root):
# TODO: return the number of nodes on the longest root-to-leaf path
pass
# Sample tests (press Run):
print(max_depth(build_tree([3, 9, 20, None, None, 15, 7]))) # expect 3
print(max_depth(build_tree([]))) # expect 0class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
def build_tree(values):
if not values or values[0] is None:
return None
root = TreeNode(values[0])
queue = [root]
i = 1
while queue and i < len(values):
node = queue.pop(0)
if i < len(values):
if values[i] is not None:
node.left = TreeNode(values[i])
queue.append(node.left)
i += 1
if i < len(values):
if values[i] is not None:
node.right = TreeNode(values[i])
queue.append(node.right)
i += 1
return root
def max_depth(root):
# TODO: return the number of nodes on the longest root-to-leaf path
pass
# Sample tests (press Run):
print(max_depth(build_tree([3, 9, 20, None, None, 15, 7]))) # expect 3
print(max_depth(build_tree([]))) # expect 0Show solution
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
def build_tree(values):
if not values or values[0] is None:
return None
root = TreeNode(values[0])
queue = [root]
i = 1
while queue and i < len(values):
node = queue.pop(0)
if i < len(values):
if values[i] is not None:
node.left = TreeNode(values[i])
queue.append(node.left)
i += 1
if i < len(values):
if values[i] is not None:
node.right = TreeNode(values[i])
queue.append(node.right)
i += 1
return root
def max_depth(root):
if root is None:
return 0
return 1 + max(max_depth(root.left), max_depth(root.right))
print(max_depth(build_tree([3, 9, 20, None, None, 15, 7]))) # 3
print(max_depth(build_tree([]))) # 0class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
def build_tree(values):
if not values or values[0] is None:
return None
root = TreeNode(values[0])
queue = [root]
i = 1
while queue and i < len(values):
node = queue.pop(0)
if i < len(values):
if values[i] is not None:
node.left = TreeNode(values[i])
queue.append(node.left)
i += 1
if i < len(values):
if values[i] is not None:
node.right = TreeNode(values[i])
queue.append(node.right)
i += 1
return root
def max_depth(root):
if root is None:
return 0
return 1 + max(max_depth(root.left), max_depth(root.right))
print(max_depth(build_tree([3, 9, 20, None, None, 15, 7]))) # 3
print(max_depth(build_tree([]))) # 0A NoneNone node has depth 0; every other node’s depth is 1 plus the deeper
of its two children’s depths. Time: — every node is visited
once. Space: for the recursion stack, where hh is the tree’s
height.
2. Invert Binary Tree — LC 226 — Easy
Pattern: DFS (post-order swap) — see Depth First Search.
Problem. Given the root of a binary tree, invert it (mirror every subtree, left becomes right) and return the new root.
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
def build_tree(values):
if not values or values[0] is None:
return None
root = TreeNode(values[0])
queue = [root]
i = 1
while queue and i < len(values):
node = queue.pop(0)
if i < len(values):
if values[i] is not None:
node.left = TreeNode(values[i])
queue.append(node.left)
i += 1
if i < len(values):
if values[i] is not None:
node.right = TreeNode(values[i])
queue.append(node.right)
i += 1
return root
def to_level_list(root):
if root is None:
return []
result = []
queue = [root]
while queue:
node = queue.pop(0)
if node is None:
result.append(None)
continue
result.append(node.val)
queue.append(node.left)
queue.append(node.right)
while result and result[-1] is None:
result.pop()
return result
def invert_tree(root):
# TODO: swap every node's left and right children, return the new root
pass
# Sample tests (press Run):
print(to_level_list(invert_tree(build_tree([4, 2, 7, 1, 3, 6, 9]))))
# expect [4, 7, 2, 9, 6, 3, 1]class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
def build_tree(values):
if not values or values[0] is None:
return None
root = TreeNode(values[0])
queue = [root]
i = 1
while queue and i < len(values):
node = queue.pop(0)
if i < len(values):
if values[i] is not None:
node.left = TreeNode(values[i])
queue.append(node.left)
i += 1
if i < len(values):
if values[i] is not None:
node.right = TreeNode(values[i])
queue.append(node.right)
i += 1
return root
def to_level_list(root):
if root is None:
return []
result = []
queue = [root]
while queue:
node = queue.pop(0)
if node is None:
result.append(None)
continue
result.append(node.val)
queue.append(node.left)
queue.append(node.right)
while result and result[-1] is None:
result.pop()
return result
def invert_tree(root):
# TODO: swap every node's left and right children, return the new root
pass
# Sample tests (press Run):
print(to_level_list(invert_tree(build_tree([4, 2, 7, 1, 3, 6, 9]))))
# expect [4, 7, 2, 9, 6, 3, 1]Show solution
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
def build_tree(values):
if not values or values[0] is None:
return None
root = TreeNode(values[0])
queue = [root]
i = 1
while queue and i < len(values):
node = queue.pop(0)
if i < len(values):
if values[i] is not None:
node.left = TreeNode(values[i])
queue.append(node.left)
i += 1
if i < len(values):
if values[i] is not None:
node.right = TreeNode(values[i])
queue.append(node.right)
i += 1
return root
def to_level_list(root):
if root is None:
return []
result = []
queue = [root]
while queue:
node = queue.pop(0)
if node is None:
result.append(None)
continue
result.append(node.val)
queue.append(node.left)
queue.append(node.right)
while result and result[-1] is None:
result.pop()
return result
def invert_tree(root):
if root is None:
return None
root.left, root.right = invert_tree(root.right), invert_tree(root.left)
return root
print(to_level_list(invert_tree(build_tree([4, 2, 7, 1, 3, 6, 9]))))
# [4, 7, 2, 9, 6, 3, 1]class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
def build_tree(values):
if not values or values[0] is None:
return None
root = TreeNode(values[0])
queue = [root]
i = 1
while queue and i < len(values):
node = queue.pop(0)
if i < len(values):
if values[i] is not None:
node.left = TreeNode(values[i])
queue.append(node.left)
i += 1
if i < len(values):
if values[i] is not None:
node.right = TreeNode(values[i])
queue.append(node.right)
i += 1
return root
def to_level_list(root):
if root is None:
return []
result = []
queue = [root]
while queue:
node = queue.pop(0)
if node is None:
result.append(None)
continue
result.append(node.val)
queue.append(node.left)
queue.append(node.right)
while result and result[-1] is None:
result.pop()
return result
def invert_tree(root):
if root is None:
return None
root.left, root.right = invert_tree(root.right), invert_tree(root.left)
return root
print(to_level_list(invert_tree(build_tree([4, 2, 7, 1, 3, 6, 9]))))
# [4, 7, 2, 9, 6, 3, 1]Invert both children first, then swap them onto the current node — the order doesn’t actually matter here since the two subtrees don’t interact, but swapping after recursing keeps the logic easy to read. Time: . Space: for the recursion stack.
3. Validate Binary Search Tree — LC 98 — Medium
Pattern: DFS with propagated bounds — see Depth First Search.
Problem. Given the root of a binary tree, determine if it is a valid binary search tree: every node’s value must be strictly greater than all values in its left subtree and strictly less than all values in its right subtree.
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
def build_tree(values):
if not values or values[0] is None:
return None
root = TreeNode(values[0])
queue = [root]
i = 1
while queue and i < len(values):
node = queue.pop(0)
if i < len(values):
if values[i] is not None:
node.left = TreeNode(values[i])
queue.append(node.left)
i += 1
if i < len(values):
if values[i] is not None:
node.right = TreeNode(values[i])
queue.append(node.right)
i += 1
return root
def is_valid_bst(root):
# TODO: return True only if every node's value fits its BST bounds
pass
# Sample tests (press Run):
print(is_valid_bst(build_tree([2, 1, 3]))) # expect True
print(is_valid_bst(build_tree([5, 1, 4, None, None, 3, 6]))) # expect Falseclass TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
def build_tree(values):
if not values or values[0] is None:
return None
root = TreeNode(values[0])
queue = [root]
i = 1
while queue and i < len(values):
node = queue.pop(0)
if i < len(values):
if values[i] is not None:
node.left = TreeNode(values[i])
queue.append(node.left)
i += 1
if i < len(values):
if values[i] is not None:
node.right = TreeNode(values[i])
queue.append(node.right)
i += 1
return root
def is_valid_bst(root):
# TODO: return True only if every node's value fits its BST bounds
pass
# Sample tests (press Run):
print(is_valid_bst(build_tree([2, 1, 3]))) # expect True
print(is_valid_bst(build_tree([5, 1, 4, None, None, 3, 6]))) # expect FalseShow solution
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
def build_tree(values):
if not values or values[0] is None:
return None
root = TreeNode(values[0])
queue = [root]
i = 1
while queue and i < len(values):
node = queue.pop(0)
if i < len(values):
if values[i] is not None:
node.left = TreeNode(values[i])
queue.append(node.left)
i += 1
if i < len(values):
if values[i] is not None:
node.right = TreeNode(values[i])
queue.append(node.right)
i += 1
return root
def is_valid_bst(root):
def validate(node, low, high):
if node is None:
return True
if not (low < node.val < high):
return False
return validate(node.left, low, node.val) and validate(node.right, node.val, high)
return validate(root, float("-inf"), float("inf"))
print(is_valid_bst(build_tree([2, 1, 3]))) # True
print(is_valid_bst(build_tree([5, 1, 4, None, None, 3, 6]))) # Falseclass TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
def build_tree(values):
if not values or values[0] is None:
return None
root = TreeNode(values[0])
queue = [root]
i = 1
while queue and i < len(values):
node = queue.pop(0)
if i < len(values):
if values[i] is not None:
node.left = TreeNode(values[i])
queue.append(node.left)
i += 1
if i < len(values):
if values[i] is not None:
node.right = TreeNode(values[i])
queue.append(node.right)
i += 1
return root
def is_valid_bst(root):
def validate(node, low, high):
if node is None:
return True
if not (low < node.val < high):
return False
return validate(node.left, low, node.val) and validate(node.right, node.val, high)
return validate(root, float("-inf"), float("inf"))
print(is_valid_bst(build_tree([2, 1, 3]))) # True
print(is_valid_bst(build_tree([5, 1, 4, None, None, 3, 6]))) # FalseChecking node.left.val < node.valnode.left.val < node.val and node.right.val > node.valnode.right.val > node.val at
every node isn’t enough — a node deep in the left subtree still has to be
less than every ancestor above it, not just its immediate parent. Carrying
a (low, high)(low, high) bound down through the recursion enforces that. Time:
. Space: .
4. Binary Tree Level Order Traversal — LC 102 — Medium
Pattern: BFS, one level at a time — see Breadth First Search.
Problem. Given the root of a binary tree, return the level order traversal of its node values as a list of lists — one inner list per level, left to right.
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
def build_tree(values):
if not values or values[0] is None:
return None
root = TreeNode(values[0])
queue = [root]
i = 1
while queue and i < len(values):
node = queue.pop(0)
if i < len(values):
if values[i] is not None:
node.left = TreeNode(values[i])
queue.append(node.left)
i += 1
if i < len(values):
if values[i] is not None:
node.right = TreeNode(values[i])
queue.append(node.right)
i += 1
return root
def level_order(root):
# TODO: return a list of lists, one inner list per level, left to right
pass
# Sample tests (press Run):
print(level_order(build_tree([3, 9, 20, None, None, 15, 7])))
# expect [[3], [9, 20], [15, 7]]class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
def build_tree(values):
if not values or values[0] is None:
return None
root = TreeNode(values[0])
queue = [root]
i = 1
while queue and i < len(values):
node = queue.pop(0)
if i < len(values):
if values[i] is not None:
node.left = TreeNode(values[i])
queue.append(node.left)
i += 1
if i < len(values):
if values[i] is not None:
node.right = TreeNode(values[i])
queue.append(node.right)
i += 1
return root
def level_order(root):
# TODO: return a list of lists, one inner list per level, left to right
pass
# Sample tests (press Run):
print(level_order(build_tree([3, 9, 20, None, None, 15, 7])))
# expect [[3], [9, 20], [15, 7]]Show solution
from collections import deque
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
def build_tree(values):
if not values or values[0] is None:
return None
root = TreeNode(values[0])
queue = [root]
i = 1
while queue and i < len(values):
node = queue.pop(0)
if i < len(values):
if values[i] is not None:
node.left = TreeNode(values[i])
queue.append(node.left)
i += 1
if i < len(values):
if values[i] is not None:
node.right = TreeNode(values[i])
queue.append(node.right)
i += 1
return root
def level_order(root):
if root is None:
return []
result = []
queue = deque([root])
while queue:
level = []
for _ in range(len(queue)): # exactly the nodes at this level
node = queue.popleft()
level.append(node.val)
if node.left:
queue.append(node.left)
if node.right:
queue.append(node.right)
result.append(level)
return result
print(level_order(build_tree([3, 9, 20, None, None, 15, 7])))
# [[3], [9, 20], [15, 7]]from collections import deque
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
def build_tree(values):
if not values or values[0] is None:
return None
root = TreeNode(values[0])
queue = [root]
i = 1
while queue and i < len(values):
node = queue.pop(0)
if i < len(values):
if values[i] is not None:
node.left = TreeNode(values[i])
queue.append(node.left)
i += 1
if i < len(values):
if values[i] is not None:
node.right = TreeNode(values[i])
queue.append(node.right)
i += 1
return root
def level_order(root):
if root is None:
return []
result = []
queue = deque([root])
while queue:
level = []
for _ in range(len(queue)): # exactly the nodes at this level
node = queue.popleft()
level.append(node.val)
if node.left:
queue.append(node.left)
if node.right:
queue.append(node.right)
result.append(level)
return result
print(level_order(build_tree([3, 9, 20, None, None, 15, 7])))
# [[3], [9, 20], [15, 7]]Snapshot len(queue)len(queue) before the inner loop — that’s exactly how many
nodes belong to the current level, even though the loop body keeps
appending their children onto the same queue. Time: .
Space: for the queue and the output.
5. Number of Islands — LC 200 — Medium
Pattern: DFS flood fill on a grid — see Depth First Search.
Problem. Given an m x nm x n 2D binary grid where "1""1" is land and
"0""0" is water, return the number of islands. An island is surrounded by
water and formed by connecting adjacent lands horizontally or vertically.
def num_islands(grid):
# TODO: return the number of islands (connected groups of '1's)
pass
# Sample tests (press Run):
grid1 = [
["1", "1", "1", "1", "0"],
["1", "1", "0", "1", "0"],
["1", "1", "0", "0", "0"],
["0", "0", "0", "0", "0"],
]
print(num_islands(grid1)) # expect 1
grid2 = [
["1", "1", "0", "0", "0"],
["1", "1", "0", "0", "0"],
["0", "0", "1", "0", "0"],
["0", "0", "0", "1", "1"],
]
print(num_islands(grid2)) # expect 3def num_islands(grid):
# TODO: return the number of islands (connected groups of '1's)
pass
# Sample tests (press Run):
grid1 = [
["1", "1", "1", "1", "0"],
["1", "1", "0", "1", "0"],
["1", "1", "0", "0", "0"],
["0", "0", "0", "0", "0"],
]
print(num_islands(grid1)) # expect 1
grid2 = [
["1", "1", "0", "0", "0"],
["1", "1", "0", "0", "0"],
["0", "0", "1", "0", "0"],
["0", "0", "0", "1", "1"],
]
print(num_islands(grid2)) # expect 3Show solution
def num_islands(grid):
if not grid:
return 0
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
grid[r][c] = "0" # mark visited by "draining" it
sink(r + 1, c)
sink(r - 1, c)
sink(r, c + 1)
sink(r, c - 1)
islands = 0
for r in range(rows):
for c in range(cols):
if grid[r][c] == "1":
islands += 1
sink(r, c) # flood-fill the whole island so it's not recounted
return islands
grid1 = [
["1", "1", "1", "1", "0"],
["1", "1", "0", "1", "0"],
["1", "1", "0", "0", "0"],
["0", "0", "0", "0", "0"],
]
print(num_islands(grid1)) # 1
grid2 = [
["1", "1", "0", "0", "0"],
["1", "1", "0", "0", "0"],
["0", "0", "1", "0", "0"],
["0", "0", "0", "1", "1"],
]
print(num_islands(grid2)) # 3def num_islands(grid):
if not grid:
return 0
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
grid[r][c] = "0" # mark visited by "draining" it
sink(r + 1, c)
sink(r - 1, c)
sink(r, c + 1)
sink(r, c - 1)
islands = 0
for r in range(rows):
for c in range(cols):
if grid[r][c] == "1":
islands += 1
sink(r, c) # flood-fill the whole island so it's not recounted
return islands
grid1 = [
["1", "1", "1", "1", "0"],
["1", "1", "0", "1", "0"],
["1", "1", "0", "0", "0"],
["0", "0", "0", "0", "0"],
]
print(num_islands(grid1)) # 1
grid2 = [
["1", "1", "0", "0", "0"],
["1", "1", "0", "0", "0"],
["0", "0", "1", "0", "0"],
["0", "0", "0", "1", "1"],
]
print(num_islands(grid2)) # 3Every time you find an unvisited "1""1", that’s a brand new island — flood
fill (DFS) turns every connected "1""1" into a "0""0" so it’s never counted
twice. Time: . Space:
worst case for the recursion stack.
6. Course Schedule — LC 207 — Medium
Pattern: Cycle detection via DFS — see Depth First Search.
Problem. There are num_coursesnum_courses courses labeled 00 to
num_courses - 1num_courses - 1. prerequisites[i] = [a, b]prerequisites[i] = [a, b] means you must take course
bb before course aa. Return TrueTrue if you can finish all courses (i.e.
the prerequisite graph has no cycle).
def can_finish(num_courses, prerequisites):
# TODO: return True if all courses can be finished (no cycle in the graph)
pass
# Sample tests (press Run):
print(can_finish(2, [[1, 0]])) # expect True
print(can_finish(2, [[1, 0], [0, 1]])) # expect Falsedef can_finish(num_courses, prerequisites):
# TODO: return True if all courses can be finished (no cycle in the graph)
pass
# Sample tests (press Run):
print(can_finish(2, [[1, 0]])) # expect True
print(can_finish(2, [[1, 0], [0, 1]])) # expect FalseShow solution
def can_finish(num_courses, prerequisites):
graph = {i: [] for i in range(num_courses)}
for course, prereq in prerequisites:
graph[course].append(prereq)
state = [0] * num_courses # 0 = unvisited, 1 = visiting, 2 = done
def has_cycle(node):
if state[node] == 1:
return True # back edge into a node still being visited -> cycle
if state[node] == 2:
return False # already fully explored, safe
state[node] = 1
for neighbor in graph[node]:
if has_cycle(neighbor):
return True
state[node] = 2
return False
return not any(has_cycle(i) for i in range(num_courses))
print(can_finish(2, [[1, 0]])) # True
print(can_finish(2, [[1, 0], [0, 1]])) # Falsedef can_finish(num_courses, prerequisites):
graph = {i: [] for i in range(num_courses)}
for course, prereq in prerequisites:
graph[course].append(prereq)
state = [0] * num_courses # 0 = unvisited, 1 = visiting, 2 = done
def has_cycle(node):
if state[node] == 1:
return True # back edge into a node still being visited -> cycle
if state[node] == 2:
return False # already fully explored, safe
state[node] = 1
for neighbor in graph[node]:
if has_cycle(neighbor):
return True
state[node] = 2
return False
return not any(has_cycle(i) for i in range(num_courses))
print(can_finish(2, [[1, 0]])) # True
print(can_finish(2, [[1, 0], [0, 1]])) # FalseThree-color DFS: a node marked “visiting” that you reach again before it’s marked “done” means there’s a cycle — and a cyclic prerequisite graph can never be fully completed. Time: . Space: .
7. Clone Graph — LC 133 — Medium
Pattern: DFS + Hash Map (visited-node memo) — see Depth First Search.
Problem. Given a reference node in a connected undirected graph
(every NodeNode has an int valint val and a list of neighborsneighbors), return a deep
copy (clone) of the graph.
class Node:
def __init__(self, val=0, neighbors=None):
self.val = val
self.neighbors = neighbors if neighbors is not None else []
def build_graph(adj_list):
"""adj_list[i] = 1-indexed neighbor values of node i + 1."""
nodes = {i + 1: Node(i + 1) for i in range(len(adj_list))}
for i, neighbors in enumerate(adj_list):
nodes[i + 1].neighbors = [nodes[n] for n in neighbors]
return nodes[1] if nodes else None
def to_adj_list(node):
if node is None:
return []
visited = {}
seen_ids = {id(node)}
queue = [node]
while queue:
current = queue.pop(0)
visited[current.val] = sorted(n.val for n in current.neighbors)
for neighbor in current.neighbors:
if id(neighbor) not in seen_ids:
seen_ids.add(id(neighbor))
queue.append(neighbor)
return [visited[v] for v in sorted(visited)]
def clone_graph(node):
# TODO: return a deep copy of the graph reachable from node
pass
# Sample tests (press Run):
graph = build_graph([[2, 4], [1, 3], [2, 4], [1, 3]])
cloned = clone_graph(graph)
print(to_adj_list(cloned)) # expect [[2, 4], [1, 3], [2, 4], [1, 3]]
print(cloned is not graph) # expect True (a real copy, not the same object)class Node:
def __init__(self, val=0, neighbors=None):
self.val = val
self.neighbors = neighbors if neighbors is not None else []
def build_graph(adj_list):
"""adj_list[i] = 1-indexed neighbor values of node i + 1."""
nodes = {i + 1: Node(i + 1) for i in range(len(adj_list))}
for i, neighbors in enumerate(adj_list):
nodes[i + 1].neighbors = [nodes[n] for n in neighbors]
return nodes[1] if nodes else None
def to_adj_list(node):
if node is None:
return []
visited = {}
seen_ids = {id(node)}
queue = [node]
while queue:
current = queue.pop(0)
visited[current.val] = sorted(n.val for n in current.neighbors)
for neighbor in current.neighbors:
if id(neighbor) not in seen_ids:
seen_ids.add(id(neighbor))
queue.append(neighbor)
return [visited[v] for v in sorted(visited)]
def clone_graph(node):
# TODO: return a deep copy of the graph reachable from node
pass
# Sample tests (press Run):
graph = build_graph([[2, 4], [1, 3], [2, 4], [1, 3]])
cloned = clone_graph(graph)
print(to_adj_list(cloned)) # expect [[2, 4], [1, 3], [2, 4], [1, 3]]
print(cloned is not graph) # expect True (a real copy, not the same object)Show solution
class Node:
def __init__(self, val=0, neighbors=None):
self.val = val
self.neighbors = neighbors if neighbors is not None else []
def build_graph(adj_list):
nodes = {i + 1: Node(i + 1) for i in range(len(adj_list))}
for i, neighbors in enumerate(adj_list):
nodes[i + 1].neighbors = [nodes[n] for n in neighbors]
return nodes[1] if nodes else None
def to_adj_list(node):
if node is None:
return []
visited = {}
seen_ids = {id(node)}
queue = [node]
while queue:
current = queue.pop(0)
visited[current.val] = sorted(n.val for n in current.neighbors)
for neighbor in current.neighbors:
if id(neighbor) not in seen_ids:
seen_ids.add(id(neighbor))
queue.append(neighbor)
return [visited[v] for v in sorted(visited)]
def clone_graph(node):
if node is None:
return None
visited = {} # original node -> its clone
def dfs(n):
if n in visited:
return visited[n]
copy = Node(n.val)
visited[n] = copy
for neighbor in n.neighbors:
copy.neighbors.append(dfs(neighbor))
return copy
return dfs(node)
graph = build_graph([[2, 4], [1, 3], [2, 4], [1, 3]])
cloned = clone_graph(graph)
print(to_adj_list(cloned)) # [[2, 4], [1, 3], [2, 4], [1, 3]]
print(cloned is not graph) # Trueclass Node:
def __init__(self, val=0, neighbors=None):
self.val = val
self.neighbors = neighbors if neighbors is not None else []
def build_graph(adj_list):
nodes = {i + 1: Node(i + 1) for i in range(len(adj_list))}
for i, neighbors in enumerate(adj_list):
nodes[i + 1].neighbors = [nodes[n] for n in neighbors]
return nodes[1] if nodes else None
def to_adj_list(node):
if node is None:
return []
visited = {}
seen_ids = {id(node)}
queue = [node]
while queue:
current = queue.pop(0)
visited[current.val] = sorted(n.val for n in current.neighbors)
for neighbor in current.neighbors:
if id(neighbor) not in seen_ids:
seen_ids.add(id(neighbor))
queue.append(neighbor)
return [visited[v] for v in sorted(visited)]
def clone_graph(node):
if node is None:
return None
visited = {} # original node -> its clone
def dfs(n):
if n in visited:
return visited[n]
copy = Node(n.val)
visited[n] = copy
for neighbor in n.neighbors:
copy.neighbors.append(dfs(neighbor))
return copy
return dfs(node)
graph = build_graph([[2, 4], [1, 3], [2, 4], [1, 3]])
cloned = clone_graph(graph)
print(to_adj_list(cloned)) # [[2, 4], [1, 3], [2, 4], [1, 3]]
print(cloned is not graph) # TrueThe visitedvisited map does double duty: it prevents infinite recursion around
cycles, and it makes sure two edges pointing at the same original node end
up pointing at the same clone. Time: . Space: .
8. Word Ladder — LC 127 — Hard
Pattern: BFS shortest path over an implicit graph — see Breadth First Search.
Problem. Given begin_wordbegin_word, end_wordend_word, and a word_listword_list, return the
length of the shortest transformation sequence from begin_wordbegin_word to
end_wordend_word, changing exactly one letter at a time, where every
intermediate word must exist in word_listword_list. Return 00 if no such
sequence exists.
def ladder_length(begin_word, end_word, word_list):
# TODO: return the length of the shortest transformation sequence, or 0
pass
# Sample tests (press Run):
print(ladder_length("hit", "cog", ["hot", "dot", "dog", "lot", "log", "cog"]))
# expect 5
print(ladder_length("hit", "cog", ["hot", "dot", "dog", "lot", "log"]))
# expect 0def ladder_length(begin_word, end_word, word_list):
# TODO: return the length of the shortest transformation sequence, or 0
pass
# Sample tests (press Run):
print(ladder_length("hit", "cog", ["hot", "dot", "dog", "lot", "log", "cog"]))
# expect 5
print(ladder_length("hit", "cog", ["hot", "dot", "dog", "lot", "log"]))
# expect 0Show solution
from collections import deque
import string
def ladder_length(begin_word, end_word, word_list):
words = set(word_list)
if end_word not in words:
return 0
queue = deque([(begin_word, 1)])
visited = {begin_word}
while queue:
word, steps = queue.popleft()
if word == end_word:
return steps
for i in range(len(word)):
for ch in string.ascii_lowercase:
if ch == word[i]:
continue
candidate = word[:i] + ch + word[i + 1:]
if candidate in words and candidate not in visited:
visited.add(candidate)
queue.append((candidate, steps + 1))
return 0
print(ladder_length("hit", "cog", ["hot", "dot", "dog", "lot", "log", "cog"]))
# 5
print(ladder_length("hit", "cog", ["hot", "dot", "dog", "lot", "log"]))
# 0from collections import deque
import string
def ladder_length(begin_word, end_word, word_list):
words = set(word_list)
if end_word not in words:
return 0
queue = deque([(begin_word, 1)])
visited = {begin_word}
while queue:
word, steps = queue.popleft()
if word == end_word:
return steps
for i in range(len(word)):
for ch in string.ascii_lowercase:
if ch == word[i]:
continue
candidate = word[:i] + ch + word[i + 1:]
if candidate in words and candidate not in visited:
visited.add(candidate)
queue.append((candidate, steps + 1))
return 0
print(ladder_length("hit", "cog", ["hot", "dot", "dog", "lot", "log", "cog"]))
# 5
print(ladder_length("hit", "cog", ["hot", "dot", "dog", "lot", "log"]))
# 0Every word in word_listword_list is a node; two words are connected if they
differ by exactly one letter. BFS from begin_wordbegin_word finds the shortest
transformation sequence because BFS explores level by level — the first
time you reach end_wordend_word, you’ve found the fewest possible steps.
Time: for nn words of length LL (generating
every one-letter variant of every word). Space: .
Recap
- Binary tree problems almost always reduce to “handle
NoneNone, then combine the results fromnode.leftnode.leftandnode.rightnode.right” — that’s DFS. - BFS processes a tree or graph one full level/layer at a time, which is exactly what you need for level order traversal and for the shortest-path guarantee in Word Ladder.
- A 2D grid is just a graph in disguise: Number of Islands flood-fills connected land the same way DFS explores connected graph nodes.
- Course Schedule and Clone Graph both lean on a small piece of state per node — a three-color visited array for cycle detection, a visited-to-clone map for deep copying.
You’ve now worked through all three problem sets — Getting Started, Arrays and Strings, and Trees and Graphs. From here, the fastest way to keep improving is repetition on the real LeetCode judge: re-solve a few of these from a blank file, without peeking at Show solution, until the pattern comes out automatically.
If this helped you, consider buying me a coffee ☕
Buy me a coffeeWas this page helpful?
Let us know how we did
