Copy Graph
DESCRIPTION (credit Leetcode.com)
Given a reference to a variable node which is part of an undirected, connected graph, write a function to return a copy of the graph as an adjacency list in dictionary form. The keys of the adjacency list are the values of the nodes, and the values are the neighbors of the nodes.
node is an instance of the following class, where neighbors is a list of references to other nodes in the graph (also of type IntGraphNode):
class IntGraphNode: def __init__(self, value = 0, neighbors = None): self.value = value self.neighbors = neighbors if neighbors is not None else []
EXAMPLES
Example 1:
Input:
node = IntGraphNode(1, [IntGraphNode(2), IntGraphNode(3)])
Output:
>>> copy_graph(node) {1: [2, 3], 2: [1], 3: [1]}
Example 2: Input:
n1 = IntGraphNode(1) n2 = IntGraphNode(2) n3 = IntGraphNode(3) n4 = IntGraphNode(4) n1.neighbors = [n2, n4] n2.neighbors = [n1, n3] n3.neighbors = [n2, n4] n4.neighbors = [n1, n3]
Output:
>>> copy_graph(n1) {1: [2, 4], 2: [1, 3], 3: [2, 4], 4: [1, 3]}
Run your code to see results here
Explanation
This solution uses depth-first search to traverse each node in the original graph. We can define a recursive helper function dfs that takes in an input node to help us perform the DFS traversal.
At each node, we:
- Add the value of the node as a key in the adjacency list, and a list of its neighbor's values as the value in the dictionary.
- Recursively call the dfs function on each neighbor of the node.
The adjacency list also helps us keep track of visited nodes. If we call dfs on a node that has already been added to the adjacency list, this means we have already visited the node, so we can return right away before making any more recursive calls.
class Solution:def copy_graph(self, node: IntGraphNode) -> Dict[int, List[int]]:adj_list = {}# recursive helper function to perform depth-first searchdef dfs(node):# we have already visited this node, so return without# without making more recursive callsif node.value in adj_list:return# add the node and its neighbors to the adjacency listadj_list[node.value] = [n.value for n in node.neighbors]# recursively call dfs on each neighbor of the current nodefor neighbor in node.neighbors:dfs(neighbor)if node:dfs(node)return adj_list
We can now take a closer look at the solution by visualizing each step as it traverses the graph below:
Initialization
The first step is to initialize adj_list as an empty dictionary. We will return this dictionary at the end, after the depth-first search traversal is complete. We then define the recursive helper function dfs that takes a node as input, and make the initial call to dfs with the input node.
def clone_graph(node):adj_list = {}def dfs(node):if node.value in adj_list:returnadj_list[node.value] = [n.value for n in node.neighbors]for neighbor in node.neighbors:dfs(neighbor)if node:dfs(node)return adj_list
Depth-First Search
When the dfs function is called with a node, it first checks if the node is already present in the adj_list dictionary. If it isn't, it adds the node to the dictionary. The key is the value of the node, and the value is a list of the values of each of the node's neighbors.
def clone_graph(node):adj_list = {}def dfs(node):if node.value in adj_list:returnadj_list[node.value] = [n.value for n in node.neighbors]for neighbor in node.neighbors:dfs(neighbor)if node:dfs(node)return adj_list
def dfs(node):if node.value in adj_list:returnadj_list[node.value] = [n.value for n in node.neighbors]for neighbor in node.neighbors:dfs(neighbor)
Then, it recursively calls dfs on each neighbor of the original node.
def clone_graph(node):adj_list = {}def dfs(node):if node.value in adj_list:returnadj_list[node.value] = [n.value for n in node.neighbors]for neighbor in node.neighbors:dfs(neighbor)if node:dfs(node)return adj_list
def dfs(node):if node.value in adj_list:returnadj_list[node.value] = [n.value for n in node.neighbors]for neighbor in node.neighbors:dfs(neighbor)
When dfs is called on a node that is already present in adj_list, it returns immediately without making any more recursive calls, which helps us avoid infinite loops in the graph. After returning, the function continues to the next neighbor of the current node.
def clone_graph(node):adj_list = {}def dfs(node):if node.value in adj_list:returnadj_list[node.value] = [n.value for n in node.neighbors]for neighbor in node.neighbors:dfs(neighbor)if node:dfs(node)return adj_list
def dfs(node):if node.value in adj_list:returnadj_list[node.value] = [n.value for n in node.neighbors]for neighbor in node.neighbors:dfs(neighbor)
def dfs(node):if node.value in adj_list:returnadj_list[node.value] = [n.value for n in node.neighbors]for neighbor in node.neighbors:dfs(neighbor)
def dfs(node):if node.value in adj_list:returnadj_list[node.value] = [n.value for n in node.neighbors]for neighbor in node.neighbors:dfs(neighbor)
This process continues until all nodes in the original graph have been visited and added to the adj_list dictionary.
Animated Solution
def clone_graph(node):adj_list = {}def dfs(node):if node.value in adj_list:returnadj_list[node.value] = [n.value for n in node.neighbors]for neighbor in node.neighbors:dfs(neighbor)if node:dfs(node)return adj_list
Complexity Analysis
- Time Complexity: O(N + M) where N is the number of nodes and M is the number of edges in the graph for the depth-first search traversal.
- Space Complexity: O(N + M) where N is the number of nodes in the graph and M is the number of edges in the graph. The space complexity is due to the adjacency list that stores the graph structure: each of the N nodes is stored once as the keys, and each of the M edges is stored as part of the values in the dictionary.
Loading comments...