-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy pathpath-sum-ii.py
251 lines (232 loc) · 8.41 KB
/
path-sum-ii.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
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
# 113. Path Sum II
# 🟠 Medium
#
# https://leetcode.com/problems/path-sum-ii/
#
# Tags: Backtracking - Tree - Depth-First Search - Binary Tree
import timeit
from collections import deque
from typing import List, Optional
from data import TreeNode, deserializeStringArrayToBinaryTree
# 1e4 calls
# » RecursiveDFS 0.10539 seconds
# » BacktrackDFS 0.10922 seconds
# » IterativeDFS 0.09478 seconds
# » IterativeBFS 0.09348 seconds
# Travel down the tree adding the value of the nodes to a list of "seen"
# node values and, at the same time, subtracting the value from the
# target sum. When we get to a leaf, if its value matches the target sum
# add the path we traveled to get there to the solution list.
#
# Time complexity: O(n) - We visit each node once.
# Space complexity: O(n) - For the call stack.
#
# Runtime: 103 ms, faster than 7.32%
# Memory Usage: 19.4 MB, less than 10.56%
class RecursiveDFS:
def pathSum(
self, root: Optional[TreeNode], targetSum: int
) -> List[List[int]]:
# Store all the possible paths.
paths = []
def dfs(
node: Optional[TreeNode], targetSum: int, path: List[int]
) -> None:
# Base case.
if not node:
return
# Mark the current node as visited along this path.
path.append(node.val)
# If we are in a leaf and the value matches the current
# target sum, we have a path that matches, add it to the
# solution set.
if not node.left and not node.right:
if node.val == targetSum:
paths.append(path)
else:
# We are not at a leaf.
dfs(node.left, targetSum - node.val, [*path])
dfs(node.right, targetSum - node.val, [*path])
# Initial call.
dfs(root, targetSum, [])
return paths
# Similar to the previous depth first search solution but optimizing
# time and memory usage by not copying the path list at every call of
# the dfs function, instead using backtracking to add the current value
# for the children to access and remove it once the children have been
# processed and control returns to the parent. This way, the only time
# we copy the paths, on O(n) over the number of nodes in the path
# operation, is when we add them to the result set.
#
# Time complexity: O(n) - We visit each node once.
# Space complexity: O(n) - For the call stack.
#
# Runtime: 86 ms, faster than 32.64%
# Memory Usage: 15.5 MB, less than 72.84%
class BacktrackDFS:
def pathSum(
self, root: Optional[TreeNode], targetSum: int
) -> List[List[int]]:
# Base case, no root.
if not root:
return []
# Save the paths found
paths = []
# Define the dfs function.
def dfs(node: TreeNode, path: List[int], current_sum: int):
# Add this node's value to the current sum and the path.
current_sum += node.val
path.append(node.val)
# If this is a leaf.
if not node.left and not node.right:
# The sum matches.
if current_sum == targetSum:
paths.append(path.copy())
else:
# Else, there is, at least, one child.
if node.left:
dfs(node.left, path, current_sum)
if node.right:
dfs(node.right, path, current_sum)
# Backtrack
path.pop()
current_sum -= node.val
# Initial call
dfs(root, [], 0)
return paths
# We can explore the tree inorder adding to the stack the current target
# sum, the remaining of the previous target sum after having visited
# this node, and a list of the node values that we have traveled to get
# here.
#
# Time complexity: O(n) - We visit each node once.
# Space complexity: O(n) - For the call stack.
#
# Runtime: 67 ms, faster than 57.43%
# Memory Usage: 15.2 MB, less than 93.22%
class IterativeDFS:
def pathSum(
self, root: Optional[TreeNode], targetSum: int
) -> List[List[int]]:
# Edge case.
if not root:
return []
# Store all possible results.
result = []
# Instead of only pushing nodes to the stack, push a tuple with
# the node, the current target sum, and the path traveled.
stack = [(root, targetSum - root.val, [root.val])]
# Inorder traversal of the tree.
while stack:
# Pop the current node.
current, target, path = stack.pop()
# If this node is a leaf, check whether this path had the
# target sum.
if not current.left and not current.right and target == 0:
result.append(path)
# If the node is not a leaf.
if current.left:
stack.append(
(
current.left,
target - current.left.val,
path + [current.left.val],
)
)
if current.right:
stack.append(
(
current.right,
target - current.right.val,
path + [current.right.val],
)
)
# Return the results, it could be empty.
return result
# We can modify slightly the previous solution to explore the tree using
# Breadth-First Search instead of DFS. Since we are keeping all the
# information we need inside the nodes that we are visiting, the
# solution is pretty much the same, only the order in which we visit
# the nodes changes.
#
# Time complexity: O(n) - We visit each node once.
# Space complexity: O(n) - For the call stack.
#
# Runtime: 76 ms, faster than 40.97%
# Memory Usage: 15.2 MB, less than 93.22%
class IterativeBFS:
def pathSum(
self, root: Optional[TreeNode], targetSum: int
) -> List[List[int]]:
# Edge case.
if not root:
return []
# Store all possible results.
result = []
# Instead of only pushing nodes to the stack, push a tuple with
# the node, the current target sum, and the path traveled.
queue = deque()
queue.append((root, targetSum - root.val, [root.val]))
# Inorder traversal of the tree.
while queue:
# Pop the current node.
current, target, path = queue.popleft()
# If this node is a leaf, check whether this path had the
# target sum.
if not current.left and not current.right and target == 0:
result.append(path)
# If the node is not a leaf.
if current.left:
queue.append(
(
current.left,
target - current.left.val,
path + [current.left.val],
)
)
if current.right:
queue.append(
(
current.right,
target - current.right.val,
path + [current.right.val],
)
)
# Return the results, it could be empty.
return result
def test():
executors = [
RecursiveDFS,
BacktrackDFS,
IterativeDFS,
IterativeBFS,
]
tests = [
[
"[5,4,8,11,null,13,4,7,2,null,null,5,1]",
22,
[[5, 4, 11, 2], [5, 8, 4, 5]],
],
["[1,2,3]", 5, []],
["[1,2]", 0, []],
]
for executor in executors:
start = timeit.default_timer()
for _ in range(1):
for i, t in enumerate(tests):
sol = executor()
root = deserializeStringArrayToBinaryTree(t[0])
result = sol.pathSum(root, t[1])
# Need to sort to make the order not matter.
result.sort()
exp = sorted(t[2])
assert result == exp, (
f"\033[93m» {result} <> {exp}\033[91m for "
+ f"test {i} 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()