Analyzing BST is balanced Code - python

I was looking at this python code to check if a Binary Search Tree is balanced. However, I was wondering if someone could explain how
height = max(left_height, right_height) + 1 comes into play. How does this line of code work to calculate the height. And finally, how this
is_left_balanced, left_height = TreeNode.is_balanced(cur_node.left)
is_right_balanced, right_height = TreeNode.is_balanced(cur_node.right)
stack recursion is working because once is_balanced(cur_node.left) is called, its called again when the second TreeNode.is_balanced(cur_node.right) is called.
Im having trouble following the stack trace for this.
Here is the entire code:
def is_balanced(cur_node) :
if (not cur_node) :
height = 0
return True, height
is_left_balanced, left_height = TreeNode.is_balanced(cur_node.left)
is_right_balanced, right_height = TreeNode.is_balanced(cur_node.right)
#To get the height of the current node, we find the maximum of the
#left subtree height and the right subtree height and add 1 to it
height = max(left_height, right_height) + 1
if (not is_left_balanced or not is_right_balanced):
return False, height
#If the difference between height of left subtree and height of
#right subtree is more than 1, then the tree is unbalanced
if (abs(left_height - right_height) > 1):
return False, height
return True, height

When dealing with recursion problems, you should always pay attention to what your base cases are. In this program, there is only one base case: an empty tree, which has a trivially obtainable height of 1 and is, by definition, balanced.
Now assuming a tree is unbalanced if its tallest half has a height of more than 1 plus the height of its other half (that is, its tallest half is taller by at least two levels), or if one of its subtrees is unbalanced, then we get a recursive way of calculating this.
First, return 0 and True for the trivially balanced (empty) tree. We got our base case.
Second, if either of the halves in unbalanced then the tree is unbalanced. This is checked for each subtree, and thus recursion will keep going until finding an empty tree, where it will start returning (picture the case of a tree with only one level, one node. Imagine how the code will run and you'll probably be able to extrapolate from there).
At last, if each subtree is balanced (that's the third case, since we already got that far into the program) then the only way the tree is unbalanced is if one of its subtrees is taller than the other by more than one level. We just check that, and return the opposite of that value.
I hope this helped you understand, feel free to ask me any other questions otherwise!

Related

Mathematical explanation of Leetcode question: Container With Most Water

I was working on a medium level leetcode question 11. Container With Most Water. Besides the brute force solution with O(n^2), there is an optimal solution with complexity of O(n) by using two pointers from left and right side of the container. I am a little bit confused why this "two pointers" method must include the optimal solution. Does anyone know how to prove the correctness of this algorithm mathematically? This is an algorithm that I don't know of. Thank you!
The original question is:
You are given an integer array height of length n. There are n vertical lines drawn such that the two endpoints of the ith line are (i, 0) and (i, height[i]).
Find two lines that together with the x-axis form a container, such that the container contains the most water. Return the maximum amount of water a container can store. Notice that you may not slant the container.
A brutal solution for this question is(O(n^2)):
def maxArea(self, height: List[int]) -> int:
length = len(height)
volumn = 0
#calculate all possible combinations, and compare one by one:
for position1 in range(0,length):
for position2 in range (position1 + 1, length):
if min(height[position1],height[position2])*(position2 - position1) >=volumn:
volumn = min(height[position1],height[position2])*(position2 - position1)
else:
volumn = volumn
return volumn
Optimal solution approach, The code I wrote is like this(O(n)):
def maxArea(self, height: List[int]) -> int:
pointerOne, pointerTwo = 0, len(height)-1
maxVolumn = 0
#Move left or right pointer one step for whichever is smaller
while pointerOne != pointerTwo:
if height[pointerOne] <= height[pointerTwo]:
maxVolumn = max(height[pointerOne]*(pointerTwo - pointerOne), maxVolumn)
pointerOne += 1
else:
maxVolumn = max(height[pointerTwo]*(pointerTwo - pointerOne), maxVolumn)
pointerTwo -= 1
return maxVolumn
Does anyone know why this "two pointers" method can find the optimal solution? Thanks!
Based on my understanding the idea is roughly:
Staring from widest bars (i.e. first and last bar) and then narrowing
width to find potentially better pair(s).
Steps:
We need to have ability to loop over all 'potential' candidates (the candidates better than what we have on hand rather than all candidates as you did in brutal solution) thus starting from outside bars and no inner pairs will be missed.
If an inner bar pair does exist, it means the height is higher than bars we have on hand, so you should not just #Move left or right pointer one step but #Move left or right pointer to next taller bar .
Why #Move left or right pointer whichever is smaller? Because the smaller bar doesn't fulfill the 'potential' of the taller bar.
The core idea behind the steps is: starting from somewhere that captures optimal solution inside (step 1), then by each step you are reaching to a better solution than what you have on hand (step 2 and 3), and finally you will reach to the optimal solution.
One question left for you think about: what makes sure the optimal solution is not missed when you executing steps above? :)
An informal proof could go something like this: imagine we are at some position in the iteration before reaching the optimal pair:
|
|
|~~~~~~~~~~~~~~~~~~~~~~~|
|~~~~~~~~~~~~~~~~~~~~~~~|
|~~~~~~~~~~~~~~~~~~~~~~~|
|~~~~~~~~~~~~~~~~~~~~~~~|
^ ^
A B
Now lets fix A (the smaller vertical line) and consider all of the choices left of B that we could pair with it. Clearly all of them yield a container with a smaller amount of water than we have currently between A and B.
Since we have stated that we have yet to reach the optimal solution, clearly A cannot be one of the lines contributing to it. Therefore, we move its pointer.
Q.E.D.

Iterative deepening and move ordering

As I understand, when implementing iterative deepening the best move at one depth should be used for ordering moves at higher depths. I have one issue with this: say I got the move m as my best move at the depth n, then when searching at the depth n + 1 should the move orderer only prioritize m at the highest level of search or at every level where move m is legal?
My current implementation of iterative deepening:
Search:
pvLine = None
for depth in range(1, self.maxDepth):
self.auxSearch(board, depth, initalHash)
# find the principal variation from the TT
pvLine = self.getPVLine(board, initalHash)
bestMove = pvLine[0][0]
bestValue = pvLine[0][1]
self.ordering.setBestMove(bestMove, depth + 1)
print(f'{depth=} | {bestValue=} | {bestMove=} | {pvLine=}')
return pvLine
Move ordering:
if((move, depth) == self.bestMove):
priority += self.BESTMOVE_BONUS
setBestMove function:
def setBestMove(self, move: chess.Move, depth: int) -> None:
self.bestMove = (move, depth)
self.BESTMOVE_BONUS is a very big number, so the move will have the highest priority.
Currently, I am making sure that the move orderer only prioritizes the best move from previous shallower search at the highest level of the current search. I am not sure if my approach is correct or not?
Move ordering will give you a much faster algorithm than without and is fairly easy to implement. You can read more about it here: https://www.chessprogramming.org/Move_Ordering.
I suggest you do as of now and put the best move from previous iteration first. The best move (or the best move sequence, "principal variation") is always the move from previous depths. So if you get a sequence of moves a1, b1, and c1 from depth 3, then at depth 4 you will try a1 at depth 1, b1 at depth 2, and c1 at depth 3 first.
Second you should try good capture moves, often found with MVV-LVA. Capturing a queen with a pawn is usually a good move, but the other way around could be bad if the pawn is protected.
Other easy to implement techniques are Killer moves and History moves, also found in the link above.

Shortest path between two nodes with fixed number of nodes in path

I have a weighted graph with around 800 nodes, each with a number of connections ranging from 1 to around 300. I need to find the shortest (lowest cost) path between two nodes with some extra criteria:
The path must contain exactly five nodes.
Each node has an attribute (called position in the example code) that takes one of five values; the five nodes in the path must all have unique values for this attribute.
The algorithm needs to allow for 1-2 required nodes to be specified that the path must contain at some point in any order.
The algorithm needs to take less than 10 seconds to run, preferably as little time as possible while losing as little accuracy as possible.
My current solution in Python is to run a Depth-Limited Depth-First Search which recursively searches every possible path. To make this algorithm run in reasonable time I have introduced a limit to the number of neighbour nodes that are searched at each recursion level. This number can be lowered to decrease the computation time but at the cost of accuracy. Currently this algorithm is far too slow, with my most recent test coming in at 75 seconds with a neighbour limit of 30. If I decrease this neighbour limit any more, my testing shows that the accuracy of the algorithm begins to suffer badly. I am out of ideas on how to solve this problem while satisfying all of the above criteria. My code is as follows:
# The path must go from start -> end, be of length 5 and contain all nodes in middle
# Each node is represented as a tuple: (value, position)
def dfs(start, end, middle=[], path=Path(), best=Path([], math.inf)):
# If this is the first level of recursion, initialise the path variable
if len(path) == 0:
path = Path([start])
# If the max depth has been exceeded, check if the current node is the goal node
if len(path) >= depth:
# If it is, save the path
# Check that all required nodes have been visited
if len(path) == depth and start == end and path.cost < best.cost and all(x in path.path for x in middle):
# Store the new best path
best.path = path.path
best.cost = path.cost
return
# Run DFS on all of the neighbors of the node that haven't been searched already
# Use the weights of the neighbors as a heuristic; sort by lowest weight first
neighbors = sorted([x for x in graph.get(*start).connected_nodes], key=lambda x: graph.weight(start, x))
# Make sure that all neighbors haven't been visited yet and that their positions aren't already accounted for
positions = [x[1] for x in path.path]
# Only visit neighbouring nodes with unique positions and ids
filtered = [x for x in neighbors if x not in path.path and x[1] not in positions]
for neighbor in filtered[:neighbor_limit]:
if neighbor not in path.path:
dfs(neighbor, end, middle, Path(path.path + [neighbor], path.cost + graph.weight(start, neighbor)), best)
return best
Path Class:
class Path:
def __init__(self, path=[], cost=0):
self.path = path
self.cost = cost
def __len__(self):
return len(self.path)
Any help in improving this algorithm or even suggestions on a better approach to the problem would be much appreciated, thanks in advance!
You should iterate over all possible orderings of the 'position' attribute, and for each one use Dijkstra's algorithm or BFS to find the shortest path that respects that ordering.
Since you know the position of the first and last nodes, there are only 3! = 6 different orderings for the intermediate nodes, so you only have to run Dijkstra's algorithm 6 times.
Even in python, this shouldn't take more than a couple hundred milliseconds to run, based on the input sizes you provided.

Are binary partition trees premade before points are added to nodes?

I'm trying to implement this algorithm in Python, but due to my lack of understanding tree structures I'm confused about creation process of the partition tree.
Brief Explanation:
Algorithm that was linked, is for partitioning a high-dimensional feature space into internal and leaf nodes so that query can be performed quickly.
It divides a large space using specific random test, hyperplane that splits one large cell into two.
This answer explains everything much more precisely.
(taken from the link above)
Code Fragments:
def random_test(self, main_point): # Main point is np.ndarray instance
dimension = main_point.ravel().size
random_coefficients = self.random_coefficients(dimension)
scale_values = np.array(sorted([np.inner(random_coefficients, point.ravel())
for point in self.points]))
percentile = random.choice([np.percentile(scale_values, 100 * self.ratio), # Just as described on Section 3.1
np.percentile(scale_values, 100 * (1 - self.ratio))])
main_term = np.inner(main_point.ravel(), random_coefficients)
if self.is_leaf():
return 0 # Next node is the center leaf child
else:
if (main_term - percentile) >= 0: # Hyper-plane equation defined in the document
return -1 # Next node is the left child
else:
return 1 # Next node is the right child
self.ratio as mentioned in the algorithm linked above, is determining how balanced and shallow the tree will be, at 1/2 it is supposed to generate the most balanced and shallow tree.
Then we move onto the iterative part, where the tree keeps dividing the space further and further until it reaches the leaf node (notice the keyword reaches), the problem is, it will never truly reaches the leaf node.
Since, the definition of leaf node in the document linked above is this:
def is_leaf(self):
return (self.capacity * self.ratio) <= self.cell_count() <= self.capacity
where self.cell_count() is number of points in the cell, self.capacity is the maximum amount of points that the cell can have and self.ratio is the split ratio.
My full code should basically divide the feature space by creating new nodes at initial iteration until the leaf node is created (but the leaf node is never created). See the fragment that contains the division process.
(taken from the document linked above)
tl;dr:
Are binary partition trees prepared (filled with empty nodes) before we add any points to them? If so, don't we require to define the level (depth) of the tree?
If not, are binary partition trees created while adding points to them? If so, then how is the first point (from the first iteration) added to the tree?
They are built as you go along. The first node is right or left of line 1. Then the next level asks right or left of line 2... your illustration from the provided paper shows this with the lines being numbered in association with the choice presented for finding the node.
Ofcourse right or left is not accurate. Some lines are cut horizontally. But the spaces created are binary.
I've been able to test the new method as mentioned in the comments, and it worked perfectly fine.
The algorithm that was linked above, implicitly states that the point shall be individually dropped down into the partition tree, passing all the random tests and creating new nodes as it is dropped down.
But there is a significant problem with this method, since in order to have a balanced efficient and shallow tree, left and right nodes must be distributed evenly.
Hence, in order to split the node, at every level of the tree, every point of the node must be passed to either left or right node (by a random test), until the tree reaches the depth where all nodes at that level are leaf.
In mathematical terms, root node contains a vector space which is divided into two left and right nodes containing convex polyhedrons bounded by supporting hyper-planes by the separating hyper-plane:
Negative term of the equation (I believe we can call it bias), is where the splitting ratio starts to play, it should be percentile of all node points between 100*r to 100*(1-r), so that tree is separated more evenly and it is more shallow. Basically it decides how even should hyper-plane separation be, that's why we require nodes that contain all the points.
I have been able to implement such system:
def index_space(self):
shuffled_space = self.shuffle_space()
current_tree = PartitionTree()
level = 0
root_node = RootNode(shuffled_space, self.capacity, self.split_ratio, self.indices)
current_tree.root_node = root_node
current_tree.node_array.append(root_node)
current_position = root_node.node_position
node_array = {0: [root_node]}
while True:
current_nodes = node_array[level]
if all([node.is_leaf() for node in current_nodes]):
break
else:
level += 1
node_array[level] = []
for current_node in current_nodes:
if not current_node.is_leaf():
left_child = InternalNode(self.capacity, self.split_ratio, self.indices,
self._append(current_position, [-1]), current_node)
right_child = InternalNode(self.capacity, self.split_ratio, self.indices,
self._append(current_position, [1]), current_node)
for point in current_node.list_points():
if current_node.random_test(point) == 1:
right_child.add_point(point)
else:
left_child.add_point(point)
node_array[level].extend([left_child, right_child])
where node_array contains all the nodes of the tree (root, internal and leaf).
Unfortunately, node.random_test(x) method:
def random_test(self, main_point):
random_coefficients = self.random_coefficients()
scale_values = [np.inner(self.random_coefficients(), point[:self.indices].ravel())
for point in self.points]
percentile = np.percentile(scale_values, self.ratio * 100)
main_term = np.inner(main_point[:self.indices].ravel(), random_coefficients)
if self.is_leaf():
return 0 # Next node is the center leaf child
else:
if (main_term - percentile) >= 0: # Hyper-plane equation defined in the document
return -1 # Next node is the left child
else:
return 1 # Next node is the right child
is inefficient, since calculating percentile takes too much time. Hence I have to find another way to calculate percentile (perhaps by performing short-circuited binary search to optimize percentile).
Conclusion:
This is just a large extension of Clinton Ray Mulligan's answer - which briefly explains the solution to create such trees and hence will remain as an accepted answer.
I have just added more details in case anyone is interested in implementing randomized binary partition trees.

a* search. adding movement cost from initial position

if f = g + h then where in the below code would I add g?
Also, besides adding the movement cost from my initial position, how else can I make this code more efficient?
def a_star(initial_node):
open_set, closed_set = dict(), list()
open_set[initial_node] = heuristic(initial_node)
while open_set:
onode = get_next_best_node(open_set)
if onode == GOAL_STATE:
return reconstruct_path(onode)
del open_set[onode]
closed_set.append(onode)
for snode in get_successor_nodes(onode):
if snode in closed_set:
continue
if snode not in open_set:
open_set[snode] = heuristic(snode)
self.node_rel[snode] = onode
return False
In the last if, if snode is not in open_set (no pun intended!) you shouldn't set just the heuristic, but the heuristic plus the cost of the current node. And if snode is in the open set, you should check the minimum between the present value and the current one (if there are two or more ways to reach the same node, the least costly one should be considered).
That means you need to store both the node's "actual" cost and the "estimated" cost. The actual cost of the initial node is zero. For every new node, it's the minimum for every incoming arc between the cost of the other vertex plus the cost of the arc (in other words, the cost of the last node plus the cost to move from that to the current one). The estimated cost would have to sum both values: the actual cost so far plus the heuristic function.
I don't know how the nodes are represented in your code, so I can't give advice more specific than that. If you still have doubt, please edit your question providing more details.

Categories