I am using DFS to get all routes between two nodes.
My python code is as follows:
graph = {0: [1, 2, 3],
1: [3],
2: [0, 1],
3: []}
def DFS(start, stop, path=[], visited=[]):
global count
global result
# add the visited node to path
path.append(start)
# mark this node visited to avoid infinite loop
visited.append(start)
# found
if start == stop:
print(path)
else:
# if not found
values = graph.get(start)
for next_ in values:
# not visited node
if not next_ in visited:
DFS(next_, stop, path, visited)
# remove the node from path and unmarked it
path.remove(start)
visited.remove(start)
The problem is that if I print path in if start == stop, all 3 routes can be printed correctly.
>>> DFS(2, 3)
[2, 0, 1, 3]
[2, 0, 3]
[2, 1, 3]
But if I change to return path in if start == stop, it would return nothing.
def DFS(start, stop, path=[], visited=[]):
global count
global result
# add the visited node to path
path.append(start)
# mark this node visited to avoid infinite loop
visited.append(start)
# found
if start == stop:
return path
else:
# if not found
values = graph.get(start)
for next_ in values:
# not visited node
if not next_ in visited:
DFS(next_, stop, path, visited)
# remove the node from path and unmarked it
path.remove(start)
visited.remove(start)
>>> result = DFS(2, 3)
>>> result
But if I change to return path in if start == stop, it would return nothing.
Right; because you got to this level of recursion from the previous one, which recursively called DFS(next_, stop, path, visited)... and ignored the result.
It is the same as if you called functions normally:
def inner():
return "hello"
def outer():
inner() # oops, it is not returned.
print(outer()) # None
In general you want to return the results from your recursive calls; but your case is a little special because you need to accumulate the results from multiple recursive calls (for next_ in values:). You could build a list and return it, but this is a bit tricky:
if start == stop:
result = [path] # for uniformity, we need a list of paths in this case too.
# Also, we can't `return` here, because we'll miss the cleanup at the end.
else:
result = []
values = graph.get(start)
for next_ in values:
# BTW, Python treats `not in` as a single operator that does
# what we want here. It's preferred because it's easier to read.
if next_ not in visited:
# add results from the recursive call to our result.
result.extend(DFS(next_, stop, path, visited))
# it is `.extend` and not `.append` here because otherwise we will
# build a tree of nested lists - do you understand why?
# Either way, we want to do our cleanup, and return the collected result.
path.remove(start)
visited.remove(start)
return result # important!
Tricky, right?
My preferred solution for these situations, therefore, is to write a recursive generator, and collect the results outside the recursion:
# Inside the function, we do:
if start == stop:
yield path
else:
values = graph.get(start)
for next_ in values:
if next_ not in visited:
yield from DFS(next_, stop, path, visited))
path.remove(start)
visited.remove(start)
# Then when we call the function, collect the results:
paths = list(DFS(2, 3))
# Or iterate over them directly:
for path in DFS(2, 3):
print("For example, you could take this route:", path)
(Also, the comment you received was good advice. Recursion is a lot easier to understand when you don't try to mutate the arguments and clean up afterwards. Instead, always pass those arguments, and when you make the recursive call, pass a modified version. When the recursion returns, cleanup is automatic, because you just go back to using the old object in the old stack frame.
The problem with your code is that
result=DFS(3,2)
will only return a valid result if start=stop which is not the case as 3!=2.
To get the desired output you have to change the line
DFS(next_,stop,path,visited)
to
return DFS(next_,stop,path,visited)
Now whenever start gets equal to stop the path will be returned and this value will be propogated upwards
Related
This is an 8 puzzle and uses bfs and dfs to solve find the solution and prints out the path to the goal. I am having trouble poping and appending the children so that it can find the solution. My error is that it will only print out the two options and does not branch out from the possible solution. The terminal is still going despite not printing out anything.
Here is my code and on the bottom is a test case.
import copy
#This is the only file you need to work on. You do NOT need to modify other files
# Below are the functions you need to implement. For the first project, you only need to finish implementing bfs() and dfs()
#here you need to implement the Breadth First Search Method
def bfs(puzzle):
list = []
#initialization
state = copy.deepcopy(puzzle)
goal = [0,1,2,3,4,5,6,7,8]
possible_move = [[1,3],[0,2,4],[1,5],[0,4,6],[1,3,5,7],[2,4,8],[3,7],[4,6,8],[5,7]]
#appending the first state
queue = []
queue = [Node(state)]
for node in queue[:]:
print('the state of this game position is:\n ' + str(node.state))
loop = True
notFound = True
l = 0
while loop:
for node in queue:
#blank index in each state
blank = node.state.index(8)
print('the index of the blank is '+ str(blank))
#The possible position
possible_pos = possible_move[blank]
print('possible pos '+ str(possible_pos))
if state != goal:
for i in possible_pos:
possible_sw = copy.deepcopy(node.state)
print('index swap = '+ str(i))
temp = possible_sw[i]
possible_sw[i] = 8
possible_sw[blank] = temp
print('the child nodes is ' + str(possible_sw))
node.insertChild(possible_sw)
if possible_sw == goal:
print('end')
notFound = False
loop = False
#check each child and find the goal state
for node in queue[:]:
for child_state in node.children:
if child_state == [0,1,2,3,4,5,6,7,8]:
final_state = child_state
print('the final state is '+ str(final_state.state))
queue.pop(0)
#find the parent path
while node.parent and loop is False:
sol_path = final_state.state
list.append(sol_path.index(8))
if final_state.parent is not None:
final_state = final_state.parent
else:
parent = False
list.reverse()
list.pop(0)
print('moves list '+ str(list))
return list
#here you need to implement the Depth First Search Method
def dfs(puzzle):
list = []
return list
#This will be for next project
def astar(puzzle):
list = []
return list
def swap(list, pos1, pos2):
list[pos1],list[pos2] = list[pos2], list[pos1]
return list
class Node:
def __init__(self,state,parent = None):
self.parent = parent
self.state = state
self.children = []
def insertChild(self, child_state):
self.children.append(Node(child_state,self))
#test cases
# p =[0, 1, 2, 3, 4, 5, 8, 6, 7]
p = [0, 1, 2, 3, 4, 5, 6, 8, 7]
#p = [0, 1, 2, 3, 8, 4, 6, 7, 5]
#p =[0, 4, 1, 3, 8, 2, 6, 7, 5]
bfs(p)
print("+++++++++++++++++++++")
#dfs(p)
There are several issues with your attempt:
The queue never receives more entries; queue.append is never called. On the other hand, the inner loop over queue[:] empties the queue with pop, removing its only element. And from that moment on the queue remains empty.
The Node constructor is called only once, so there will never be more than one node, and the test node.parent will always be false, making the last while loop useless, and the moveslist (if any) will never be printed
If the end is not found in the first iteration -- meaning the initial position is not one move away from the goal -- the outer loop will get into an infinite loop: on its second iteration the queue is empty, so there is nothing to do, and the loop name will never become True.
if state != goal makes little sense as the state name never changes in the loop. If anything, this should reference node.state, not state.
The list.pop(0) at the very end unnecessarily removes a move. The loop condition already checks if the node has a parent -- so skipping the root state -- so then you'll miss two states.
The code does not check whether the initial position is maybe the goal position, and so it will not return an empty move list as solution when this is the case.
Some other remarks:
swap is never called.
The code has several names which serve no purpose, like l, notFound.
It uses list which is a native name in Python -- choose a different name.
The children attribute of the Node instances is not useful. Although you iterate it to find the final state, the logic for identifying whether the goal was reached, is already present elsewhere in the code... it doesn't need children.
deepcopy is not needed: the lists you use are not "deep". You can simply copy them by applying the list (native) function, or slicing them with [:].
Assigning twice to queue in sequence (in the initialisation) makes the first assignment useless. Just have the second assignment.
The loop in the initialisation part should not be a loop. At that moment you know the queue has only one entry, so iterating it is overkill. You can just output the initial state using puzzle. But maybe you wanted to output the state in the loop...
There is no need for temp to perform a swap. First of all, Python can do tuple assignment, but also: a move is not really a swap of two unknown values: you know that one of the two is the empty cell (8), so you can safely overwrite that cell with the other cell's value, and then set the other cell's value to 8 -- no temp is needed.
Here is your code corrected with the above remarks:
class Node:
def __init__(self,state,parent = None):
self.parent = parent
self.state = state
def bfs(puzzle):
solution = []
#initialization
goal = [0,1,2,3,4,5,6,7,8]
possible_move = [[1,3],[0,2,4],[1,5],[0,4,6],[1,3,5,7],[2,4,8],[3,7],[4,6,8],[5,7]]
node = Node(puzzle)
queue = [node]
while True:
node = queue.pop(0)
print('the state of this game position is:\n ' + str(node.state))
if node.state == goal:
break
blank = node.state.index(8)
print('the index of the blank is '+ str(blank))
possible_pos = possible_move[blank]
print('possible pos '+ str(possible_pos))
for i in possible_pos:
possible_sw = node.state[:]
print('index swap = '+ str(i))
possible_sw[blank] = possible_sw[i]
possible_sw[i] = 8
print('the child node is ' + str(possible_sw))
queue.append(Node(possible_sw, node))
while node.parent:
solution.append(node.state.index(8))
node = node.parent
solution.reverse()
print('moves list '+ str(solution))
return solution
The example below is from a source online and what I'm not sure of is why we need to append to allPaths a new copy of currentPath. I thought that in deleting the last element as we go back up the recursive call stack by doing del currentPath[-1] makes sure that we are not adding the previous path to the new path
class TreeNode:
def __init__(self, val, left=None, right=None):
self.val = val
self.left = left
self.right = right
def find_paths(root, required_sum):
allPaths = []
find_paths_recursive(root, required_sum, [], allPaths)
return allPaths
def find_paths_recursive(currentNode, required_sum, currentPath, allPaths):
if currentNode is None:
return
# add the current node to the path
currentPath.append(currentNode.val)
# if the current node is a leaf and its value is equal to required_sum, save the current path
if currentNode.val == required_sum and currentNode.left is None and currentNode.right is None:
allPaths.append(list(currentPath))
else:
# traverse the left sub-tree
find_paths_recursive(currentNode.left, required_sum -
currentNode.val, currentPath, allPaths)
# traverse the right sub-tree
find_paths_recursive(currentNode.right, required_sum -
currentNode.val, currentPath, allPaths)
# remove the current node from the path to backtrack,
# we need to remove the current node while we are going up the recursive call stack.
del currentPath[-1]
def main():
root = TreeNode(12)
root.left = TreeNode(7)
root.right = TreeNode(1)
root.left.left = TreeNode(4)
root.right.left = TreeNode(10)
root.right.right = TreeNode(5)
required_sum = 23
print("Tree paths with required_sum " + str(required_sum) +
": " + str(find_paths(root, required_sum)))
main()
It is important to realise that during the whole process there is only one currentPath list. It is the one that is created in the initial call:
find_paths_recursive(root, required_sum, [], allPaths)
# ^^---here!
All that happens to that single list is that elements are appended to it, and then deleted from it again (when backtracking). It continually grows and shrinks, grows and shrinks,... throughout its lifetime. But it is the same, single list instance.
If you were to append that list to allPaths without taking a copy, i.e. like this:
allPaths.append(currentPath)
...then realise that while that list is a member of allPaths, it will be mutated by future append and delete actions on it! Even more: as the above statement is executed again later on:
allPaths.append(currentPath)
... exactly the same list is appended that is already in allPaths... because there is only one currentPath list! So you'll end up with allPaths having repeated references to one and the same list.
Concluding: it is important to take a copy of currentPath which will not be mutated any more by the future mutations on currentPath. It is like taking a snapshot of the current state of currentPath.
The find_paths_recursive function was designed such that appending to allPaths is the way to return the results to the caller.
def find_paths(root, required_sum):
allPaths = []
find_paths_recursive(root, required_sum, [], allPaths)
return allPaths
Here in find_paths, allPaths is passed to find_paths_recursive as an empty list and after it is done, it will contain the results (paths from root to leaf which fulfill the described condition).
I would recommend breaking the problem down into separate parts. First we write a generic paths function -
def paths (t = None, p = ()):
if not t:
return
elif t.left or t.right:
yield from paths(t.left, (*p, t.val))
yield from paths(t.right, (*p, t.val))
else:
yield (*p, t.val)
mytree = TreeNode \
( 12
, TreeNode(7, TreeNode(4))
, TreeNode(1, TreeNode(10))
)
Now we can see how paths works -
for p in paths(mytree):
print(p)
(12, 7, 4)
(12, 1, 10)
Now we can write solver that specializes paths -
def solver (t = None, q = 0):
for p in paths(t):
if sum(p) == q:
yield p
solver is a generator which yields all possible solutions. It is a good choice for programs like this because you can pause/cancel the solving as soon as you found the solution(s) you are looking for -
for sln in solver(mytree, 23):
print(sln)
The output isn't particularly interesting because each branch in mytree sums to 23 -
(12, 7, 4)
(12, 1, 10)
If we make anothertree with different values, we can see more interesting output -
anothertree = TreeNode \
( 1
, TreeNode(7, TreeNode(4), TreeNode(5))
, TreeNode(9, TreeNode(2), TreeNode(7))
)
for sln in solver(anothertree, 12):
print(sln)
(1, 7, 4)
(1, 9, 2)
I have the following directed graph and every node has one or more attributes. I try to modify bfs algorithm to find all possible paths from a starting node until the given attributes are covered. I also want the path that i found to not be a part of a cycle.
For this graph if i start from node 1 and i want to cover attr 4 the paths that my algorithm will find are:
1-2-3
1-2-5-3
1-2-5-6-8
If i add the edge 3-1 then the paths 1-2-3 and 1-2-5-3 i want not to be accepted because are part of a cycle. So in my algorithm i try to check the neighbors of the last visited node and if the neighbor has already visited then i try to discard this path but my algorithm doesnt work in this case. If i add the edge 3-1 it returns the same paths. How can i fix this?
Here is my code:
G = nx.DiGraph()
G.add_edge(1,2)
G.add_edge(2,3)
G.add_edge(2,5)
G.add_edge(3,4)
G.add_edge(5,3)
G.add_edge(5,6)
G.add_edge(5,7)
G.add_edge(6,8)
G.add_edge(3,1)
def checkIfRequiredAttrsAreCovered(path, attrsToBeCovered):
coveredAttrs = []
counter = 0
for node in path:
coveredAttrs.extend(G.node[node]['attrs'])
for i in attrsToBeCovered:
if i in coveredAttrs:
counter = counter + 1
if counter == len(attrsToBeCovered):
return True
else:
return False
def bfs(G, startingNode, attrsToBeCovered):
paths = []
q = queue.Queue()
q.put([startingNode])
while not q.empty():
v = q.get()
if checkIfRequiredAttrsAreCovered(v, attrsToBeCovered) == True:
for i in G.neighbors(v[-1]):
if i in v:
break
paths.append(v) #print(v)
else:
for node in G.neighbors(v[-1]):
if node not in v:
path = []
path.extend(v)
path.append(node)
q.put(path)
print(paths)
I'll assume that you don't care if nodes are part of a bigger cycle. E.g. if 4 is connected to 1 and 3 is in a cycle 1-2-3-4. If you want to handle this, you may start a dfs from each matching node, with the current path set as visited.
First, you should use snake case in Python
Second, you should use sets to compare the attributes covered to the attributes to be covered. For a path, compute the set of covered attributes and compare the sets:
def check_if_required_attrs_are_covered(G, path, attrs_to_be_covered): # be sure to pass G as an argument here
covered_attrs = set([G.node[n]['attrs'] for n in path])
return covered_attrs >= attrs_to_be_covered
Third, some remarks on the bfs function:
A test if b == True: is equivalent to if b:, because for a boolean b == (b == True) (try with True and False to convince yourself)
The way you append a path to q may be shortened to q.put(v+ [node])
You probably do not need a synchonized queue: use a list
Use return instead of print or even better, create a generator that yields paths when they are found.
Four: what is the problem? Look at the for i in G.neighbors(v[-1]): loop.
Whether you break or not, you go to the line paths.append(v).
That's why you do not exclude the paths with cycles. You want to distinguish the normal end of the loop from the break.
That's a perfect case for confidential loop syntax in Python: the for...else loop.
I quote the doc: "a loop’s else clause runs when no break occurs". This gives the following code:
for i in G.neighbors(v[-1]):
if i in v:
break
else: # no neighbor from v[-1] in v
yield v # instead of paths.append(v)
But you can also use any for a more natural expression:
if not any(i in v for i in G.neighbors(v[-1])):
yield v # instead of paths.append(v)
This gives the following code:
def bfs(G, starting_node, attrs_to_be_covered):
q = [[starting_node]]
while q:
v = q.pop()
if check_if_required_attrs_are_covered(G, v, attrs_to_be_covered): # be sure to pass G as an argument
if not any(i in v for i in G.neighbors(v[-1])):
yield v
else:
for node in G.neighbors(v[-1]):
if node not in v:
q.append(v+ [node])
Try it with:
print (list(bfs(G, 1, set(["attr4"]))))
I have an A* search algorithm which crashes the program because of a memory error, and I don't know why. These are the relevant bits of code:
def __init__(self, graph):
self.graph = graph
def search(self, start, end):
openset = set()
closedset = set()
current = start
openset.add(current)
while openset:
print current
current = min(openset, key=lambda o:o.g + o.h)
if current == end:
path = []
while current.parent:
path.append(current)
current = current.parent
path.append(current)
return path[::-1]
openset.remove(current)
closedset.add(current)
for node in self.graph[current]:
if node in closedset:
continue
if node in openset:
new_g = current.g + current.move_cost(node)
if node.g > new_g:
node.g = new_g
node.parent = current
else:
node.g = current.g + current.move_cost(node)
node.h = self.heuristic(node, start, end)
node.parent = current
openset.add(node)
return None
The graph passed to it is generated at the start of the program:
def make_graph(self, size, impassable):
nodes = [[astar_gridnode(x, y) for y in range(size)] for x in range(size)]
graph = {}
for x, y in product(range(size), range(size)):
node = nodes[x][y]
graph[node] = []
for i, j in product([-1, 0, 1], [-1, 0, 1]):
# Check that we are inside the grid area.
if not (0 <= x + i < size): continue
if not (0 <= y + j < size): continue
# Check if the target area is impassable.
if (x + i, y + j) in impassable: continue
# All looks good. Add target space as reachable from current (x, y) space.
graph[nodes[x][y]].append(nodes[x+i][y+j])
return graph, nodes
And here is how the search is called:
def find_path(self, agent, target_coords, impassable, graph, nodes):
paths = astar_grid(graph)
start = nodes[agent.grid_pos[0]][agent.grid_pos[1]]
end = nodes[target_coords[0]][target_coords[1]]
path = paths.search(start, end)
This all works like it's supposed to the first time a search is done, and it works if a search is done with start, end variables and a path which doesn't intersect the previous path. It also works if a new graph is generated before each search, but that's not possible because the graph object is huge and causes the program to freeze for a couple seconds when it's being created.
If a search is made which intersects with a previous path the program freezes for a minute, and I get this error:
File "C:\...\pathfinding.py", line 16, in find_path
path = paths.search(start, end)
File "C:\...\astar.py", line 19, in search
current = current.parent
MemoryError
What is the reason for the crash and how can we fix it? I don't understand why it would crash, as it seems to me that the original graph is not modified in a search, and that a new search object is created each time a search is called, which leaves me mystified as to why it works when it works, and crashes when it does.
I agree with hughdbrown — you very likely have a cycle in the parent chain, and printing out current immediately before you assign current = current.parent is likely to tell you whether this is true.
You say that the original graph is not modified in a search, but it is. You are modifying the .parent pointer. At first, all the .parent pointers are set to None, but after you've run the search, some of them are non-None. Since it should be None and it isn't, the while current.parent: condition is failing to find the end of the path, and it's branching off into previously computed paths.
Try setting start.parent = None at the beginning of the search. Or clear the parent pointers after the search finishes (more expensive but cleaner) (you only have to clear them for things in openset and closedset).
I have a homework in python, where i am supposed to make a "robot" get from the start to the finish, and return the path to the goal. I have the robot searching, but i want the list to just show the path from start to finish. Right now the pathList returns all visited squares.. and also, when it comes to the goal it doesn't stop, just continue with the other nodes.
What am i missing?
def find(labyrinth, robotPos, pathList = []):
frontier = adjacent_passages(labyrinth, robotPos)
pathList.append(robotPos)
if len(frontier) == 1:
print("Corner")
return []
for i in range(0, len(frontier)):
if frontier[i] == goal:
pathList.append(frontier[i])
return pathList
for i in range(0, len(frontier)):
if frontier[i] not in pathList:
pathList.append(frontier[i])
if (find(labyrinth, frontier[i], pathList) == []):
pathList.pop()
return pathList
I don't know if this is the answer to your problem, but the first thing I notice is you shouldn't be using a list as a default function parameter (pathList = []).
See "Least Astonishment" and the Mutable Default Argument
You're appending robotPos to pathList without removing it even if the search fails, that's why the list contains all visited positions. I'd suggest to avoid list mutations (append/pop) altogether and instead pass a new value as an argument to the next iteration:
if (find(labyrinth, frontier[i], pathList + [frontier[i]]) == [])...
Other possible simplifications are:
drop robotPos. Let the current position be the last item of the path argument
use one loop instead of two
in loops, use for x in stuff instead of for i in range(0, len(stuff))
Something like
def find(path):
if path[-1] == goal:
return path
for new_position in adjacent_positions(path[-1]):
if new_position not in path:
found_path = find(path + [new_position])
if found_path:
return found_path
return None