Start learning free

LESSON 476 OF 535 · GRAPHS

Clone Graph in C++: BFS with an Unordered Map in O(V + E)

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 NULL and return NULL.

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 NULL and returns NULL.

 

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 13
    Ctrl + Enter to check

    C++ · GRAPHS

    Clone Graph in C++, explained

    Problem overview

    The function cloneGraph receives node, node 1 of a connected undirected graph, or nullptr when the graph is empty. The lesson provides the node type, shown in the starter code as a comment: a struct Node with an int val, a vector<Node*> neighbors and a constructor Node(int x) that sets val. The graph has n nodes, node i has the value i, and every node can be reached from node 1.

    The function returns a deep copy: exactly n brand-new nodes with the same values, where each new node's neighbor list 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 function returns the copy of node 1, and returns nullptr for an empty graph.

    The examples describe a graph by adjacency lists. For [[2, 4], [1, 3], [2, 4], [1, 3]] the graph is a square of four nodes, 1-2, 2-3, 3-4 and 4-1. The copy has four new nodes with the same shape. For [[]] it is a single node with no neighbors, and for [] the function gets nullptr and returns nullptr.

    The lesson 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 map

    Copying a node is easy. The difficulty is the links. A node's neighbors link back to it, so a naive copy that follows the neighbors would go around the square forever, and a node reached by two routes would be copied twice.

    The fix is 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 a queue. A node that already has a copy is never made or queued again, which stops the search from going around a cycle and guarantees one copy per node. The map also answers "what is the copy of this neighbor?" whenever a link has to be created.

    The search is a BFS. Every node in the queue already has its copy, but its copy still lacks a neighbor list. When a node leaves the queue, the search walks over its neighbors in order, makes any missing copy, and appends the copy of each neighbor to the node's copy. Walking the neighbors in the original order keeps the copied lists in the same order. A recursive DFS with the same map works too, but a graph of 50,000 nodes in a long chain makes the recursion deep, which is why the lesson uses a queue.

    On the square graph, the copy has four new nodes whose neighbor lists are 1: 2 4, 2: 1 3, 3: 2 4 and 4: 1 3, the original shape, and none of them is a node of the original graph. For a single node with no neighbors, the copy is a new node with the value 1 and an empty neighbor list.

    Step by step

    1. Handle the empty graph. If node is nullptr, return nullptr. The next lines read the node's value, which needs a node.
    2. Create the map: an unordered_map<Node*, Node*> named copies. Its keys are pointers, so two different nodes are two different keys, even with equal values.
    3. Copy the first node: copies[node] = new Node(node->val). The new makes a node that outlives the function and gives its pointer.
    4. Create a queue, queue<Node*> q, of the nodes whose neighbors still have to be copied.
    5. Push node onto the queue.
    6. Loop while the queue isn't empty.
    7. Take the front. Read q.front() into current first and then call q.pop(), since pop returns nothing.
    8. Loop over current->neighbors with a Node* neighbor. Over the whole run each edge is seen twice, once from each end.
    9. Test whether the neighbor has no copy yet: copies.count(neighbor) == 0.
    10. Make its copy with new Node(neighbor->val).
    11. Queue the neighbor, so its own neighbors get copied later.
    12. Link the copies: 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.
    13. Return copies[node] after the loop, the copy of node 1.

    The solution in C++

    class Solution {
    public:
        Node* cloneGraph(Node* node) {
            if (node == nullptr) {
                return nullptr;
            }
            unordered_map<Node*, Node*> copies;
            copies[node] = new Node(node->val);
            queue<Node*> q;
            q.push(node);
            while (!q.empty()) {
                Node* current = q.front();
                q.pop();
                for (Node* neighbor : current->neighbors) {
                    if (copies.count(neighbor) == 0) {
                        copies[neighbor] = new Node(neighbor->val);
                        q.push(neighbor);
                    }
                    copies[current]->neighbors.push_back(copies[neighbor]);
                }
            }
            return copies[node];
        }
    };
    

    The C++ details worth a closer look:

    • count instead of [] for the test. copies.count(neighbor) is 1 when the key is present and 0 otherwise, and it doesn't change the map. Writing copies[neighbor] to test would insert the key with a null pointer when it is missing, and the node would then look as if it already had a copy.
    • new and ownership. Each new Node(...) allocates on the heap, so the copies stay alive after the function returns. The caller owns them. The local map holds only pointers and disappears on return without freeing anything.
    • front then pop. q.front() returns a reference to the first element, and q.pop() removes it without returning it. Here the pointer is copied into current before the pop, so using it afterwards is safe.
    • Pointers as map keys. The map compares and hashes the addresses of the nodes. That is what is wanted: identity, not value.
    • Range-for over pointers. Node* neighbor is a copy of the pointer, not of the node, so the loop is cheap.

    Complexity

    Time: O(V + E). Each node is copied once and queued once. Each adjacency list is walked once, so every edge is looked at twice, once from each end. Map operations take constant time on average.

    Space: O(V) extra. The map holds one entry per node, and the queue never holds more than V nodes. The new nodes and their neighbor lists are the output itself.

    Edge cases and common mistakes

    • The empty graph. nullptr in, nullptr out. Without the first check, node->val would dereference a null pointer.
    • A single node. The map gets one entry, the queue yields one node with no neighbors, and the function returns a new node with an empty list.
    • Linking to the original. Appending neighbor instead of copies[neighbor] gives a copy whose lists lead back into the original graph. The tests report any node of the copy that belongs to the original.
    • Copying a node twice. Without the map check, a node reached from two neighbors is copied twice and the copy has duplicate nodes. On a cycle the search wouldn't end at all.
    • Marking too late. The copy is recorded when the node joins the queue, not when it leaves. Otherwise two nodes in the queue could both create a copy of the same neighbor.
    • Changing the neighbor order. The copies' lists must be in the original order. Appending while walking the original list preserves it.
    • Returning the wrong node. The result is the copy of the node that was passed in, copies[node], not the last node made.

    Testing

    • Adjacency lists [[2, 4], [1, 3], [2, 4], [1, 3]] give a copy with the same four lists, made of four new nodes.
    • [[]] gives one new node with the value 1 and no neighbors.
    • [] (nullptr) gives nullptr.
    Keep going with C++500+ step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free