Start learning free

LESSON 477 OF 536 · GRAPHS

Clone Graph in Python: BFS with a Dict of Copies

Practice O(V + E) Read the full explanation

You are given node 1 of a connected, undirected graph. Return a deep copy (a clone) of the graph.

Each node holds a value and the list of its neighbors; the node class is shown in the starter code. The graph has n nodes, numbered from 1 to n, and node i has the value i. The graph is connected, so every node can be reached from node 1 by going from neighbor to neighbor.

A deep copy is made of exactly n brand-new nodes, one for each node of the graph, with the same value. The neighbor list of each new node holds the new nodes of the original node's neighbors, in the same order. No node of the copy may be a node of the original graph. Return the new node 1. If the graph is empty (n = 0), you get None and return None.

The examples show a graph by its adjacency lists: adjList[i - 1] holds the values of node i's neighbors. After your function returns, the tests walk your copy from the node you returned, print the neighbors of each node, node by node, and report any node that belongs to the original graph.

 

Example 1:

Input: adjList = [[2, 4], [1, 3], [2, 4], [1, 3]]
Output: [[2, 4], [1, 3], [2, 4], [1, 3]]
Explanation: The graph has 4 nodes, joined in a square:
    1 - 2
    |   |
    4 - 3
Node 1's neighbors are nodes 2 and 4, node 2's are nodes 1 and 3, node 3's are nodes 2 and 4,
and node 4's are nodes 1 and 3. The copy has the same shape, made of 4 new nodes.

Example 2:

Input: adjList = [[]]
Output: [[]]
Explanation: The graph is a single node, node 1, with no neighbors. The copy is one new node with the value 1.

Example 3:

Input: adjList = []
Output: []
Explanation: The graph is empty: the function gets None and returns None.

 

Constraints:

  • 0 <= n <= 5 * 104
  • Node i has the value i, for 1 <= i <= n.
  • There are at most 105 edges. No edge joins a node to itself, and no edge appears twice.
  • The graph is connected.

Note:

Your algorithm should run in O(V + E) time, where V is the number of nodes and E the number of edges, and use O(V) extra space.

The tutorial walks the graph with a BFS, as in Course Schedule, and keeps a hash map from each original node to its copy. A node's copy is made the first time the search meets the node, and that is also when the node joins the queue. When a node leaves the queue, the copies of its neighbors are appended to its copy's list. The map is what stops the search from going around a cycle forever: a node that already has a copy is never made or queued again. A recursive DFS with the same map works too.

STEP 1 OF 11
    Ctrl + Enter to check

    PYTHON · GRAPHS

    Clone Graph in Python, explained

    Problem overview

    The method cloneGraph receives node, node 1 of a connected, undirected graph, or None when the graph is empty. It returns a deep copy of the graph: the copy of node 1.

    The lesson provides the node class, shown in the starter code as a comment. A Node has a val and a list neighbors of other nodes. Node i has the value i. A deep copy consists of exactly n brand-new nodes, one per original node, with the same values. The neighbor list of each new node holds the new nodes of the original node's neighbors, in the same order. No node of the copy may be a node of the original graph.

    The examples show graphs as adjacency lists, where entry i - 1 holds the values of node i's neighbors. For [[2, 4], [1, 3], [2, 4], [1, 3]] the graph is a square of four nodes (1 - 2 - 3 - 4 - 1), and the copy is another square of four new nodes. For [[]] the graph is one node with no neighbors. For [] the function gets None and returns None.

    The statement asks for O(V + E) time and O(V) extra space, where V is the number of nodes and E the number of edges.

    Approach: one copy per node, found through a dict

    Copying a graph is harder than copying a list or a tree, because nodes can be reached by several paths and the graph can contain cycles. Two things can go wrong:

    • A neighbor is copied twice, once from each of two nodes that reach it. The copy then has more than n nodes, and links that should meet at one node lead to two.
    • The walk goes around a cycle forever.

    One dict fixes both. It maps each original node to its copy. A node's copy is made the first time the search meets the node. Whenever the search meets that node again, it finds the copy in the dict instead of making a new one.

    The search is a breadth-first search with a queue. A node joins the queue at the moment its copy is made, so each node is queued exactly once. When a node leaves the queue, its copy has to receive its neighbor list: for each neighbor of the original, the copy of that neighbor is appended to the copy's list. Neighbors are visited in the original order, so the copied lists keep that order.

    On the square graph, the search runs like this:

    took 1 made [2, 4] queue [2, 4] copy list [2, 4]
    took 2 made [3] queue [4, 3] copy list [1, 3]
    took 4 made [] queue [3] copy list [1, 3]
    took 3 made [] queue [] copy list [2, 4]
    

    Each line shows the node taken from the queue, the nodes whose copies were created while looking at its neighbors, the queue afterwards, and the values in the neighbor list of its copy. When node 2 is taken, its neighbor 1 already has a copy, so only the link is added.

    A depth-first search with the same dict also works. The queue version avoids deep recursion, which matters because a graph of 50,000 nodes in a long chain would exceed Python's default recursion limit.

    Step by step

    1. Handle the empty graph. If node is None, return None. The next lines read the node's value, which needs a node.
    2. Create copies, a dict with one entry: node mapped to Node(node.val). The dict finds the copy of any node met so far, and it also tells which nodes have been met.
    3. Create a deque named queue holding just node. Every node in it already has its copy, and what the copy still lacks is its neighbors.
    4. Loop while queue is not empty. An empty deque counts as false, so while queue: is enough.
    5. Take the front node into current with popleft(). Its copy's neighbor list is filled in now.
    6. Loop over current.neighbors with neighbor. Over the whole run, each edge is seen twice, once from each end.
    7. Check neighbor not in copies. in on a dict looks at its keys, so this means the search meets the node for the first time.
    8. Make the copy: copies[neighbor] = Node(neighbor.val). A node that has a copy never gets a second one.
    9. Queue the neighbor, still inside the if, so its own neighbors are copied when it leaves the queue.
    10. Link the copies, back at the if's indentation: append copies[neighbor] to copies[current].neighbors. This is the key line. The copy of current points to the copy of neighbor, never to neighbor itself.
    11. Return copies[node] after the loop, the copy of node 1. The graph is connected, so every node was reached.

    The solution in Python

    class Solution:
        def cloneGraph(self, node: Optional['Node']) -> Optional['Node']:
            if node is None:
                return None
            copies = {node: Node(node.val)}
            queue = deque([node])
            while queue:
                current = queue.popleft()
                for neighbor in current.neighbors:
                    if neighbor not in copies:
                        copies[neighbor] = Node(neighbor.val)
                        queue.append(neighbor)
                    copies[current].neighbors.append(copies[neighbor])
            return copies[node]
    

    Python details worth a closer look:

    • Nodes as dict keys. The Node class doesn't define its own equality or hash, so Python compares and hashes its instances by identity. Two different nodes are two different keys, even when they have the same val. Node(1) == Node(1) is False. That is exactly what the dict needs.
    • The new nodes start with an empty list. The class uses neighbors if neighbors is not None else [], so each Node(val) gets its own fresh list. That's why appending to copies[current].neighbors never touches another node's list.
    • deque for the queue. popleft() and append() take constant time. list.pop(0) would shift every element and cost linear time per call. The lesson's editor already makes deque available. In a file of your own, add from collections import deque.
    • Appending the original would be a shallow copy. A line such as copies[current].neighbors.append(neighbor) would let the copy point into the original graph. The tests report any node of the copy that belongs to the original.
    • Truthiness. while queue: relies on an empty deque being false.

    Complexity

    Time: O(V + E). Each node is queued once and each copy is created once. Each edge is looked at twice, once from each end, with a dict lookup that takes constant time on average.

    Space: O(V). The dict holds one entry per node, and the queue never holds more than V nodes. The copy itself is also O(V + E), since it is the output.

    Edge cases and common mistakes

    • The empty graph. Without the None check, node.val raises AttributeError.
    • A single node. [[]] gives one new node with an empty neighbor list. The loop takes node 1, finds no neighbors and ends.
    • Cycles. In the square graph, node 4 is a neighbor of both 1 and 3. Because the dict is checked before copying, it is copied once and queued once.
    • Copying before checking. Writing copies[neighbor] = Node(neighbor.val) without the if would replace an existing copy, and earlier links would point to a node that's no longer in the dict.
    • Queueing outside the if. A node queued each time it is met would be processed several times, and its copy would get duplicate neighbors.
    • Losing the order. Neighbors must be appended in the original order. For a node whose original list is [3, 2], the copy's list is also [3, 2].

    Testing

    • adjList = [[2, 4], [1, 3], [2, 4], [1, 3]] gives [[2, 4], [1, 3], [2, 4], [1, 3]], with 4 new nodes and none shared with the original.
    • adjList = [[]] gives [[]].
    • adjList = [] gives [].
    • adjList = [[2], [1]] gives [[2], [1]]: two nodes pointing at each other.
    • adjList = [[2, 3], [1, 3], [1, 2]] gives [[2, 3], [1, 3], [1, 2]], a triangle.
    • adjList = [[3, 2], [1, 3], [1, 2]] gives [[3, 2], [1, 3], [1, 2]]: the order of neighbors is kept.
    Keep going with Python500+ step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free