-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy pathclone-graph.py
172 lines (159 loc) · 6.04 KB
/
clone-graph.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
# 133. Clone Graph
# 🟠 Medium
#
# https://leetcode.com/problems/clone-graph/
#
# Tags: Hash Table - Depth-First Search - Breadth-First Search - Graph
import timeit
from collections import deque
# Definition for a Node.
class Node:
def __init__(self, val=0, neighbors=None):
self.val = val
self.neighbors = neighbors if neighbors is not None else []
# Use two loops, on the first one, use BFS to make copies of all nodes,
# on the second, use BFS again to add the correct adjacency list to the
# newly created nodes.
#
# Time complexity: O(n) - We iterate over all the nodes twice.
# Space complexity: O(n) - The dictionary and the seen set will grow to
# the size of the input, if we consider that the input is limited to
# 100 nodes max, then O(1)
#
# Runtime: 91 ms, faster than 5.40%
# Memory Usage: 14.4 MB, less than 77.29%
class TwoLoopsBFS:
def cloneGraph(self, node: Node) -> Node:
# Base case, if not node return None.
if not node:
return None
# Use BFS to process the nodes and a hashmap to keep a pointer
# to the new nodes.
q = deque([node])
d = {}
# Process all nodes making copies of them.
while q:
current: Node = q.popleft()
# If we haven't processed this node yet.
if current.val not in d:
# Make a copy and save it to the dictionary.
copy = Node(current.val)
d[copy.val] = copy
# Enqueue the neighbors.
q.extend(current.neighbors)
# We have copies of all original nodes, link them back starting
# by the original root.
q = deque([node])
seen = set()
while q:
current = q.popleft()
# If we haven't processed this nodes' adjacency list yet.
if current.val not in seen:
# Mark the node processed.
seen.add(current.val)
# Get the node's copy.
copy: Node = d[current.val]
# Append the copies of the original neighbors.
new_neighbors = []
for n in current.neighbors:
# Append this neighbor to the queue.
q.append(n)
# Append its copy to the new node's neighbors.
new_neighbors.append(d[n.val])
# Link the adjacency list to the node.
copy.neighbors = new_neighbors
# Return the new root.
return d[node.val]
# Optimize the previous solution using only one loop. We append nodes
# that we have seen to an array and process their neighbors using the
# queue.
#
# Time complexity: O(n) - We visit each node once.
# Space complexity: O(n) - The dictionary and the queue will grow with
# the size of the input, if we consider that they are limited to
# 100 values, then we can say that space is O(1).
#
# Runtime: 85 ms, faster than 8.98%
# Memory Usage: 14.4 MB, less than 26.32%
class OnePassBFS:
def cloneGraph(self, node: Node) -> Node:
# Base case, no root.
if not node:
return None
# Use a dictionary to store clones.
clones = {}
clones[node.val] = Node(node.val, [])
# Use a queue to process elements.
q = deque([node])
while q:
current = q.popleft()
copy: Node = clones[current.val]
# Process this node's adjacency list
for n in current.neighbors:
# If we have not already processed this neighbor.
if n.val not in clones:
clones[n.val] = Node(n.val, [])
q.append(n)
# Append either the neighbor copy to this node's list.
copy.neighbors.append(clones[n.val])
# Return the clone of the input node.
return clones[node.val]
# Optimize the previous solution avoiding having to hash the values
# using an array indexed by node values. We can do that because the
# node values are guaranteed to be <= 100.
#
# Time complexity: O(n) - We visit each node once.
# Space complexity: O(n) - The array and the queue will grow with the
# size of the input, if we consider that they are limited to 100 values,
# then we can say that space is O(1).
#
# RRuntime: 45 ms, faster than 84.53%
# Memory Usage: 14.4 MB, less than 77.29%
class OnePassBFSList:
def cloneGraph(self, node: Node) -> Node:
# Base case, no root.
if not node:
return None
# Node values are guaranteed to be <= 100, we can use an array
# to store them, instead of a hashmap.
clones = [None] * 101
clones[node.val] = Node(node.val)
# Use a queue to process elements.
q = deque([node])
while q:
current = q.popleft()
copy: Node = clones[current.val]
# Process this node's adjacency list
for n in current.neighbors:
# If we have not already processed this neighbor.
if not clones[n.val]:
clones[n.val] = Node(n.val, [])
q.append(n)
# Append either the neighbor copy to this node's list.
copy.neighbors.append(clones[n.val])
# Return the clone of the input node.
return clones[node.val]
def test():
executors = [
TwoLoopsBFS,
OnePassBFS,
OnePassBFSList,
]
tests = []
for executor in executors:
start = timeit.default_timer()
for _ in range(1):
for col, t in enumerate(tests):
sol = executor()
result = sol.cloneGraph(t[0])
exp = t[1]
assert result == exp, (
f"\033[93m» {result} <> {exp}\033[91m for"
+ f" test {col} using \033[1m{executor.__name__}"
)
stop = timeit.default_timer()
used = str(round(stop - start, 5))
cols = "{0:20}{1:10}{2:10}"
res = cols.format(executor.__name__, used, "seconds")
print(f"\033[92m» {res}\033[0m")
test()