15 puzzle astar search that goes into an infinite loop - python

I am trying to develop a 15 star puzzle program in Python and its supposed to sort everything in numerical order using the a star search algorithm with the 0 being at the end.
Here is my a star algorithm I've developed so far:
"""Search the nodes with the lowest f scores first.
You specify the function f(node) that you want to minimize; for example,
if f is a heuristic estimate to the goal, then we have greedy best
first search; if f is node.depth then we have breadth-first search.
There is a subtlety: the line "f = memoize(f, 'f')" means that the f
values will be cached on the nodes as they are computed. So after doing
a best first search you can examine the f values of the path returned."""
def best_first_graph_search_manhattan(root_node):
start_time = time.time()
f = manhattan(root_node)
node = root_node
frontier = []
# how do we create this association?
heapq.heappush(frontier, node)
explored = set()
z = 0
while len(frontier) > 0:
node = heapq.heappop(frontier)
print(node.state.tiles)
explored.add(node)
if (goal_test(node.state.tiles)):
#print('In if statement')
path = find_path(node)
end_time = time.time()
z = z + f
return path, len(explored), z, (end_time - start_time)
for child in get_children(node):
# calcuate total cost
f_0 = manhattan(child)
z = z + f_0
print(z)
if child not in explored and child not in frontier:
#print('Pushing frontier and child')
heapq.heappush(frontier, child)
print('end of for loop')
return None
"""
Return the heuristic value for a given state using manhattan function
"""
def manhattan(node):
# Manhattan Heuristic Function
# x1, y1 = node.state.get_location()
# x2, y2 = self.goal
zero_location = node.state.tiles.index('0')
x1 = math.floor(zero_location / 4)
y1 = zero_location % 4
x2 = 3
y2 = 3
return abs(x2 - x1) + abs(y2 - y1)
"""
astar_search() is a best-first graph searching algortithim using equation f(n) = g(n) + h(n)
h is specified as...
"""
def astar_search_manhattan(root_node):
"""A* search is best-first graph search with f(n) = g(n)+h(n).
You need to specify the h function when you call astar_search, or
else in your Problem subclass."""
return best_first_graph_search_manhattan(root_node)
Here is the rest of my program. Assume that everything is working correctly in the following:
import random
import math
import time
import psutil
import heapq
#import utils.py
import os
import sys
from collections import deque
# This class defines the state of the problem in terms of board configuration
class Board:
def __init__(self,tiles):
self.size = int(math.sqrt(len(tiles))) # defining length/width of the board
self.tiles = tiles
#This function returns the resulting state from taking particular action from current state
def execute_action(self,action):
new_tiles = self.tiles[:]
empty_index = new_tiles.index('0')
if action=='l':
if empty_index%self.size>0:
new_tiles[empty_index-1],new_tiles[empty_index] = new_tiles[empty_index],new_tiles[empty_index-1]
if action=='r':
if empty_index%self.size<(self.size-1):
new_tiles[empty_index+1],new_tiles[empty_index] = new_tiles[empty_index],new_tiles[empty_index+1]
if action=='u':
if empty_index-self.size>=0:
new_tiles[empty_index-self.size],new_tiles[empty_index] = new_tiles[empty_index],new_tiles[empty_index-self.size]
if action=='d':
if empty_index+self.size < self.size*self.size:
new_tiles[empty_index+self.size],new_tiles[empty_index] = new_tiles[empty_index],new_tiles[empty_index+self.size]
return Board(new_tiles)
# This class defines the node on the search tree, consisting of state, parent and previous action
class Node:
def __init__(self,state,parent,action):
self.state = state
self.parent = parent
self.action = action
#self.initial = initial
#Returns string representation of the state
def __repr__(self):
return str(self.state.tiles)
#Comparing current node with other node. They are equal if states are equal
def __eq__(self,other):
return self.state.tiles == other.state.tiles
def __hash__(self):
return hash(self.state)
def __lt__(self, other):
return manhattan(self) < manhattan(other)
# Utility function to randomly generate 15-puzzle
def generate_puzzle(size):
numbers = list(range(size*size))
random.shuffle(numbers)
return Node(Board(numbers),None,None)
# This function returns the list of children obtained after simulating the actions on current node
def get_children(parent_node):
children = []
actions = ['l','r','u','d'] # left,right, up , down ; actions define direction of movement of empty tile
for action in actions:
child_state = parent_node.state.execute_action(action)
child_node = Node(child_state,parent_node,action)
children.append(child_node)
return children
# This function backtracks from current node to reach initial configuration. The list of actions would constitute a solution path
def find_path(node):
path = []
while(node.parent is not None):
path.append(node.action)
node = node.parent
path.reverse()
return path
# Main function accepting input from console , running iterative_deepening_search and showing output
def main():
global nodes_expanded
global path
global start_time
global cur_time
global end_time
nodes_expanded = 0
process = psutil.Process(os.getpid())
initial_memory = process.memory_info().rss / 1024.0
initial = str(input("initial configuration: "))
initial_list = initial.split(" ")
root = Node(Board(initial_list),None,None)
print(astar_search_manhattan(root))
final_memory = process.memory_info().rss / 1024.0
print('Directions: ', path)
print('Total Time: ', (end_time-start_time), ' seconds')
print('Total Memory: ',str(final_memory-initial_memory)+" KB")
print('Total Nodes Expanded: ', nodes_expanded)
# Utility function checking if current state is goal state or not
def goal_test(cur_tiles):
return cur_tiles == ['1','2','3','4','5','6','7','8','9','10','11','12','13','14','15','0']
if __name__=="__main__":main()
I've managed to narrow it down into my for loop in my best_first_graph_search_manhattan function and it appears that the infinite loop is caused if the if statement where its checking if child is not in explored and child is not in frontier. I'm unsure if its the way I'm calling my child function or the way I'm pushing frontier and child into my priority queue. I have imported heapq into my program and I've done extensive research where importing that function allows you to utilize priority queue into your program. Please don't mind other variables that are not used in my a star search.
Here is a test case: 1 0 3 4 5 2 6 8 9 10 7 11 13 14 15 12 | DRDRD
Thank you all very much for your help!

Related

Why is this tree search implementation causing a performance leak?

I'm coding a tree search in a simultaneous move game in python. I'm using python at this stage for ease of development, before rewriting the project in C++. For this reason, I would normally not be too concerned about performance. Originally my code was structured like this
class DecisionNode():
def __init__(self, players):
self.transitions = dict()
self.N = [dict() for _ in players]
self.V = [dict() for _ in players]
self.visited = 0
self.c = 2
self.players = players
def search(self, s, depth=0, verbose=False, mode=None):
if not self.visited: #unexpanded node
score, log = s.rollout()
actions = s.actions()
for acts, n, v in zip(actions, self.N, self.V):
for a in acts:
n[a] = 0
v[a] = 0
for x in actions[0]:
for y in actions[1]:
self.transitions[(x, y)] = TransitionNode(self.players)
self.visited += 1
return score, depth, False
else:
if not self.transitions: #terminal node
score, log = s.rollout()
return score, depth, True
else: #recursive call
actionTuple = self.selectActions(verbose, mode)
transition = self.transitions[actionTuple]
if not transition.step:
transition.expand(s.apply(actionTuple, battle.Path()))
steps = transition.select()
path, s_ = s.apply(actionTuple, battle.Path(steps))[0]
child = transition.decisions[steps]
score, d, terminal = child.search(s_, depth+1, verbose, mode)
for _, a, n, v, in zip(score, actionTuple, self.N, self.V):
n[a] += 1
v[a] += _
self.visited += 1
return score, d, terminal
def selectActions(self, verbose=False, mode=None):
actionTuple = ()
data = [[], []]
for _ in self.players:
actions = self.N[_].keys()
for a in actions:
n = self.N[_][a] + 1
v = self.V[_][a]
if mode is None:
score = v/n + self.c * math.sqrt(math.log(self.visited)/n)
data[_].append((a, score))
actionTuple += (max(data[_],key= lambda datum : datum[1])[0],)
return actionTuple
class TransitionNode():
def __init__(self, players):
self.players = players
self.step = []
self.probs = []
self.paths = []
self.decisions = dict()
def expand(self, batch):
for path, state in batch:
self.step.append(path.step)
self.probs.append(path.p)
self.paths.append(path)
self.decisions[path.step] = DecisionNode(self.players)
def select(self):
steps = numpy.random.choice(self.paths, 1, p=self.probs)[0].step
return steps
Essentially, there are two different nodes, one for the player's decisions and one for chance. Crucially, all the methods for the search are in the class. It's a recursive search; at each decision node each player selects a move according to the UCT formula, that node has a dictionary of action tuples and points to a chance node. That chance node has a dict of decision nodes it points to, and it randomly samples a decision node. This continues deeper down the tree, until it hits a terminal node or an unexpanded decision node, where it expands and returns the rollout value back up the tree to the root.
Now this code is python slow but not slow. I've been timing how long it takes to do bathes of 3000 root to leaf traversals like above, and importantly it does not take any longer to do the tenth batch vs the first. You might expect it would take longer because of expansion of the tree, therefore deeper traversals. But in the case I gave it most of the traversals are hitting terminal nodes about 8 recursive calls in.
So why fix what's not broken? Well I noticed that my memory usage was exploding, probably because each node had its own copy of the methods attached. So, I rewrote the code so that both node classes only had statistics attached, and all the motion was provided by another Search class which contains all the methods. I also changed the provably ineffective UCT algorithm to a regret matching one. This is going to be slower, since its a bit more involved:
import numpy
import battle
import time
class Decision():
def __init__(self):
self.expanded = False
class Chance():
def __init__(self):
self.X = (0,0)
self.n = 0
class Search():
def __init__(self):
pass
def expandDecision(self, node, state):
node.actions = state.actions()
node.regrets = tuple([0 for a in _] for _ in node.actions)
node.strategies = tuple([0 for a in _] for _ in node.actions)
node.chances = dict()
for _ in range(len(node.actions[0])):
for __ in range(len(node.actions[1])):
node.chances[(_, __)] = Chance()
node.expanded = True
def expandChance(self, chance, state, moves):
chance.decisions = dict()
batch = state.apply(moves)
chance.steps = tuple(pair[0].step for pair in batch)
chance.p = tuple(pair[0].p for pair in batch)
def sampleChance(self, chance):
step = chance.steps[numpy.random.choice(range(len(chance.steps)), 1, p=chance.p)[0]]
if not step in chance.decisions.keys():
chance.decisions[step] = Decision()
return chance.decisions[step]
def getStrategy(self, node):
epsilon = 10**-3
s = [[max(__, epsilon) for __ in _] for _ in node.regrets]
t = [sum(_) for _ in s]
s = [[___/__ for ___ in _] for _, __ in zip(s, t)]
return s
def avgScore(self, chance):
return tuple((chance.X[_])/(chance.n + (chance.n == 0)) for _ in range(2))
def getRegrets(self, node, actions, u):
r = [[self.avgScore(node.chances[actions[:_] + (i,) + actions[_+1:]])[_] - u[_] for i in range(len(node.actions[_]))] for _ in range(2)]
r[0][actions[0]] = 0
r[1][actions[1]] = 0
return r
def sampleActions(self, strategy, gamma=0):
uniform = tuple([1/len(_)]*len(_) for _ in strategy)
fuzzy = tuple([(1-gamma)*x + (gamma)*y for x, y in zip(_, __)] for _, __ in zip(strategy, uniform))
return tuple(numpy.random.choice(range(len(_)), 1, p=_)[0] for _ in fuzzy)
def getMoves(self, node, actions):
return tuple(_[i] for i, _ in zip(actions, node.actions))
def updateDecision(self, node, r, s):
for _ in range(2):
for __ in range(len(node.actions[_])):
node.regrets[_][__] += r[_][__]
node.strategies[_][__] += s[_][__]
def updateChance(self, chance, u):
chance.X = tuple(chance.X[_] + u[_] for _ in range(2))
chance.n += 1
def averageStrategy(self, node):
return tuple(tuple(__/s for __ in _) for _, s in zip(node.strategies, map(sum, node.strategies)))
def search(self, node, state, gamma=0, depth=0):
if not node.expanded:
self.expandDecision(node, state)
return state.rollout()[0], depth, False
if not any(node.actions):
return state.rollout()[0], depth, True
sigma = self.getStrategy(node)
actions = self.sampleActions(sigma, gamma)
moves = self.getMoves(node, actions)
chance = node.chances[actions]
if not chance.n:
self.expandChance(chance, state, moves)
node_ = self.sampleChance(chance)
u, _, terminal = self.search(node_, state.apply(moves)[0][1], gamma, depth+1)
regrets = self.getRegrets(node, actions, u)
self.updateDecision(node, regrets, sigma)
self.updateChance(chance, u)
return self.avgScore(chance), _, terminal
if __name__ == '__main__':
node = Decision()
state = battle.s
__ = 3000
test = Search()
a = time.time()
total_depth = 0
terminal_count = 0
for _ in range(__ * 10):
test = Search()
u, depth, terminal = test.search(node, state, gamma=.03)
total_depth += depth
terminal_count += terminal
if (_+1)%__ == 0:
print(_+1)
for action, chance in node.chances.items():
print(node.actions)
print(action, chance.n, test.avgScore(chance))
print(test.averageStrategy(node))
#depth
avg_depth = total_depth/__
terminal_ratio = terminal_count/__
print('avg_depth: ', avg_depth)
print('terminal: ', terminal_ratio)
total_depth = 0
terminal_count = 0
#time
b = time.time()
print('t= ', b - a)
a = b
However this implementation is much slower. rather that a constant time (~20 sec) per batch, I have times of 20, 60, 100, 140 etc. Each batch of 3000 simulations is taking 40 seconds slower than the last!
I actually wrote a control case that uses the detached search of the later code and the UCT of the former and this still exhibits the slowdown effect. I thought maybe it was because I was reusing the same search object for all calls so I tried periodically re-instantiating it but to no avail.

Counting nodes in a binary tree recursively

I have to count nodes in a binary tree recursively. I'm new to python and can't find any solution for my problem to finish my code.
This is what I have already tried. As you can see it is not complete, and I can't figure out where to go.
class Tree:
def __init__(self, root):
self.root = root
def add(self, subtree):
self.root.children.append(subtree)
class Node:
def __init__(self, value, children=None):
self.value = value
self.children = children if children is not None else []
def check_root(tree):
if tree.root is None:
return 0
if tree.root is not None:
return count_nodes(tree)
def count_nodes(tree):
if tree.root.children is not None:
j = 0
for i in tree.root.children:
j = 1 + count_nodes(tree)
return j
print(count_nodes(Tree(None))) # 0
print(count_nodes(Tree(Node(10)))) # 1
print(count_nodes(Tree(Node(5, [Node(6), Node(17)])))) #3
With every new step I'm getting different error. E.g. with this code I have exceeded maximum recursion depth.
Thank you for your time reading this. Any hint or help what to do next would be greatly appreciated.
I would start by passing the root node to the count_nodes function -
print(count_nodes(Tree(None)).root) # 0
print(count_nodes(Tree(Node(10))).root) # 1
print(count_nodes(Tree(Node(5, [Node(6), Node(17)]))).root) #3
or make a helper function for that.
Then the count_nodes function can simply look like this
def count_nodes(node):
return 1 + sum(count_nodes(child) for child in node.children)
EDIT: I have just noticed, you can have a None root, this means, you should also handle that:
def count_nodes(node):
if node is None:
return 0
return 1 + sum(count_nodes(child) for child in node.children)
And if you really want to handle tree or node in one function, you can make it a bit uglier:
def count_nodes(tree_or_node):
if isinstance(tree_or_node, Tree):
return count_nodes(tree_or_node.root)
if tree_or_node is None:
return 0
return 1 + sum(count_nodes(child) for child in tree_or_node.children)
and then you can call it like you originally did.
Your problem is that you're counting the same tree infinitely. Take a look at this line:
j = 1 + count_nodes(tree)
An Easy Way:
Lets assume, A is a binary tree with children or nodes which are not NULL. e.g.
3
/ \
7 5
\ \
6 9
/ \ /
1 11 4
Now in order to count number of nodes, we have a simple workaround.
Recursive Method: >>> get_count(root)
For a binary tree, the basic idea of Recursion is to traverse the tree in Post-Order. Here, if the current node is full, we increment result by 1 and add returned values of the left and right sub-trees such as:
class TestNode():
def __init__(self, data):
self.data = data
self.left = None
self.right = None
Now we move forward to get the count of full nodes in binary tree by using the method below:
def get_count(root):
if (root == None):
return 0
res = 0
if (root.left and root.right):
res += 1
res += (get_count(root.left) +
get_count(root.right))
return res
At the end, in order to run the code, we'll manage a main scope:
Here we create our binary tree A as given above:
if __name__ == '__main__':
root = TestNode(3)
root.left = TestNode(7)
root.right = TestNode(5)
root.left.right = TestNode(6)
root.left.right.left = TestNode(1)
root.left.right.right = TestNode(4)
Now at the end, inside main scope we will print count of binary tree nodes such as:
print(get_Count(root))
Here is the time complexity of this recursive function to get_count for binary tree A.
Time Complexity: O(n)

TypeError between two instances of Vertices (nodes) in Dijkstra's SPF algorithm

I am currently working on solving a train schedule optimization problem as part of my studies. In this problem, a utility function has to be maximized which increases in the number of (critical) stations visited, and decreases in the amount of trains used and in the total amount of minutes the trains are running.
The problem consists of stations (nodes) and connections (edges). Data on both of these is first loaded from two CSV files. Then, classes are instantiated for each station (containing the name and whether or not it is critical), and each connection (containing the stations in the connection, and the time it costs to travel to one another). These stations and connection are both stored in dictionaries.
As a first step, my groupmates and I decided we first wanted to implement a version of Dijkstra's Pathfinding Algorithm in order to find the quickest route between two stations. BogoToBogo has a very detailed guide on how to implement a version of Dijkstra's algorithm. We decided first to try and implement their code to see what the results would be. However, a TypeError keeps popping up:
TypeError: '<' not supported between instances of 'Vertex' and 'Vertex'
If anyone has an idea what is causing this error, any help would be greatly appriciated!
#Makes the shortest path from v.previous
def shortest(v, path):
if v.previous:
path.append(v.previous.get_id())
shortest(v.previous, path)
return
def dijkstra(aGraph, start, target):
print('Dijkstras shortest path')
# Set the distance for the start node to zero
start.set_distance(0)
# Put tuple pair into the priority queue
unvisited_queue = [(v.get_distance(),v) for v in aGraph]
heapq.heapify(unvisited_queue)
while len(unvisited_queue):
# Pops a vertex with the smallest distance
uv = heapq.heappop(unvisited_queue)
current = uv[1]
current.set_visited()
#for next in v.adjacent:
for next in current.adjacent:
# if visited, skip
if next.visited:
continue
new_dist = current.get_distance() + current.get_weight(next)
if new_dist < next.get_distance():
next.set_distance(new_dist)
next.set_previous(current)
print('updated : current = ' + current.get_id() + ' next = ' + next.get_id() + ' new_dist = ' + next.get_distance())
else:
print('not updated : current = ' + current.get_id() + ' next = ' + next.get_id() + ' new_dist = ' + next.get_distance())
# Rebuild heap
# 1. Pop every item
while len(unvisited_queue):
heapq.heappop(unvisited_queue)
# 2. Put all vertices not visited into the queue
unvisited_queue = [(v.get_distance(),v) for v in aGraph if not v.visited]
heapq.heapify(unvisited_queue)
if __name__ == "__main__":
# Calling the CSV loading functions in mainActivity
# These functions will also instantiate station and connections objects
load_stations(INPUT_STATIONS)
load_connections(INPUT_CONNECTIONS)
g = Graph()
for index in stations:
g.add_vertex(stations[index].name)
for counter in connections:
g.add_edge(connections[counter].stat1, connections[counter].stat2, int(connections[counter].time))
for v in g:
for w in v.get_connections():
vid = v.get_id()
wid = w.get_id()
print( vid, wid, v.get_weight(w))
dijkstra(g, g.get_vertex('Alkmaar'), g.get_vertex('Zaandam'))
target = g.get_vertex('Zaandam')
path = [target.get_id()]
shortest(target, path)
print('The shortest path :' + (path[::-1]))
In this case, the function dijkstra is called, given the parameters g (which is a instance of the Graph class), Alkmaar, and Zaandam.
# Represents a grid of nodes/stations composed of nodes and edges
class Graph:
def __init__(self):
self.vert_dict = {}
self.num_vertices = 0
def __iter__(self):
return iter(self.vert_dict.values())
def add_vertex(self, node):
self.num_vertices = self.num_vertices + 1
new_vertex = Vertex(node)
self.vert_dict[node] = new_vertex
return new_vertex
def get_vertex(self, n):
if n in self.vert_dict:
return self.vert_dict[n]
else:
return None
def add_edge(self, frm, to, cost = 0):
if frm not in self.vert_dict:
self.add_vertex(frm)
if to not in self.vert_dict:
self.add_vertex(to)
self.vert_dict[frm].add_neighbor(self.vert_dict[to], cost)
self.vert_dict[to].add_neighbor(self.vert_dict[frm], cost)
def get_vertices(self):
return self.vert_dict.keys()
def set_previous(self, current):
self.previous = current
def get_previous(self, current):
return self.previous
The Graph class.
# Represents a node (station)
class Vertex:
def __init__(self, node):
self.id = node
self.adjacent = {}
# Set distance to infinity for all nodes
self.distance = sys.maxsize
# Mark all nodes unvisited
self.visited = False
# Predecessor
self.previous = None
def add_neighbor(self, neighbor, weight=0):
self.adjacent[neighbor] = weight
def get_connections(self):
return self.adjacent.keys()
def get_id(self):
return self.id
def get_weight(self, neighbor):
return self.adjacent[neighbor]
def set_distance(self, dist):
self.distance = dist
def get_distance(self):
return self.distance
def set_previous(self, prev):
self.previous = prev
def set_visited(self):
self.visited = True
def __str__(self):
return str(self.id) + ' adjacent: ' + str([x.id for x in self.adjacent])
The Vertex class.
Thanks for your time!
I think this might help, but the way when posting to stackoverflow just post as little and complete information as possible
# Put tuple pair into the priority queue
unvisited_queue = [(v.get_distance(),v) for v in aGraph]
heapq.heapify(unvisited_queue)
if you look at this code it converts the list to a heap which requires < comparison of whatever you give to it, define a __gt__() method in the vertex class , the function will determine what gets popped first so write it as you see fit and I think the error will go away. :-)

A* algorithm implementation node-parent bug

I have a problem with my attempt at A* - this does not happen every time it runs, but every so often it makes it so two cells become each other's parents.
This in turn leads to an infinite loop when I try to recreate the path. I've been trying to find out why this happens for some time now, but can't figure it out, so any help would be appreciated.
def find_path(self, start, end):
tstart = time.time()
count = 0
evaled = set()
notEvaled = set([start])
start.g_score(start)
start.h_score(end)
start.f_score()
while len(notEvaled) != 0:
current = min(notEvaled, key=attrgetter('f'))
notEvaled.remove(current)
for neighbour in current.neighbours:
if neighbour.full or neighbour in evaled: continue
if neighbour in notEvaled:
if neighbour.parent != None and neighbour.parent.g > current.g:
neighbour.parent = current
neighbour.g_score(start)
neighbour.f_score()
else:
neighbour.parent = current
neighbour.g_score(start)
neighbour.h_score(end)
neighbour.f_score()
notEvaled.add(neighbour)
if neighbour == end:
path = []
while end != None:
path.insert(0, end)
end = end.parent
return path
evaled.add(current)
return None
Here are my score function, but i doubt they matter
def g_score(self, start):
if self == start:
self.g = 0
else:
self.g = self.parent.g + 1
def h_score(self, end):
self.h = abs(end.x - self.x) + abs(end.y - self.y)
def f_score(self):
self.f = self.g + self.h
Before I begin: the common naming convention is openSet/closedSet instead of evaled/notEvaled. The naming convention you use becomes confusing when you need to use a double negative, as in: aNode not in notEvaled. So, in my answer I use the openSet/closedSet convension.
One thing that stands out to me is you add the current node to the closedSet (evaled) immediately after removing it from the openSet (notEvaled).
The second thing that stands out is that you check whether a node is the endpoint before adding it to the openSet. While this may seem like an optimization, it actually forces you to perform the check on every single opened node, rather than on the subset of open nodes that are actually visited.
Here is an adaptation of your A* algorithm that should work as expected. It is based on the pseudo code from Wikipedia except that it integrates the reconstruct_path method:
def find_path(self, start, end):
tstart = time.time()
count = 0
closedSet = set()
openSet = set([start])
start.g_score(start)
start.h_score(end)
start.f_score()
while len(openSet) != 0:
current = min(openSet, key=attrgetter('f'))
openSet.remove(current)
closedSet.add(current)
#Check if goal is reached
if(current == end):
#Construct path from start to goal
path = []
while end != None:
path.insert(0,end)
end = end.parent
return path
#Open neighbors
for neighbour in current.neighbours:
if neighbour.full: continue
if neighbour in closedSet: continue
tentative_g_score = 1 + current.g
if neighbour not in openSet or tentative_g_score < neighbor.g:
neighbour.parent = current
neighbour.g = tentative_g_score
neighbour.f_score()
if neighbour not in openSet:
openSet.add(neighbour)
return None

Optimize finding diameter of binary tree in Python

I'm wondering how I can optimally find the diameter (or longest path between any two leaf nodes) of a binary tree. I have the basic solution below, but the second solution requires passing pointers. How can I do something like this in Python?
def find_tree_diameter(node):
if node == None:
return 0
lheight = height(node.left)
rheight = height(node.right)
ldiameter = find_tree_diameter(node.left)
rdiameter = find_tree_diameter(node.right)
return max(lheight+rheight+1, ldiameter, rdiameter)
def find_tree_diameter_optimized(node, height):
lheight, rheight, ldiameter, rdiameter = 0, 0, 0, 0
if node == None:
# *height = 0;
return 0
ldiameter = diameterOpt(root.left, &lheight)
rdiameter = diameterOpt(root.right, &rheight)
# *height = max(lheight, rheight) + 1;
return max(lh + rh + 1, max(ldiameter, rdiameter));
Python supports multiple return values, so you don't need pointer arguments like in C or C++. Here's a translation of the code:
def diameter_height(node):
if node is None:
return 0, 0
ld, lh = diameter_height(node.left)
rd, rh = diameter_height(node.right)
return max(lh + rh + 1, ld, rd), 1 + max(lh, rh)
def find_tree_diameter(node):
d, _ = diameter_height(node)
return d
The function diameter_height returns the diameter and the height of the tree, and find_tree_diameter uses it to just compute the diameter (by discarding the height).
The function is O(n), no matter the shape of the tree. The original function is O(n^2) in the worst case when the tree is very unbalanced because of the repeated height calculations.
Simple Python 3 solution
def findDepth(root):
if root is None:
return 0
return 1 + max(findDepth(root.left), findDepth(root.right))
class Solution:
def diameterOfBinaryTree(self, root: TreeNode) -> int:
if root is None:
return 0
left = findDepth(root.left)
right = findDepth(root.right)
ldia = self.diameterOfBinaryTree(root.left)
rdia = self.diameterOfBinaryTree(root.right)
return max(left+right, max(ldia, rdia))

Categories

Resources