Start learning free

LESSON 481 OF 540 · GRAPHS

Clone Graph in Scala: BFS with a HashMap 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 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

    SCALA · GRAPHS

    Clone Graph in Scala, explained

    Problem overview

    The method cloneGraph(node: Node): Node receives node 1 of a connected, undirected graph and returns a deep copy of the whole graph: the copy of node 1. The graph has n nodes numbered 1 to n, and node i has the value i. The lesson provides the Node class: it has a var field val of type Int and a var neighbors, a mutable.ArrayBuffer[Node] that starts empty.

    A deep copy is made of exactly n brand-new nodes, one per original node, with the same values. The neighbour list of each new node holds the new nodes of the original node's neighbours, in the same order. No node of the copy may be a node of the original graph. For an empty graph the method receives null and returns null.

    The examples write a graph as adjacency lists, where the list at position i - 1 holds the values of node i's neighbours. For [[2, 4], [1, 3], [2, 4], [1, 3]], a square of four nodes, the copy has the same shape. After running the method on it, walking the copy from the returned node prints [[2, 4], [1, 3], [2, 4], [1, 3]], and none of the four nodes is a node of the original. For a single node with no neighbours, [[]], the copy is one new node. For the empty graph the result is null.

    Approach: one copy per node, found through a map

    Copying a node's value is easy. Copying its neighbours is the problem: they point back, so a graph with a cycle would send a naive copy around the cycle forever, and a node reached by two routes would be copied twice.

    The fix is a 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. When a node leaves the queue, the copies of its neighbours are appended to its copy's neighbour list. A node that already has a copy is never copied or queued again, which also stops the search from circling a cycle.

    This is a breadth-first search. Every node enters the queue once, and each edge is looked at twice, once from each end, so the work is proportional to nodes plus edges.

    A recursive depth-first search with the same map works too. The queue version keeps the call stack flat, which matters for a long chain such as 50,000 nodes in a row. The method copies such a chain correctly: the copy has 50,000 nodes.

    Step by step

    1. Handle the empty graph. If node == null, return null. The next lines read the node's value, which needs a node.
    2. Create the map copies, an empty mutable.HashMap[Node, Node]. It maps each original node to its copy.
    3. Put the first entry in: copies(node) = new Node(node.`val`). The map both finds the copy of any node met so far and tells which nodes have been met.
    4. Create the queue queue, an empty mutable.Queue[Node], for nodes whose neighbours still have to be copied.
    5. Enqueue node. Every node in the queue already has its copy. What the copy still lacks is its neighbour list, which is filled when the node leaves the queue.
    6. Loop while queue.nonEmpty. The search ends when every reachable node, which here means every node, has been taken from the queue.
    7. Dequeue the node at the front into current. Now its copy's neighbour list gets filled.
    8. Loop over current.neighbors with neighbor.
    9. Check !copies.contains(neighbor). On a map, contains looks at the keys. No copy yet means this is the first time the search meets the node.
    10. Make the copy: copies(neighbor) = new Node(neighbor.`val`). This is the one moment this node's copy is made.
    11. Enqueue neighbor so its own neighbours get copied when it leaves the queue. A node joins the queue only when its copy is made, so each node is queued exactly once.
    12. Link the copies after the if, still inside the loop: copies(current).neighbors += copies(neighbor). The copy of current points to the copy of neighbor, never to neighbor itself. The neighbours arrive in the original order, so each copied list keeps that order.
    13. Return copies(node) as the last expression: the copy of node 1.

    The solution in Scala

    class Solution {
        def cloneGraph(node: Node): Node = {
            if (node == null) {
                return null
            }
            val copies = mutable.HashMap[Node, Node]()
            copies(node) = new Node(node.`val`)
            val queue = mutable.Queue[Node]()
            queue.enqueue(node)
            while (queue.nonEmpty) {
                val current = queue.dequeue()
                for (neighbor <- current.neighbors) {
                    if (!copies.contains(neighbor)) {
                        copies(neighbor) = new Node(neighbor.`val`)
                        queue.enqueue(neighbor)
                    }
                    copies(current).neighbors += copies(neighbor)
                }
            }
            copies(node)
        }
    }
    

    The Scala details worth a closer look:

    • val is a keyword, so the field is written with backticks: node.`val`. The same backticks appear in new Node(node.`val`).
    • Identity, not value, is the key. Node does not define equals or hashCode, so two nodes are equal only when they are the same object. Two different nodes both holding 1 are different keys: new Node(1) == new Node(1) is false, and a map given both as keys has size 2. That is what the algorithm needs.
    • copies(node) = ... on a mutable.HashMap inserts or replaces an entry. Reading copies(neighbor) for a missing key would throw, which is why the code checks contains first. Every read happens after the copy exists.
    • mutable.Queue provides enqueue to add at the back and dequeue() to remove from the front. The lesson provides mutable; in a file of your own, add import scala.collection.mutable.
    • += on an ArrayBuffer appends an element. Because neighbors is a var holding a mutable buffer, += changes the buffer in place.
    • null and ==. node == null is safe on a null reference in Scala, and it is the check the empty graph needs. The early return sits outside any loop, so it is an ordinary one.

    Complexity

    Let V be the number of nodes and E the number of edges.

    Time: O(V + E). Each node is queued once. When a node leaves the queue, its neighbour list is scanned once, and over the whole run each edge is scanned twice. Map and queue operations take constant time on average.

    Space: O(V) extra. The map holds one entry per node, and the queue holds at most all of them. The copy itself, V nodes and E links, is the output.

    Edge cases and common mistakes

    • The empty graph. null in, null out. Without the first check, reading node.`val` would throw a NullPointerException.
    • A single node. The loop runs once, finds no neighbours, and the result is a new node with an empty list.
    • Cycles. In the square, node 1's copy is linked to the copies of 2 and 4, and later those link back to 1's copy. The map is what makes them point to the same new node and not to new duplicates.
    • Linking to the original. Writing copies(current).neighbors += neighbor would build a copy that points into the original graph. The printed values would look right while the copy shared nodes with the original, which the tests check.
    • Making the copy later than the enqueue. Every node needs its copy at the moment it joins the queue, because the linking line reads copies(neighbor) straight away.
    • Losing the order. Neighbours must be appended in the original order. The loop does this because it walks current.neighbors from the start. A copy of [[3, 2], [1, 3], [2, 1]] keeps node 1's neighbours as 3 then 2.
    • Using values as keys. The values are unique here, so a map from Int to Node would also work, but mapping nodes to nodes is the form that works for any graph.

    Testing

    • [[2, 4], [1, 3], [2, 4], [1, 3]] copies to [[2, 4], [1, 3], [2, 4], [1, 3]] with no shared nodes.
    • [[]] copies to [[]].
    • [], the empty graph, gives null.
    • [[2, 3], [1], [1]] copies to [[2, 3], [1], [1]].
    • [[3, 2], [1, 3], [2, 1]] copies to [[3, 2], [1, 3], [2, 1]].
    Keep going with Scala500+ step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free