Traverse Tree and finding all possible path to target - python

Original Question:
Given the root of a binary tree and an integer targetSum, return the number of paths where the sum of the values along the path equals targetSum. The path does not need to start or end at the root or a leaf, but it must go downwards (i.e., traveling only from parent nodes to child nodes).
I have posted my implementation below
Fails on test case: [1,null,2,null,3,null,4,null,5] , tagetSum = 3
it return 3 instead of 2
After some debugging I found out that node with value 3 is recorded twice, does anyone know why?
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def pathSum(self, root: Optional[TreeNode], targetSum: int) -> int:
self.res = 0
def dfs(node, sum):
if not node:
return
sum += node.val
if sum == targetSum:
self.res += 1
dfs(node.left, 0)
dfs(node.right, 0)
dfs(node.left, sum)
dfs(node.right, sum)
dfs(root, 0)
return self.res
Asked chatGPT and got the right solution...
class Solution:
def pathSum(self, root: Optional[TreeNode], targetSum: int) -> int:
self.res = 0
def dfs(node, sum):
if not node:
return
sum += node.val
if sum == targetSum:
self.res += 1
dfs(node.left, sum)
dfs(node.right, sum)
def traverse(node):
if not node:
return
dfs(node, 0)
traverse(node.left)
traverse(node.right)
traverse(root)
return self.res
Can someone explain the difference between the 2 implementaiton, I feel like they are doing the same thing.

As mentioned in comments, in your version two recursive calls are made on node.left. They have different sums (well, if sum is not 0!), but then both of those executions of dfs will have their own recursive call of dfs(node.left, 0) which is done to start new paths at that node. But this means dfs is called multiple times with the same arguments without any reason for a double count.
This does not happen in the correct solution. There the call with 0 is only made in traverse. Those calls determine the starting node of the paths that will be inspected. dfs takes care of trying all possible paths from the same starting node. In your code the two concepts are mixed and lead to duplicates.
I add here an alternative implementation that keeps track of the root-to-node sums in a list and then checks if the current sum (from the root) differs with exactly targetSum from a previous sum on the path.
class Solution:
def pathSum(self, root: Optional[TreeNode], targetSum: int) -> int:
def dfs(node, total, sums):
if not node:
return 0
total += node.val
count = sum((total - start == targetSum) for start in sums)
sums.append(total)
count += dfs(node.left, total, sums) + dfs(node.right, total, sums)
sums.pop()
return count
return dfs(root, 0, [0])

Related

How to optimise the solution to not get memory limit exceeded error or what might be getting me the error?

I came across the following problem.
You are given the root of a binary tree with n nodes.
Each node is uniquely assigned a value from 1 to n.
You are also given an integer startValue representing
the value of the start node s,
and a different integer destValue representing
the value of the destination node t.
Find the shortest path starting from node s and ending at node t.
Generate step-by-step directions of such path as a string consisting of only the
uppercase letters 'L', 'R', and 'U'. Each letter indicates a specific direction:
'L' means to go from a node to its left child node.
'R' means to go from a node to its right child node.
'U' means to go from a node to its parent node.
Return the step-by-step directions of the shortest path from node s to node t
Example 1:
Input: root = [5,1,2,3,null,6,4], startValue = 3, destValue = 6
Output: "UURL"
Explanation: The shortest path is: 3 → 1 → 5 → 2 → 6.
Example 2:
Input: root = [2,1], startValue = 2, destValue = 1
Output: "L"
Explanation: The shortest path is: 2 → 1.
I created the solution by finding the least common ancestor and then doing a depth-first-search to find the elements, Like this:-
# Definition for a binary tree node.
# class TreeNode(object):
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution(object):
def getDirections(self, root, startValue, destValue):
"""
:type root: Optional[TreeNode]
:type startValue: int
:type destValue: int
:rtype: str
"""
def lca(root):
if root == None or root.val == startValue or root.val == destValue:
return root
left = lca(root.left)
right = lca(root.right)
if left and right:
return root
return left or right
def dfs(root, value, path):
if root == None:
return ""
if root.val == value:
return path
return dfs(root.left, value, path + "L") + dfs(root.right, value, path + "R")
root = lca(root)
return "U"*len(dfs(root, startValue, "")) + dfs(root, destValue, "")
The solution runs good, however for a very large input it throws "Memory Limit Exceeded" error, can anyone tell me how I can optimise the solution, or what might I be doing that could be getting me into it ?
The reason you're getting a memory limit exceeded is the arguments to the dfs function. Your 'path' variable is a string that can be as large as the height of the tree (which can be the size of the whole tree if it's unbalanced).
Normally that wouldn't be a problem, but path + "L" creates a new string for every recursive call of the function. Besides being very slow, this means that your memory usage is O(n^2), where n is the number of nodes in the tree.
For example, if your final path is "L" * 1000, your call stack for dfs will look like this:
Depth 0: dfs(root, path = "")
Depth 1: dfs(root.left, path = "L")
Depth 2: dfs(root.left.left, path = "LL")
...
Depth 999: path = "L"*999
Depth 1000: path = "L"*1000
Despite all those variables being called path, they are all completely different strings, for a total memory usage of ~(1000*1000)/2 = 500,000 characters at one time. With one million nodes, this is half a trillion characters.
Now, this doesn't happen just because strings are immutable; in fact, even if you were using lists (which are mutable), you'd still have this problem, as path + ["L"] would still be forced to create a copy of path.
To solve this, you need to have exactly one variable for the path stored outside of the dfs function, and only append to it from the recursive dfs function. This will ensure you only ever use O(n) space.
def dfs(root, value, path):
if root is None:
return False
if root.val == value:
return True
if dfs(root.left, value, path):
path.append("L")
return True
elif dfs(root.right, value, path):
path.append("R")
return True
return False
root = lca(root)
start_to_root = []
dfs(root, startValue, start_to_root)
dest_to_root = []
dfs(root, destValue, dest_to_root)
return "U" * len(start_to_root) + ''.join(reversed(dest_to_root))

Inserting a node in Binary search tree in python

I know the basic working code for inserting a node in BST. But I expected this function to add a node at the right end of BST (because its value is the maximum in that tree), but it doesn't work and I wanna know the reason. I tried debugging it but I'm still not clear why it's behaving this way.
def putValueInBST(root, val): # assuming the val = 7 which is max of all the existing node values in BST
if root is None:
root = Node(val)
elif val > root.data:
putValueInBST(root.right, val)
else:
putValueInBST(root.left, val)
This code below behaves as expected.
def put_val_manually(r, val):
r.right.right = Node(val)
Aren't both of the above functions kinda similar, since they are adding the node at the end of the BST?
(of course in the put_val_manually() function, I am doing it directly.)
The full code is here: https://i.ibb.co/yf2YTYy/code.png
Try to return the node
def putValueInBST(root, val): # assuming the val = 7 which is max of all the existing node values in BST
if root is None:
return Node(val)
elif val > root.data:
root.right=putValueInBST(root.right, val)
else:
root.left=putValueInBST(root.left, val)
root=putValueInBST(root,val)
This code works

Why is my tree-node function producing Null when I run an array through it?

I am working on the LeetCode problem 104. Maximum Depth of Binary Tree:
Given the root of a binary tree, return its maximum depth.
A binary tree's maximum depth is the number of nodes along the longest path from the root node down to the farthest leaf node.
My attempt is not working: I first add the root to a queue (if root is not None), and then process it, by adding its children to the queue.
While doing this, I keep a counter, and each time I add a child node, I increment the counter by 1. When both left and right child exist, I will only increment the counter by 1.
from collections import deque
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
class Solution:
def max_depth(self,root):
counter = 1
queue = deque
if not root:
return 0
else:
queue.append(root)
while queue:
root = queue.popleft()
if root.left:
queue.append(root.left)
counter +=1
if root.right:
queue.append(root.right)
if root.left:
continue
else:
counter +=1
return counter
However, when I run the above on LeetCode, for an input of say [3,9,20,null,null,15,7], I get 'None' as a result.
Is it because I have structured the function to not take a list as an input?
Is it because I have structured the function to not take a list as an input?
No. This may be confusing, but on LeetCode the raw list representation of the input is translated to an instance of TreeNode before your function is called. So you should never have to deal with this list structure. It is merely the common input format that LeetCode uses across the different programming languages. But the conversion to the target language's data structure is done for you before your implementation is called.
Your code produces an error on the first call of queue.append because of this line:
queue = deque
This is wrong, as this makes queue a synonym for the class deque. But it should be an instance of it, so do:
queue = deque()
With that fix, the function does not return None.
However, its logic is not correct:
I keep a counter, and each time I add a child node, I increment the counter by 1. When both left and right child exist, I will only increment the counter by 1.
This practically means that you count the number of nodes that have at least one child, i.e. you count the number of internal nodes of the tree.
This is not correct. For instance, the following tree has 7 internal nodes:
___ 10 __
/ \
5 14
/ \ / \
1 8 12 20
/ \ / \ / \ / \
0 2 6 9 11 13 18 22
Obviously, 7 is not the correct answer. It should be 4 in this case.
Your queue-based solution will visit the nodes level by level, but you don't have any information about when you pass from one level to the next.
You can solve this by using two (standard) lists: the first list will have all the nodes from one level, and the second list will collect those from the next level. When that is done you know you have processed one level. Then you make the second list the first, and empty the second. Then you can restart this process for as long as there are nodes to process:
class Solution:
def maxDepth(self, root: TreeNode) -> int:
counter = 0
queue = []
if root:
queue.append(root)
while queue:
counter +=1
nextlevel = []
for root in queue:
if root.left:
nextlevel.append(root.left)
if root.right:
nextlevel.append(root.right)
queue = nextlevel
return counter
Making it a bit more compact, it can be:
class Solution:
def maxDepth(self, root: TreeNode) -> int:
counter = 0
if root:
queue = [root]
while queue:
counter +=1
queue = [root.left for root in queue if root.left
] + [root.right for root in queue if root.right]
return counter
You can also go for a depth-first traversal instead of the breadth-first traversal you were going for:
class Solution:
def maxDepth(self, root: TreeNode) -> int:
return 1 + max(self.maxDepth(root.left),
self.maxDepth(root.right)) if root else 0

Creating Binary Tree

Most of the questions I've searched for regarding binary trees shows the implementation of binary search trees, but not binary trees. The terms of a complete binary tree are:
Either an empty tree or it has 1 node with 2 children, where each
child is another Binary Tree.
All levels are full (except for possibly the last level)
All leaves on the bottom-most level are
as far left as possible.
I've come up with a concept but it doesn't seem to running through the recursion properly -- Does anyone know what I'm doing wrong?
class Node():
def __init__(self, key):
self.key = key
self.left = None
self.right = None
def add(self, key):
if self.key:
if self.left is None:
self.left = Node(key)
else:
self.left.add(key)
if self.right is None:
self.right = Node(key)
else:
self.right.add(key)
else:
self.key = key
return (self.key)
The problem in your code is that you are adding the same value multiple times. You add the node, and then still recurse deeper, where you do the same.
The deeper problem is that you don't really know where to insert the node before you have reached the bottom level of the tree, and have detected where that level is incomplete. Finding the correct insertion point may need a traversal through the whole tree... which is defeating the speed gain you would expect to get from using binary trees in the first place.
I provide here three solutions, starting with the most efficient:
1. Using a list as tree implementation
For complete trees there is a special consideration to make: if you number the nodes by level, starting with 0 for the root, and within each level from left to right, you notice that the number of a node's parent is (k-1)/2 when its own number is k. In the other direction: if a node with number k has children, then its left child has number k*2+1, and the right child has a number that is one greater.
Because the tree is complete, there will never be gaps in this numbering, and so you could store the nodes in a list, and use the indexes of that list for the node numbering. Adding a node to the tree now simply means you append it to that list. Instead of a Node object, you just have the tree list, and the index in that list is your node reference.
Here is an implementation:
class CompleteTree(list):
def add(self, key):
self.append(key)
return len(self) - 1
def left(self, i):
return i * 2 + 1 if i * 2 + 1 < len(self) else -1
def right(self, i):
return i * 2 + 2 if i * 2 + 2 < len(self) else -1
#staticmethod
def parent(i):
return (i - 1) // 2
def swapwithparent(self, i):
if i > 0:
p = self.parent(i)
self[p], self[i] = self[i], self[p]
def inorder(self, i=0):
left = self.left(i)
right = self.right(i)
if left >= 0:
yield from self.inorder(left)
yield i
if right >= 0:
yield from self.inorder(right)
#staticmethod
def depth(i):
return (i + 1).bit_length() - 1
Here is a demo that creates your example tree, and then prints the keys visited in an in-order traversal, indented by their depth in the tree:
tree = CompleteTree()
tree.add(1)
tree.add(2)
tree.add(3)
tree.add(4)
tree.add(5)
for node in tree.inorder():
print(" " * tree.depth(node), tree[node])
Of course, this means you have to reference nodes a bit different from when you would use a real Node class, but the efficiency gain pays off.
2. Using an extra property
If you know how many nodes there are in a (sub)tree, then from the bit representation of that number, you can know where exactly the next node should be added.
For instance, in your example tree you have 5 nodes. Imagine you want to add a 6 to that tree. The root node would tell you that you currently have 5 and so you need to update it to 6. In binary that is 110. Ignoring the left-most 1-bit, the rest of the bits tell you whether to go left or right. In this case, you should go right (1) and then finally left (0), creating the node in that direction. You can do this iteratively or recursively.
Here is an implementation with recursion:
class Node():
def __init__(self, key):
self.key = key
self.left = None
self.right = None
self.count = 1
def add(self, key):
self.count += 1
if self.left is None:
self.left = Node(key)
elif self.right is None:
self.right = Node(key)
# extract from the count the second-most significant bit:
elif self.count & (1 << (self.count.bit_length() - 2)):
self.right.add(key)
else:
self.left.add(key)
def inorder(self):
if self.left:
yield from self.left.inorder()
yield self
if self.right:
yield from self.right.inorder()
tree = Node(1)
tree.add(2)
tree.add(3)
tree.add(4)
tree.add(5)
for node in tree.inorder():
print(node.key)
3. Without extra property
If no property can be added to Node objects, then a more extensive search is needed to find the right insertion point:
class Node():
def __init__(self, key):
self.key = key
self.left = None
self.right = None
def newparent(self):
# Finds the node that should serve as parent for a new node
# It returns a tuple:
# if parent found: [-1, parent for new node]
# if not found: [height, left-most leaf]
# In the latter case, the subtree is perfect, and its left-most
# leaf is the node to be used, unless self is a right child
# and its sibling has the insertion point.
if self.right:
right = self.right.newparent()
if right[0] == -1: # found inbalance
return right
left = self.left.newparent()
if left[0] == -1: # found inbalance
return left
if left[0] != right[0]:
return [-1, right[1]] # found inbalance
# temporary result in perfect subtree
return [left[0]+1, left[1]]
elif self.left:
return [-1, self] # found inbalance
# temporary result for leaf
return [0, self]
def add(self, key):
_, parent = self.newparent()
if not parent.left:
parent.left = Node(key)
else:
parent.right = Node(key)
def __repr__(self):
s = ""
if self.left:
s += str(self.left).replace("\n", "\n ")
s += "\n" + str(self.key)
if self.right:
s += str(self.right).replace("\n", "\n ")
return s
tree = Node(1)
tree.add(2)
tree.add(3)
tree.add(4)
tree.add(5)
print(tree)
This searches recursively the tree from right to left, to find the candidate parent of the node to be added.
For large trees, this can be improved a bit, by doing a binary-search among paths from root to leaf, based on the length of those paths. But it will still not be as efficient as the first two solutions.
You can use the sklearn Decision trees, as they are able to be set up as binary decision trees as well. link to the documentation here.
You really need to augment your tree in some way. Since this is not a binary search tree, the only real information you have about each node is whether or not it has a left and right child. Unfortunately, this isn't helpful in navigating a complete binary tree. Imagine a complete binary tree with 10 levels. Until the 9th level, every single node has both a left child and a right child, so you have no way of knowing which path to take down to the leaves. So the question is, what information do you add to each node? I would add the count of nodes in that tree.
Maintaining the count is easy, since every time you descend down a subtree you know to add one to the count at that node. What you want to recognize is the leftmost imperfect subtree. Every perfect binary tree has n = 2^k - 1, where k is the number of levels and n is the number of nodes. There are quick and easy ways to check if a number is 1 less than a power of two (see the first answer to this question), and in fact in a complete binary tree every node has at most one child that isn't the root of a perfect binary tree. Follow a simple rule to add nodes:
If the left child is None, set root.left = Node(key) and return
Else if the right child is None, set root.right = Node(key) and return
If one of the children of the current node is the root of an imperfect subtree, make that node the current node (descend down that subtree)
Else if the sizes are unequal, make the node with the smaller subtree the current node.
Else, make the left child the current node.
By augmenting each node with the size of the subtree rooted there, you have all the information you need at every node to build a recursive solution.

recursion in traversing a binary tree

I am trying to do the leetcode problem problem #113, which is "Given a binary tree and a sum, find all root-to-leaf paths where each path's sum equals the given sum"
My problem is why does my code #1 shown below prints the values of all nodes in the tree? How does the recursion stack work in code #1 as opposed to how the recursion stack work in the code #2, which is a correct solution?
Thank you so much for helping me!
#code #1
class Solution:
def pathSum (self, root, sum):
self.res = []
self.dfs(root, sum, [])
return self.res
def dfs(self, root, sum, path):
if not root:
return
sum -= root.val
path += [root.val]
if not root.left and not root.right and sum == 0:
self.res.append(path)
self.dfs(root.left, sum, path)
self.dfs(root.right, sum, path)
#code #2
class Solution:
def pathSum (self, root, sum):
self.res = []
self.dfs (root, sum, [])
return self.res
def dfs (self, root, sum, path):
if not root:
return
sum -= root.val
if not root.left and not root.right and sum == 0:
self.res.append(path + [root.val])
self.dfs(root.left, sum, path+[root.val])
self.dfs(root.right, sum, path+[root.val])
Well, the comments above are well deserved, some examples would help here. For instance, you talk about printing, but there are no print statements here, so how are you driving this and how are you using this?
Having said that, I suspect the problem will trace back to the fact that Code#1 changes the value of path, while code #2 does not. Now if path was a number, this wouldn't matter because it would be passed in by value. However, you passed in [] originally (an empty list) which is an object . . . so it is passed in by reference. As a result, as code #1 proceeds you keep changing the node (path) above you, but in Code #2, the passed in paths never change.

Categories

Resources