Clone a Graph (LeetCode 133) is a foundational graph problem that tests your understanding of deep copy vs shallow copy, cycle handling in graphs, and the difference between BFS and DFS traversal with memoization. It appears at Google and Facebook interviews.
Problem
Given a reference to a node in a connected undirected graph, return a deep copy (clone) of the graph. Each node has a value and a list of neighbors.
DFS Solution
class Node:
def __init__(self, val=0, neighbors=None):
self.val = val
self.neighbors = neighbors if neighbors is not None else []
def clone_graph_dfs(node: Node) -> Node:
"""
DFS with memoization (old_node -> cloned_node mapping).
The memo dict handles cycles: if we've already cloned a node,
return the existing clone instead of recursing infinitely.
Time: O(V + E), Space: O(V)
"""
if node is None:
return None
memo = {} # Maps original node -> cloned node
def dfs(original):
if original in memo:
return memo[original] # Return already-cloned node (handles cycles)
clone = Node(original.val)
memo[original] = clone # Register BEFORE recursing to handle cycles
for neighbor in original.neighbors:
clone.neighbors.append(dfs(neighbor))
return clone
return dfs(node)
BFS Solution
from collections import deque
def clone_graph_bfs(node: Node) -> Node:
"""
BFS-based deep copy. More iterative, avoids Python's recursion limit.
"""
if node is None:
return None
memo = {node: Node(node.val)}
queue = deque([node])
while queue:
original = queue.popleft()
for neighbor in original.neighbors:
if neighbor not in memo:
memo[neighbor] = Node(neighbor.val)
queue.append(neighbor)
# Connect the clone's neighbor
memo[original].neighbors.append(memo[neighbor])
return memo[node]
Why the Memo Dict Goes Before Recursion
This ordering is critical:
clone = Node(original.val)
memo[original] = clone # ← MUST happen before recursing into neighbors
for neighbor in original.neighbors:
clone.neighbors.append(dfs(neighbor)) # dfs may return to this node via cycle
If you register after recursing, a cycle (A → B → A) would cause infinite recursion: A’s clone isn’t in memo when B tries to clone A again.
Testing the Clone
def verify_deep_copy(original: Node, cloned: Node) -> bool:
"""Verify clone is a true deep copy: same structure, different objects."""
visited = set()
stack = [(original, cloned)]
while stack:
orig, clone = stack.pop()
if id(orig) == id(clone):
return False # Same object — not a deep copy!
if orig.val != clone.val:
return False # Different values
if len(orig.neighbors) != len(clone.neighbors):
return False
if id(orig) in visited:
continue
visited.add(id(orig))
for o_neighbor, c_neighbor in zip(orig.neighbors, clone.neighbors):
stack.append((o_neighbor, c_neighbor))
return True
Generalizing: Deep Copy Patterns
The clone graph pattern applies to any object graph with cycles:
- Cloning a linked list with random pointers (LeetCode 138)
- Copying directory trees with symlinks (cycles)
- Serializing graph state in game engines
The universal pattern: memo = {} where you register the new object immediately before recursing into references.
Related Graph Topics
- BFS vs DFS — Clone Graph works with both; DFS with memoization is more concise, BFS avoids recursion limits for large graphs
- Detect a Cycle in a Graph — the memo dict in Clone Graph handles cycles just as visited sets handle cycles in traversal; understand the parallel