title: Clone Graph slug: clone-graph difficulty: medium leetcode_id: 133 leetcode_url: https://leetcode.com/problems/clone-graph/ categories: - graphs - hash-tables patterns: - dfs - bfs function_signature: "def clone_graph(node: Node | None) -> Node | None:" test_cases: visible: - input: { adjList: [[2, 4], [1, 3], [2, 4], [1, 3]] } expected: [[2, 4], [1, 3], [2, 4], [1, 3]] - input: { adjList: [[]] } expected: [[]] - input: { adjList: [] } expected: [] hidden: - input: { adjList: [[2], [1]] } expected: [[2], [1]] - input: { adjList: [[2, 3], [1, 3], [1, 2]] } expected: [[2, 3], [1, 3], [1, 2]] - input: { adjList: [[2, 3, 4], [1, 3], [1, 2, 4], [1, 3]] } expected: [[2, 3, 4], [1, 3], [1, 2, 4], [1, 3]] - input: { adjList: [[2], [1, 3], [2, 4], [3, 5], [4]] } expected: [[2], [1, 3], [2, 4], [3, 5], [4]] - input: { adjList: [[2, 3, 4, 5], [1], [1], [1], [1]] } expected: [[2, 3, 4, 5], [1], [1], [1], [1]] - input: { adjList: [[2], [1, 3], [2]] } expected: [[2], [1, 3], [2]] description: | Given a reference of a node in a **connected** undirected graph. Return a **deep copy** (clone) of the graph. Each node in the graph contains a value (`int`) and a list (`List[Node]`) of its neighbors. ``` class Node: def __init__(self, val = 0, neighbors = None): self.val = val self.neighbors = neighbors if neighbors is not None else [] ``` **Test case format:** For simplicity, each node's value is the same as the node's index (1-indexed). For example, the first node with `val == 1`, the second node with `val == 2`, and so on. The graph is represented in the test case using an adjacency list. An **adjacency list** is a collection of unordered lists used to represent a finite graph. Each list describes the set of neighbors of a node in the graph. The given node will always be the first node with `val = 1`. You must return the **copy of the given node** as a reference to the cloned graph. constraints: | - `0 <= Number of nodes <= 100` - `1 <= Node.val <= 100` - `Node.val` is unique for each node - There are no repeated edges and no self-loops in the graph - The graph is connected and all nodes can be visited starting from the given node examples: - input: "adjList = [[2,4],[1,3],[2,4],[1,3]]" output: "[[2,4],[1,3],[2,4],[1,3]]" explanation: "There are 4 nodes in the graph. 1st node's neighbors are 2nd and 4th nodes. 2nd node's neighbors are 1st and 3rd nodes. 3rd node's neighbors are 2nd and 4th nodes. 4th node's neighbors are 1st and 3rd nodes." - input: "adjList = [[]]" output: "[[]]" explanation: "The graph consists of only one node with val = 1 and it does not have any neighbors." - input: "adjList = []" output: "[]" explanation: "This is an empty graph with no nodes." explanation: intuition: | Imagine you're making a photocopy of a web of interconnected sticky notes. Each sticky note has a number and strings connecting it to other notes. You can't just copy the notes — you also need to recreate all the strings connecting them to the *new* copies, not the originals. The core challenge is handling **cycles and shared references**. If node A connects to node B, and node B connects back to node A, when you clone A and try to clone its neighbor B, then clone B's neighbors, you'll encounter A again. Without tracking what you've already cloned, you'd create infinite copies! Think of it like this: as you explore the graph, you need a **guest list** (hash map) that records "original node → cloned node". Before cloning any node, check if it's already on the list. If yes, return the existing clone. If no, create a new clone and add it to the list. This pattern — using a hash map to track visited/cloned nodes — is fundamental to graph traversal problems where you need to avoid infinite loops or duplicate processing. approach: | We solve this using **DFS with a Hash Map** to track cloned nodes: **Step 1: Handle the base case** - If the input node is `None`, return `None` immediately - An empty graph has no nodes to clone   **Step 2: Initialise the visited map** - `visited`: A hash map that maps original nodes to their clones - This serves two purposes: tracking what we've cloned AND providing quick access to clones when building neighbor lists   **Step 3: Define the recursive clone function** - If the current node is already in `visited`, return its clone (this handles cycles!) - Otherwise, create a new node with the same value - Add the mapping `original → clone` to `visited` BEFORE recursing (crucial for cycle handling) - Recursively clone each neighbor and add to the clone's neighbor list - Return the cloned node   **Step 4: Start the traversal** - Call the clone function on the starting node - The recursion will naturally traverse the entire connected graph - Return the clone of the starting node   The DFS approach naturally explores the graph depth-first, and the hash map ensures each node is cloned exactly once regardless of how many times we encounter it. common_pitfalls: - title: Infinite Recursion from Cycles description: | Graphs can have cycles — node A connects to B, which connects back to A. Without tracking visited nodes, you'll recurse forever: ``` clone(A) → clone neighbor B → clone neighbor A → clone neighbor B → ... ``` The fix is to add the clone to your visited map **before** recursing into neighbors. This way, when you encounter a node you're currently processing, you return the partially-built clone instead of creating a new one. wrong_approach: "Clone neighbors without checking if already visited" correct_approach: "Add to visited map before recursing, check visited before cloning" - title: Connecting to Original Nodes Instead of Clones description: | A subtle bug: when building the clone's neighbor list, you might accidentally add the *original* neighbor nodes instead of their *clones*: ```python # WRONG: connects clone to original neighbors clone.neighbors = node.neighbors # RIGHT: connects clone to cloned neighbors clone.neighbors = [clone_node(n) for n in node.neighbors] ``` The result would be a "clone" that's actually interconnected with the original graph — not a true deep copy. wrong_approach: "Copying the neighbors list reference directly" correct_approach: "Recursively clone each neighbor and build a new list" - title: Forgetting the Empty Graph Case description: | When the input is `None` (empty graph), you must return `None`. Attempting to access `.val` or `.neighbors` on `None` will cause a runtime error. Always handle the base case first before any other logic. key_takeaways: - "**Hash map for graph cloning**: The pattern of mapping `original → clone` is essential for any deep copy operation on graph structures" - "**Add to visited before recursing**: This prevents infinite loops in cyclic graphs — add the node to your tracking structure *before* processing its neighbors" - "**DFS vs BFS both work**: This problem can be solved with either traversal; DFS uses the call stack, BFS uses an explicit queue" - "**Foundation for graph problems**: This cloning technique applies to problems like copying linked lists with random pointers, serialising/deserialising graphs, and more" time_complexity: "O(n + e) where `n` is the number of nodes and `e` is the number of edges. We visit each node once and traverse each edge once." space_complexity: "O(n). The hash map stores one entry per node, and the recursion stack can be at most `n` deep for a linear graph." solutions: - approach_name: DFS with Hash Map is_optimal: true code: | class Solution: def cloneGraph(self, node: 'Node') -> 'Node': if not node: return None # Map from original node to its clone visited = {} def clone(node: 'Node') -> 'Node': # If already cloned, return the clone (handles cycles) if node in visited: return visited[node] # Create a new node with the same value clone_node = Node(node.val) # IMPORTANT: Add to visited BEFORE recursing to handle cycles visited[node] = clone_node # Recursively clone all neighbors for neighbor in node.neighbors: clone_node.neighbors.append(clone(neighbor)) return clone_node return clone(node) explanation: | **Time Complexity:** O(n + e) — We visit each node once and process each edge once. **Space Complexity:** O(n) — The hash map stores n entries, and recursion depth can be O(n). The DFS approach uses recursion to traverse the graph. The key insight is adding the clone to the visited map *before* recursing into neighbors — this ensures that when we encounter a cycle, we return the existing clone rather than creating infinite copies. - approach_name: BFS with Hash Map is_optimal: true code: | from collections import deque class Solution: def cloneGraph(self, node: 'Node') -> 'Node': if not node: return None # Map from original node to its clone visited = {node: Node(node.val)} # BFS queue contains original nodes to process queue = deque([node]) while queue: current = queue.popleft() # Process each neighbor of the current node for neighbor in current.neighbors: if neighbor not in visited: # Clone the neighbor and add to visited visited[neighbor] = Node(neighbor.val) # Add to queue for processing its neighbors queue.append(neighbor) # Connect the clone to its cloned neighbor visited[current].neighbors.append(visited[neighbor]) return visited[node] explanation: | **Time Complexity:** O(n + e) — Same as DFS, we visit each node and edge once. **Space Complexity:** O(n) — Hash map stores n entries, queue can hold up to n nodes. BFS uses an explicit queue instead of recursion. We clone nodes when we first discover them (adding to both visited and queue), then connect clones to their neighbors as we process each node. This approach is often preferred when you want to avoid deep recursion stacks.