Simple Artificial Intelligence Search Problem Infinite Loop Bug in Python - python

I am working on Project 0 for Harvard's CS50 Artificial Intelligence class and we had to write a function called shortest_path which is used in the program in finding the shortest path between two actors in a movie database based on shared movies they have worked on with other actors. I did write the function but am getting an infinite loop and can't figure out why I am getting it. I followed the directions from lecture where it says to only add a node to the frontier if the node does not exist in the frontier and also does not exist in previously explored nodes. Overlooking this is something that could lead to an infinite loop but I am still getting one. The test case I used is when the name of the source actor is Kevin and target actor is Kevin. I just pick two different Kevins from the list that's provided after entering Kevin as sources first and targets first name. I am posting my shortest_path function and a link where the rest of the project files can be downloaded with the relevant CSV files for the data being used to test the project.
Link to project files: Download the distribution code from https://cdn.cs50.net/ai/2020/x/projects/0/degrees.zip and unzip it.
The only function we had to modify was the shortest_path function in the distribution code provided so that's all I am posting, along with the classes for the node and Queuefrontier data structure being used. The rest of the code is in the distribution files in case you need to see how it all fits into place.
My code for shortest_path function:
def shortest_path(source, target):
"""
Returns the shortest list of (movie_id, person_id) pairs
that connect the source to the target.
If no possible path, returns None.
"""
path = []
# Keep track of number of states explored
num_explored = 0
# Initialize frontier to just the starting position
start = Node(state=source, parent=None, action=None)
frontier = QueueFrontier()
frontier.add(start)
# Initialize an empty explored set
explored = set()
# Keep looping until solution found
counter = 0
while True:
print(counter)
counter = counter + 1
# If nothing left in frontier, then no path
if frontier.empty():
return None
# Choose a node from the frontier
node = frontier.remove()
num_explored += 1
# If node is the goal, then we have a solution
if node.state == target:
actions = []
cells = []
path=[]
while node.parent is not None:
#actions.append(node.action)
#cells.append(node.state)
path.append(node.action,node.state)
node = node.parent
#actions.reverse()
#cells.reverse()
path.reverse()
#solution = (actions, cells)
return path
# Mark node as explored
explored.add(node.state)
# Add neighbors to frontier
for action, state in neighbors_for_person(node.state):
if not frontier.contains_state(state) and state not in explored:
child = Node(state=state, parent=node, action=action)
frontier.add(child)
Code for Class Node and QueueFrontier:
class Node():
def __init__(self, state, parent, action):
self.state = state
self.parent = parent
self.action = action
def toString(self):
print(f"State={self.state}, Parent={self.parent}")
class StackFrontier():
def __init__(self):
self.frontier = []
def add(self, node):
self.frontier.append(node)
def contains_state(self, state):
return any(node.state == state for node in self.frontier)
def empty(self):
return len(self.frontier) == 0
def remove(self):
if self.empty():
raise Exception("empty frontier")
else:
node = self.frontier[-1]
self.frontier = self.frontier[:-1]
return node
class QueueFrontier(StackFrontier):
def remove(self):
if self.empty():
raise Exception("empty frontier")
else:
node = self.frontier[0]
self.frontier = self.frontier[1:]
return node

Related

Leetcode 133. Clone Graph with dfs understanding

Hi I have a problem understanding dfs. What I know is DFS has two versions; we mark visited before call and after call.
def solution1(start):
def dfs1(cur):
for nei in cur.neighbors:
if nei not in visited:
## mark visit before call
visited.add(nei)
dfs1(nei)
## drive dfs1
visited = set()
visited.add(start)
dfs1(start)
def solution2(start):
def dfs2(cur):
## mark visit after call
visited.add(cur)
for nei in cur.neighbors:
if nei not in visited:
dfs2(nei)
## drive dfs2
dfs2(start)
However, when I applied version1 (mark visited before a call) to the problem(https://leetcode.com/problems/clone-graph/), it complained and did not copy.
This is my solution:
"""
# Definition for a Node.
class Node:
def __init__(self, val = 0, neighbors = None):
self.val = val
self.neighbors = neighbors if neighbors is not None else []
"""
class Solution:
"""
def dfs1(cur):
for nei in cur.neighbors:
if nei in visited: continue
visited.add(nei)
dfs1(nei)
visited.add(start_node)
dfs1(start_node)
"""
def dfs(self, cur, visited):
new_node = Node(cur.val)
# visited[cur.val] = new_node
new_neighbors = []
for nei in cur.neighbors:
if nei.val not in visited:
visited[nei.val] = nei
new_neighbors.append(self.dfs(nei, visited))
else:
new_neighbors.append(visited[nei.val])
new_node.neighbors = new_neighbors
return new_node
def cloneGraph(self, node: 'Node') -> 'Node':
if node == None:
return None
visited = {}
visited[node.val] = node
return self.dfs(node, visited)
Let me know why this has problem. I don't understand why it does not work.
Main reason that your code doesn't work is because you store original node inside visited dictionary. You have to store processed DFS clone of a node inside visited. Why? Because inside new_neighbors.append(visited[nei.val]) you add visited node to neighbours of new cloned node. But any cloned node should have only cloned neighbours, not originals.
I decided to implement my own version of your algorithm/idea using DFS, next code is successfully accepted by LeetCode system (all tests pass):
class Solution:
def __init__(self):
self.visited = {}
def cloneGraph(self, node: 'Node') -> 'Node':
if node is None:
return None
if node.val in self.visited:
return self.visited[node.val]
nnode = Node(val = node.val)
self.visited[nnode.val] = nnode
nnode.neighbors = [self.cloneGraph(c) for c in node.neighbors]
return nnode
We use the hash map to create a map from original nodes to copied nodes. Then we use the hash map to check if we visit the node before or not.
def cloneGraph(self,node):
# I used closure instead of passing this object
visited={}
def dfs(node):
if node in visited:
# this returned value will be used inside for loop copy.neighbors.append(dfs(nei))
return visited[node]
#if node is not in visited we add it
# this is the first part of the copy. first copy the val
copy=Node(node.val)
visited[node]=copy
# we create the neighbors array of "copy" node
for nei in node.neighbors:
copy.neighbors.append(dfs(nei))
return copy
return dfs(node) if node else None

How can I reference a list initialized in a function? (Python)

The error I'm receiving is local variable 'actions' referenced before assignment. I have a feeling that this is because I am creating the list within the function and then trying to reference it within. This is the specific code from which I receiving the error.
if node.state == target:
actions: []
cells: []
while node.parent is not None:
actions.append(node.action)
cells.append(node.state)
node = node.parent
This is more context for the origin of the code I am writing.
def shortest_path(source, target):
"""
Returns the shortest list of (movie_id, person_id) pairs
that connect the source to the target.
If no possible path, returns None.
"""
# Create a frontier
frontier = QueueFrontier()
# Initialise explored set
explored = set()
# Create a start Node
node = Node(state=source, parent=None, action=None)
# Start with the source as a node (The initial state) and add it to the frontier
frontier.add(node)
# Start looping until we get a solution
while True:
# Check if Frontier is empty then no solution
if frontier.empty():
raise Exception("No path found")
# Remove node from the frontier
node = frontier.remove()
# Check if node contains the goal state
if node.state == target:
actions: []
cells: []
while node.parent is not None:
actions.append(node.action)
cells.append(node.state)
node = node.parent
actions.reverse()
cells.reverse()
solution = (actions, cells)
return solution
I really appreciate any help on this.
use = to assign
if node.state == target:
actions = []
cells = []
while node.parent is not None:
actions.append(node.action)
cells.append(node.state)
node = node.parent

Linked lists in Python

I have a Linked Lists assignment for school although I am just getting the hang of class constructors. I am trying to simply get the basics of the linked list data structure down, and I understand the basic concept. I have watched lots of Youtube tutorials and the like, but where I am failing to understand is how to print out the cargo or data in my nodes using a loop.
I have written something along these lines:
class Node:
def __init__(self, value, pointer):
self.value = value
self.pointer = pointer
node4 = Node(31, None)
node3 = Node(37, None)
node2 = Node(62, None)
node1 = Node(23, None)
Now...I understand that each node declaration is a call to the class constructor of Node and that the list is linked because each node contains a pointer to the next node, but I simply don't understand how to print them out using a loop. I've seen examples using global variables for the "head" and I've seen subclasses created to accomplish the task. I'm old and dumb. I was wondering if someone could take it slow and explain it to me like I'm 5. If anyone out there has the compassion and willingness to hold my hand through the explanation, I would be greatly obliged. Thank you in advance, kind sirs.
First of all, your nodes should be created something like this :
node4 = Node(31, node3)
node3 = Node(37, node2)
node2 = Node(62, node1)
node1 = Node(23, None)
Now, i am sure you can see that the last node in the list would point to None. So, therefore, you can loop through the list until you encounter None. Something like this should work :
printhead = node4
while True:
print(printhead.value)
if printhead.pointer is None:
break;
else :
printhead = printhead.pointer
This is a very basic linked list implementation for educational purposes only.
from __future__ import print_function
"""The above is needed for Python 2.x unless you change
`print(node.value)` into `print node.value`"""
class Node(object):
"""This class represents list item (node)"""
def __init__(self, value, next_node):
"""Store item value and pointer to the next node"""
self.value = value
self.next_node = next_node
class LinkedList(object):
"""This class represents linked list"""
def __init__(self, *values):
"""Create nodes and store reference to the first node"""
node = None
# Create nodes in reversed order as each node needs to store reference to next node
for value in reversed(values):
node = Node(value, node)
self.first_node = node
# Initialize current_node for iterator
self.current_node = self.first_node
def __iter__(self):
"""Tell Python that this class is iterable"""
return self
def __next__(self):
"""Return next node from the linked list"""
# If previous call marked iteration as done, let's really finish it
if isinstance(self.current_node, StopIteration):
stop_iteration = self.current_node
# Reset current_node back to reference first_node
self.current_node = self.first_node
# Raise StopIteration to exit for loop
raise stop_iteration
# Take the current_node into local variable
node = self.current_node
# If next_node is None, then the current_node is the last one, let's mark this with StopIteration instance
if node.next_node is None:
self.current_node = StopIteration()
else:
# Put next_node reference into current_node
self.current_node = self.current_node.next_node
return node
linked_list = LinkedList(31, 37, 62, 23)
for node in linked_list:
print(node.value)
This doesn't handle many cases properly (including break statement in the loop body) but the goal is to show minimum requirements for linked list implementation in Python.

Dijkstra's Algorithm - wrong order of nodes in shortest path

I've been working on a school assignment, where I need to implement Dijkstra's algorithm. That wouldn't be too hard by itself but unfortunately, the automatic checking script disagrees with all of my implementations (I actually made like 8 different versions). All the initial data checking works correctly, only when the script generates random data, it differs. My path and script's path has the same distance, but different vertexes on the path. For example:
Teachers path: City2, City15, City16, City6,
Students path: City2, City15, City18, City0, City6,
I even contacted the teacher who just responded with "You have to use priority queue :-)" despite me using one (in fact, several implementations of one, from my own to heapq). Am I doing something wrong or is it the teacher script that's incorrect? I hope the code is self-commenting enough to be understandable. Thank you for any advice you can give me.
The algorithm is called on source vertex and computes shortest distance and path to every other connected node. If the vertex has same minDistance (ie. priority) as some that's already there, it should go in front of it, not after it.
class Node:
"""Basic node of the priority queue"""
def __init__(self, data, priority):
self.data = data
self.nextNode = None
self.priority = priority
self.id = data.id
class PriorityQueue:
"""Basic priority queue with add, remove and update methods"""
def __init__(self):
self.head = None
self.count = 0
def add(self, data, priority):
"""Adds data with priority in the proper place"""
node = Node(data, priority)
if not self.head:
self.head = node
elif node.priority <= self.head.priority:
node.nextNode = self.head
self.head = node
else:
checker = self.head
while True:
if not checker.nextNode or node.priority >= checker.nextNode.priority:
break
checker = checker.nextNode
node.nextNode = checker.nextNode
checker.nextNode = node
return 0
def remove(self, data):
"""Removes specified node and reconnects the remaining nodes, does nothing if node not found"""
checker = self.head
if not self.head:
return 0
if checker.id == data.id:
self.head = checker.nextNode
while True:
checker = checker.nextNode
if not checker or not checker.nextNode:
return 0
if checker.nextNode.id == data.id:
checker.nextNode = checker.nextNode.nextNode
break
return 0
def update(self, data):
"""Updates priority of existing node via removing and re-adding it"""
self.remove(data)
self.add(data, data.minDistance)
return 0
def getMin(self):
"""Returns the minimum priority data"""
min = self.head
return min.data
class Edge:
"""Edge of the graph, contains source, target and weight of line"""
def __init__(self, source, target, weight):
self.source = source
self.target = target
self.weight = weight
class Vertex:
"""Vertex of the graph, everything except id and name is filled later"""
def __init__(self, id, name):
self.id = id
self.name = name
self.minDistance = float('inf')
self.previousVertex = None
self.edges = []
self.visited = False
class Dijkstra:
"""Dijkstra's algorithm implementation"""
def __init__(self):
self.vertexes = []
self.nodes = {}
self.unvisited = PriorityQueue()
def createGraph(self, vertexes, edgesToVertexes):
"""Connects edges to appropriate vertexes, adds vertexes to node dictionary"""
self.vertexes = vertexes
for vertex in self.vertexes:
for edge in edgesToVertexes:
if vertex.id == edge.source:
vertex.edges.append(edge)
edgesToVertexes.remove(edge)
self.nodes[vertex.id] = vertex
return 0
def getVertexes(self):
"""Returns vertexes in graph, should be called after creating it just to check"""
return self.vertexes
def computePath(self, sourceId):
"""Fills in minDistance and previousVertex of all nodes from source"""
mainNode = self.nodes[sourceId]
mainNode.minDistance = 0
self.unvisited.add(mainNode, 0)
while self.unvisited.head:
mainNode = self.unvisited.getMin()
mainNode.visited=True
for edge in mainNode.edges:
tempDistance = mainNode.minDistance + edge.weight
targetNode = self.nodes[edge.target]
self.unvisited.remove(mainNode)
if tempDistance < targetNode.minDistance:
targetNode.minDistance = tempDistance
targetNode.previousVertex = mainNode
self.unvisited.update(targetNode)
return 0
def getShortestPathTo(self, targetId):
"""Returns list of shortest parth to targetId from source. Call only after doing ComputePath"""
path = []
mainNode = self.nodes[targetId]
while True:
path.append(mainNode)
mainNode = mainNode.previousVertex
if not mainNode:
break
return list(reversed(path))
def resetDijkstra(self):
"""Resets ComputePath but leaves graph untouched"""
for vertex in self.vertexes:
vertex.minDistance = float('inf')
vertex.previousVertex = None
return 0
def createGraph(self, vertexes, edgesToVertexes):
"""Connects edges to appropriate vertexes, adds vertexes to node dictionary"""
self.vertexes = vertexes
for vertex in self.vertexes:
for edge in edgesToVertexes:
if vertex.id == edge.source:
vertex.edges.append(edge)
edgesToVertexes.remove(edge)
self.nodes[vertex.id] = vertex
return 0
I belive this was wrong => edgesToVertexes.remove(edge)
I had similar home work and used some of your code and this one line was incorrect I think. It removed one path from vortex in every loop.

Implementing a Depth First Traversal based on a strategy in Python

Good day.
I have a problem implementing a depth first search based on a Strategy, which is defined in a strategy.py class. There is also a graph and a traversal class. The traversal class is responsible for well, traversing the graph.
The strategy class is as follows:
class Strategy:
init_priority = 0
def __init__(self, init_pri = 0):
self.init_priority = init_pri
def init(self, graph, node):
"""Called at beginning of traversal process. Expected that
this will carry out any necessary initialisation for the
specific traversal process
"""
pass
def visit(self, node, pri):
"""Called whenever NODE is visited by a traversal process.
PRI is the priority associated with the node in the priority
queue used by the traversal process.
"""
pass
def complete(self, node):
"""Called at the end of all the processing performed in visiting NODE.
"""
pass
def discover(self, nbr, node, weight, pri):
"""Return the priority that should be associated with NBR when it is
added to the priority queue.
Called whenever NBR is discovered for the first time. NODE
is the node from which the neighbour was discovered, and
WEIGHT is the value on the edge from NODE to NBR. PRI is the
value associated with NODE in the priority queue, at the time
of discovering NBR.
"""
def rediscover(self, nbr, node, weight, pri):
"""Return the priority that should be associated with NBR when it is
added to the priority queue.
Called whenever NBR is rediscovered. NODE is the node from
which the neighbour is rediscovered, and WEIGHT is the value
associated with the edge from NODE to NBR. PRI is the
priority of NODE in the priority queue. It is provided in
case it is relevant to the traversal strategy (e.g. for Dijkstra's)
"""
pass
def getResult(self):
"""Called at the end of the traversal process. It should
return whatever is relevant or appropriate for the type of
traversal implemented by this strategy.
"""
pass
I managed to implement a breadth first search as follows:
class BreadthFirst(Strategy):
sequence = None # the sequence in which nodes are visted
treeEdges = None # the edges used to visit the nodes traversed
root = -1 # the origin of the traversal
last_pri = -1 # the most recent priority used
def __init__(self):
"""The BreadthFirst strategy uses an initial priority of 0"""
Strategy(0)
def init(self, graph, node):
"""We reset all our state information so that old traversals do not
affect the one that is about to start."""
self.last_pri = self.init_priority
self.treeEdges = []
self.sequence = []
self.root = -1
def visit(self, node, src, pri):
"""Breadth first traversal pays no attention to weights."""
self.sequence.append(node)
if src == -1:
self.root = node
else:
self.treeEdges.append((src, node))
def complete(self, node):
pass
def discover(self, nbr, node, pri):
"""Want FIFO behaviour so increment priority (ignore weights)"""
self.last_pri += 1
return self.last_pri
def rediscover(self, nbr, node, pri):
"""Rules for rediscovery same as for discovery (because weights are
ignored)"""
self.last_pri += 1
return self.last_pri
def getResult(self):
"""Return the details of the traversal as a dictionary."""
return {"origin":self.root,
"tree":self.treeEdges,
"sequence":self.sequence}
Depth first is giving me a hassle of a time though. Here's what I have so far:
class DepthFirst(Strategy):
forward = None # the forward sequence in which nodes are visted
back = None # the backward sequence in which nodes are visited
treeEdges = None # the edges used to visit the nodes traversed
cross = None
root = -1 # the origin of the traversal
last_pri = -1 # the most recent priority used
def __init__(self):
"""The DepthFirst strategy uses an initial priority of 0"""
Strategy(0)
def init(self, graph, node):
"""Called at beginning of traversal process. Expected that
this will carry out any necessary initialisation for the
specific traversal process
"""
self.last_pri = self.init_priority
self.treeEdges = []
self.forward = []
self.back = []
self.cross = []
def visit(self, node, src, pri):
"""Called whenever NODE is visited by a traversal process.
PRI is the priority associated with the node in the priority
queue used by the traversal process.
"""
self.forward.append(node)
if src == -1:
self.root = node
else:
self.treeEdges.append((src, node))
def complete(self, node):
"""Called at the end of all the processing performed in visiting NODE.
"""
if node not in self.forward:
self.cross.append(node)
def discover(self, nbr, node, pri):
"""Return the priority that should be associated with NBR when it is
added to the priority queue.
Called whenever NBR is discovered for the first time. NODE
is the node from which the neighbour was discovered, and
WEIGHT is the value on the edge from NODE to NBR. PRI is the
value associated with NODE in the priority queue, at the time
of discovering NBR.
"""
self.forward.append((node, nbr))
self.last_pri -= 1
return self.last_pri
def rediscover(self, nbr, node, pri):
"""Return the priority that should be associated with NBR when it is
added to the priority queue.
Called whenever NBR is rediscovered. NODE is the node from
which the neighbour is rediscovered, and WEIGHT is the value
associated with the edge from NODE to NBR. PRI is the
priority of NODE in the priority queue. It is provided in
case it is relevant to the traversal strategy (e.g. for Dijkstra's)
"""
self.back.append((nbr, node))
self.last_pri -= 1
return self.last_pri
def getResult(self):
"""Called at the end of the traversal process. It should
return whatever is relevant or appropriate for the type of
traversal implemented by this strategy.
"""
return {"tree":self.treeEdges,
"forward":self.forward,
"back":self.back,
"cross":self.cross}
Any tips, pointers? They would be well appreciated.
if you were just writing the two, you'd do the usual iterative loop, using a stack for DFS and a queue for BFS. here you are unifying those with a priority queue. so you need to make the priorities up so that those two behaviours come out. for DFS that means that every time you add something it has higher priority than before (so it comes out before what's already in there) - an increasing positive number is fine. for BFS it needs to be lower than anything you have added so far (so it comes out after what's already in there) - a decreasing negative number works well.
this is just my take from scanning your code. i may be wrong and i'm not going to look in detail - i just thought it was an interesting way of looking at things that might help.
ps it's normal to tag homework with "homework". if you don't, people will bitch.

Categories

Resources