Breadth-First-Search results vs Depth-First-Search results confusing - python

I implemented a problem solving agent program, to solve the classic 8-puzzle problem, using a Node and Problem class as described in Peter Norvig's "AI a Modern Approach 3rd Ed."
I then solve the puzzle with both a Breadth-First-Search (BFS) and then a Depth-First-Search (DFS) of the state space. Although both find a solution, I am confused about the solution arrived at by the DFS. The BFS solves the puzzle with 12 moves, but the DFS solves it with over a 100 moves and that does not seem right.
Part of my implementation traces the route from the goal or final node back to the root node. I would expect both algorithms to produce the same result (i.e. 12 moves) as they both use the same class method to trace the route; and I assume that both algorithms would identify the same node as the goal node. So unless my assumption here is wrong, I either have an error in my code or there are multiple solutions to the puzzle.
import numpy as np
initial_board = np.array([[1,2,3],
[4,0,5],
[6,7,8]])
initial_space = (1,1)
initial_state=[initial_action,(initial_board,initial_space)]
goal_board = np.array([[1,2,3],
[4,5,6],
[0,7,8]])
goal_space = (2,0)
goal_state = [goal_board,goal_space]
class Problem:
"""The abstract class for a formal problem. You should subclass
this and implement the methods actions and result, and possibly
__init__, goal_test, and path_cost. Then you will create instances
of your subclass and solve them with the various search functions."""
def __init__(self, initial, goal=None):
"""The constructor specifies the initial state, and possibly a goal
state, if there is a unique goal. Your subclass's constructor can add
other arguments."""
self.initial = initial
self.goal = goal
def actions(self, state):
"""Return the actions that can be executed in the given
state. The result would typically be a list, but if there are
many actions, consider yielding them one at a time in an
iterator, rather than building them all at once."""
raise NotImplementedError
def result(self, state, action):
"""Return the state that results from executing the given
action in the given state. The action must be one of
self.actions(state)."""
raise NotImplementedError
def goal_test(self, state):
"""Return True if the state is a goal. The default method compares the
state to self.goal or checks for state in self.goal if it is a
list, as specified in the constructor. Override this method if
checking against a single self.goal is not enough."""
if isinstance(self.goal, list):
return is_in(state, self.goal)
else:
return state == self.goal
def path_cost(self, c, state1, action, state2):
"""Return the cost of a solution path that arrives at state2 from
state1 via action, assuming cost c to get up to state1. If the problem
is such that the path doesn't matter, this function will only look at
state2. If the path does matter, it will consider c and maybe state1
and action. The default method costs 1 for every step in the path."""
return c + 1
def value(self, state):
"""For optimization problems, each state has a value. Hill Climbing
and related algorithms try to maximize this value."""
raise NotImplementedError
class PuzzleProblem(Problem):
def actions(self, state):
"""Return the actions that can be executed in the given
state. The result would typically be a list, but if there are
many actions, consider yielding them one at a time in an
iterator, rather than building them all at once."""
actions = []
(row,col) = state[1][1]
if row > 0 :
actions.append('U')
if row < 2:
actions.append('D')
if col > 0:
actions.append('L')
if col < 2:
actions.append('R')
return actions
def result(self, state, action):
"""Return the state that results from executing the given
action in the given state. The action must be one of
self.actions(state)."""
mat = state[1][0]
(row, col) = state[1][1]
if action == 'U':
mat1 = np.copy(mat)
mat1[row][col] = mat1[row-1][col]
mat1[row-1][col] = 0
return [action,(mat1,(row-1,col))]
if action == 'D':
mat1 = np.copy(mat)
mat1[row][col] = mat1[row+1][col]
mat1[row+1][col] = 0
return [action,(mat1,(row+1,col))]
if action == 'L':
mat1 = np.copy(mat)
mat1[row][col] = mat1[row][col-1]
mat1[row][col-1] = 0
return [action,(mat1,(row,col-1))]
if action == 'R':
mat1 = np.copy(mat)
mat1[row][col] = mat1[row][col+1]
mat1[row][col+1] = 0
return [action,(mat1,(row,col+1))]
def goal_test(self, state):
"""Return True if the state is a goal. The default method compares the
state to self.goal or checks for state in self.goal if it is a
list, as specified in the constructor. Override this method if
checking against a single self.goal is not enough."""
#print('State to test: ')
#print(state[1][0])
#file1.write(str(state[1][0]))
if isinstance(self.goal, list):
if (np.all(state[1][0] == self.goal[0])) and (state[1][1] == self.goal[1]):
print('GOAL REACHED')
return True
else:
return False
puzzle = PuzzleProblem(initial_state,goal_state)
from collections import deque
class Node:
"""A node in a search tree. Contains a pointer to the parent (the node
that this is a successor of) and to the actual state for this node. Note
that if a state is arrived at by two paths, then there are two nodes with
the same state. Also includes the action that got us to this state, and
the total path_cost (also known as g) to reach the node. Other functions
may add an f and h value; see best_first_graph_search and astar_search for
an explanation of how the f and h values are handled. You will not need to
subclass this class."""
def __init__(self, state, parent=None, action=None, path_cost=0):
"""Create a search tree Node, derived from a parent by an action."""
self.state = state
self.parent = parent
self.action = action
self.path_cost = path_cost
self.depth = 0
if parent:
self.depth = parent.depth + 1
def __repr__(self):
return "<Node {}>".format(self.state)
def __lt__(self, node):
return self.state < node.state
def expand(self, problem):
"""List the nodes reachable in one step from this node."""
#return [self.child_node(problem, action) for action in problem.actions(self.state)]
actions = problem.actions(self.state)
action_results = []
children = []
for action in actions:
action_results.append(problem.result(self.state,action))
for action_state in action_results:
children.append(self.child_node(problem,action_state[0]))
return children
def child_node(self, problem, action):
"""[Figure 3.10]"""
next_state = problem.result(self.state, action)
next_node = Node(next_state, self, action, problem.path_cost(self.path_cost, self.state, action, next_state))
return next_node
def solution(self):
"""Return the sequence of actions to go from the root to this node."""
#file2 = open("BFSNodes.txt","a")
file4 = open("DFSNodes.txt","a")
sol = [node.state[1][0] for node in self.path()[1:]]
file4.writelines(str(sol))
file4.close()
return [node.action for node in self.path()[1:]]
def path(self):
"""Return a list of nodes forming the path from the root to this node."""
node, path_back = self, []
while node:
path_back.append(node)
node = node.parent
return list(reversed(path_back))
# We want for a queue of nodes in breadth_first_graph_search or
# astar_search to have no duplicated states, so we treat nodes
# with the same state as equal. [Problem: this may not be what you
# want in other contexts.]
def __eq__(self, other):
return isinstance(other, Node) and self.state == other.state
def __hash__(self):
# We use the hash value of the state
# stored in the node instead of the node
# object itself to quickly search a node
# with the same state in a Hash Table
return hash(self.state)
def not_in_explored(node,explored_list):
for n in explored_list:
if (n.state[1][0] == node.state[1][0]).all():
return False
return True
def breadth_first_tree_search(problem):
"""
[Figure 3.7]
Search the shallowest nodes in the search tree first.
Search through the successors of a problem to find a goal.
The argument frontier should be an empty queue.
Repeats infinitely in case of loops.
"""
frontier = deque([Node(problem.initial)]) # FIFO queue
explored_nodes = []
breaker = 0
file1 = open("BFSsol.txt","a")
while frontier:
breaker +=1
#print('BREAKER: ',breaker)
if breaker > 1000000:
print('breaking')
break
node = frontier.popleft()
if not_in_explored(node,explored_nodes):
explored_nodes.append(node)
if problem.goal_test(node.state):
solution = node.solution()
file1.write(str(solution))
file1.close()
return solution
frontier.extend(node.expand(problem))
return None
def depth_first_tree_search(problem):
"""
[Figure 3.7]
Search the deepest nodes in the search tree first.
Search through the successors of a problem to find a goal.
The argument frontier should be an empty queue.
Repeats infinitely in case of loops.
"""
file3 = open("DFSsol.txt","a")
explored_nodes = []
frontier = [Node(problem.initial)] # Stack
breaker = 0
while frontier:
breaker +=1
#print(breaker)
if breaker > 1000000:
print('breaking')
break
node = frontier.pop()
if not_in_explored(node,explored_nodes):
explored_nodes.append(node)
if problem.goal_test(node.state):
solution = node.solution()
file3.write(str(solution))
file3.close()
return solution
frontier.extend(node.expand(problem))
return None
breadth_first_tree_search(puzzle)
depth_first_tree_search(puzzle)
The BFS derives this solution:
['R', 'D', 'L', 'L', 'U', 'R', 'D', 'R', 'U', 'L', 'L', 'D']
But the DFS derives many, many more moves.
I would expect both algorithms to derive at the 12-move solution and therefor do not understand why the DFS produces so many moves for a solution.
Can anyone point me to an error in the code; or explain the outcomes if it is correct?

What you describe is expected: DFS is not optimal, it will find solutions that are not necessarily the shortest. It just, as the name says, runs down the search tree in a depth-first manner, meaning it will never backtrack unless it has to. It returns the first solution it finds, using the first guess at every junction in the tree, and if there is none, tries the second, third, etc. So what it finds is not necessarily the shortest.
The upside of DFS is that it is more memory efficient. It doesn't need to keep track of several unfinished candidate plans because it always only considers one option.
Upshot: your code is quite possibly correct.

Related

Moving a node to the end of a chain of nodes

I have a function where I have to move an existing code for example
def print_chain_and_ids(chain):
current = chain
while current != None:
print(id(current), current.get_data())
current = current.get_next()
a = Node('first')
b = Node('middle')
c = Node('last')
a.set_next(b)
b.set_next(c)
print_chain_and_ids(a)
move_node_to_end(a, 'middle')
print_chain_and_ids(a)
so now the chain goes:
a ----> b ----> c
with the node c at the end of the chain.
If I wanted to move the node b to the end of the chain so it goes:
a ----> c ----> b
so that it doesn't change the value of the last node but just moves it around. I have a node class ready:
class Node:
def __init__(self, init_data):
self.data = init_data
self.next = None
def get_data(self):
return self.data
def get_next(self):
return self.next
def set_data(self, new_data):
self.data = new_data
def set_next(self, new_next):
self.next = new_next
def __str__(self):
return str(self.data)
I wanted to know how I would go about doing this.
I want to make to make a function which takes two inputs, the first node of the chain, and also the value which moves to the last position of the chain. So:
def node_to_end(first_node, value_to_move):
.....
This is where I have to modify the position of the nodes. So that value to move goes to the last position.
a = Node('blue')
b = Node('red')
c = Node('green')
a.set_next(b)
b.set_next(c)
which would result in blue red green
node_to_end(a, 'red')
would create a chain of
blue green red
Thank you for any help.
Your chain-of-nodes is very similar to what is called a singly-linked-list. With those, nodes are typically removed by keeping track of the previous node while traversing along the list, that way when the target node is found, you'll know which one (if any) came before it. Having that piece of information allows the list to be easily modified.
Here's how to apply that to your code. I also added a little utility to print the contents of chains to make it clear what is in them.
class Node:
def __init__(self, init_data):
self.data = init_data
self.next = None
def get_data(self):
return self.data
def get_next(self):
return self.next
def set_data(self, new_data):
self.data = new_data
def set_next(self, new_next):
self.next = new_next
def __str__(self):
return str(self.data)
def move_to_end(start, target):
"""Move target node from the chain beginning with start to end."""
previous = None
current = start
while current:
if current.get_next() is target:
previous = current
break
current = current.get_next()
if previous: # target found in chain?
previous.set_next(target.get_next()) # remove it
# and move it to the end
current = target
while current:
if not current.get_next(): # last node in chain?
target.set_next(None)
current.set_next(target)
break
current = current.get_next()
def print_node_chain(start):
"""Utility to print out a node chain starting from given node."""
values = []
current = start
while current:
values.append(str(current))
current = current.get_next()
print(' ----> '.join(values) + ' ----> None')
a = Node('blue')
b = Node('red')
c = Node('green')
a.set_next(b)
b.set_next(c)
print_node_chain(a)
move_to_end(a, b)
print_node_chain(a)
Output:
blue ----> red ----> green ----> None
blue ----> green ----> red ----> None
Well this will require some computation. Let's assume the generic case:
ei-1 -> ei -> ei+1 -> ... -> en
Now we want to move ei to the last position:
ei-1 -> ei+1 -> ... -> en -> ei
So the question is: what should change? Based on the generic case, there are three pointers that must change:
ei-1 now points to ei+1;
ei now points to nothing (None); and
en now points to ei.
Now since the linked list is not doubly linked: a node has no reference to the previous node, the only thing we can do to get the previous element, is iterate from the root until we find the element:
def get_prev_element(root,node):
while root.get_next() is not node:
root = root.get_next()
Once we have the previous element, we can let it link to the next element:
previous.set_next(node.get_next())
But now we still have to find out what the last element is. And guess what, we do this again, by iterating, we can however start from our own node:
def get_last(node):
while node.get_next() is not None:
node = node.get_next()
return node
Now we set the next of the last node, to our own node:
last.set_next(node)
and finally we set our own next to None:
node.set_next(None)
Now grouping this all together we can define a method on the node:
class Node:
# ...
def get_previous(self,root):
previous = root
while previous.get_next() is not self:
previous = previous.get_next()
return previous
def get_last(self):
last = self
while last.get_next() is not None:
last = last.get_next()
return last
def move_last(self,root):
previous = self.get_previous(root)
previous.set_next(self.get_next())
last = self.get_last()
last.set_next(self)
self.set_next(None)
Note that since the linked list is not double linked, you will have to give it the root element.
Intermezzo: given you have references to a, b and c. Of course there is no reason to calculate the previous and last: you know that a is the previous and that c is the last. So in that case, the code is simply:
a.set_next(c)
c.set_next(b)
b.set_next(None)
This code does not work if the root element is the node itself. I leave it as an exercise to modify the code to let it work properly. It is not very hard.

Recursion: Children are being added to wrong parents in iterative deepening depth first search

.getSuccessorStates(node) returns the correct successor states (children). I have tested this method on its own using the graph provided in this problem. The problem lies when I add recursion to the mix for this iterative deepening depth-first search algorithm. When I check the value of the self.parent dictionary after everything has been returned in order to backtrack and find the shortest path, some nodes in the dictionary are matched with an incorrect parent. I'm 99% sure it has to do with recursion when I store self[parent] = node, because getSuccessorStates gives the correct children for the given node, but self.parent contains some values with wrong parent to children pairings. I'm wondering if I put self.parent[child] = node in the wrong place in the code?
def __init__(self, graph):
self.graph = graph
self.nodesCreated = 0
self.maxFrontier = 0
self.maxExplored = 0
self.parent = {}
def recursive_depth_limited_search(self, node, limit):
if node == self.getGraph().getGoalState():
return node
elif limit == 0:
return "cutoff"
else:
cutoff_occurred = False
for child in self.getGraph().getSuccessorStates(node):
self.setNodesCreated(self.getNodesCreated() + 1)
self.parent[child] = node
result = self.recursive_depth_limited_search(child, limit - 1)
if result == "cutoff":
cutoff_occurred = True
elif result != "No solution found":
return result
if cutoff_occurred:
return "cutoff"
else:
return "No solution found"

Recursive insert into a tree with 4 children

I'm trying to construct a tree in Python that can take 4 children. The children are defined as an array.
What I'm struggling with is inserting recursively into this tree.
Here is what I've done so far:
Node Class:
class node:
def __init__(self,value,city,children=[None,None,None,None]):
self.value = value
self.children = children
self.city = city
Tree Class:
from node import *
from isFull import *
class tree:
root = None
def __int__(self):
self.root = None
def insert(self, city, value):
if self.root == None:
self.root = node(value, city, children=[None,None,None,None])
else:
self.rec_insert(city, value, self.root, 0)
def rec_insert(self, city, value, nodes, index):
if nodes.children[index] is None:
nodes.children[index] = node(value, city, children=[None,None,None,None])
return
elif index < 3:
self.rec_insert(city, value, nodes, index + 1)
elif index == 3:
self.rec_insert(city, value, nodes.children[0], 0)
So what I have observed is that this first if statement actually works. I can insert a root and into the first layer of children.
if nodes.children[index] is None:
Now the problem arises in level 2. Probably because I'm descending wrong.
At the start I can insert normally into layer 2, however as it gets to the right side of the tree it skips the last child in layer 2.
My logic behind this function:
self.rec_insert(city, value, nodes.children[0], 0)
I wanted to make it descend just into the left most child then my other conditional statements will make it shift right as it inserts.
This check:
elif index == 3:
I use it to determine if all the children have been inserted into in a node.
Any help will be appreciated.

Why is my implementation of Iterative Deepening Depth-First Search taking as much memory as BFS?

BFS requires O(b^d) memory, whereas IDDFS is known to run in only O(bd) memory. However, when I profile these two implementations they turn out to use exactly the same amount of RAM - what am I missing?
I'm using a Tree class with a branching factor or 10 to run the tests:
class Tree(object):
def __init__(self, value):
self.key = value
self.children = [ ]
def insert(self, value):
if len(self.children) == 0:
self.children = [ Tree(value) for x in range(10) ]
else:
for ch in self.children:
ch.insert(value)
My implementation of iddfs:
def iddfs(t):
for i in range(0,8):
printGivenLevel(t, i)
def printGivenLevel(t, level):
if not t:
return
if level == 1:
pass
elif level > 1:
for ch in t.children:
printGivenLevel(ch, level - 1)
BFS is
def bfs(t):
currLevel = [t]
nextLevel = []
while currLevel:
for node in currLevel:
if node:
nextLevel.extend([ x for x in node.children ])
currLevel = nextLevel
nextLevel = []
The code is not really doing anything, just looping through the whole tree.
I'm using https://github.com/fabianp/memory_profiler to profile the code.
IDDFS's memory benefits only apply to an implicit tree, where nodes are generated as they're reached and discarded soon after. With a tree represented completely in memory, the tree itself already takes O(b^d) memory, and the memory required for either IDDFS or BFS is minor in comparison.

Trees in Python

Please help me to understand trees in Python. This is an example of tree implementation I found in the Internet.
from collections import deque
class EmptyTree(object):
"""Represents an empty tree."""
# Supported methods
def isEmpty(self):
return True
def __str__(self):
return ""
def __iter__(self):
"""Iterator for the tree."""
return iter([])
def preorder(self, lyst):
return
def inorder(self, lyst):
return
def postorder(self, lyst):
return
class BinaryTree(object):
"""Represents a nonempty binary tree."""
# Singleton for all empty tree objects
THE_EMPTY_TREE = EmptyTree()
def __init__(self, item):
"""Creates a tree with
the given item at the root."""
self._root = item
self._left = BinaryTree.THE_EMPTY_TREE
self._right = BinaryTree.THE_EMPTY_TREE
def isEmpty(self):
return False
def getRoot(self):
return self._root
def getLeft(self):
return self._left
def getRight(self):
return self._right
def setRoot(self, item):
self._root = item
def setLeft(self, tree):
self._left = tree
def setRight(self, tree):
self._right = tree
def removeLeft(self):
left = self._left
self._left = BinaryTree.THE_EMPTY_TREE
return left
def removeRight(self):
right = self._right
self._right = BinaryTree.THE_EMPTY_TREE
return right
def __str__(self):
"""Returns a string representation of the tree
rotated 90 degrees to the left."""
def strHelper(tree, level):
result = ""
if not tree.isEmpty():
result += strHelper(tree.getRight(), level + 1)
result += " " * level
result += str(tree.getRoot()) + "\n"
result += strHelper(tree.getLeft(), level + 1)
return result
return strHelper(self, 0)
def __iter__(self):
"""Iterator for the tree."""
lyst = []
self.inorder(lyst)
return iter(lyst)
def preorder(self, lyst):
"""Adds items to lyst during
a preorder traversal."""
lyst.append(self.getRoot())
self.getLeft().preorder(lyst)
self.getRight().preorder(lyst)
def inorder(self, lyst):
"""Adds items to lyst during
an inorder traversal."""
self.getLeft().inorder(lyst)
lyst.append(self.getRoot())
self.getRight().inorder(lyst)
def postorder(self, lyst):
"""Adds items to lystduring
a postorder traversal."""
self.getLeft().postorder(lyst)
self.getRight().postorder(lyst)
lyst.append(self.getRoot())
def levelorder(self, lyst):
"""Adds items to lyst during
a levelorder traversal."""
# levelsQueue = LinkedQueue()
levelsQueue = deque ([])
levelsQueue.append(self)
while levelsQueue != deque():
node = levelsQueue.popleft()
lyst.append(node.getRoot())
left = node.getLeft()
right = node.getRight()
if not left.isEmpty():
levelsQueue.append(left)
if not right.isEmpty():
levelsQueue.append(right)
This is programm that makes the small tree.
"""
File: testbinarytree.py
Builds a full binary tree with 7 nodes.
"""
from binarytree import BinaryTree
lst = ["5", "+", "2"]
for i in range(len(lst)):
b = BinaryTree(lst[0])
d = BinaryTree(lst[1])
f = BinaryTree(lst[2])
# Build the tree from the bottom up, where
# d is the root node of the entire tree
d.setLeft(b)
d.setRight(f)
def size(tree):
if tree.isEmpty():
return 0
else:
return 1 + size(tree.getLeft()) + size(tree.getRight())
def frontier(tree):
"""Returns a list containing the leaf nodes
of tree."""
if tree.isEmpty():
return []
elif tree.getLeft().isEmpty() and tree.getRight().isEmpty():
return [tree.getRoot()]
else:
return frontier(tree.getLeft()) + frontier(tree.getRight())
print ("Size:", size(d))
print ("String:")
print (d)
How can I make a class that will count the value of the expression, such that the answer = 7 (5+2). I really want to understand the concept with a small example.
It sounds like your problem isn't trees, which are a much more general (and simple) concept, but in how to properly populate and/or evaluate an expression tree.
If you have your operators specified in post-fix order, it becomes a lot easier.
See this wikipedia article on how to deal with infix notation when parsing input to a desktop calculator. It is called the shunting-yard algorithm.
You should do function that walks a tree in depth first order, calculating value of each node, either just taking value of it (if it is "5" for example), or making calculation (if it is "+" for example) - by walking the tree in depth first order you are sure that all subnodes of given node will be calculated when you are calculating that node (for example "5" and "2" will be calculated when you are calculating "+").
Then, at the root of the tree you'll get the result of the whole tree.
First of all, I'm not going to give much detail in case this is homework, which it sounds a bit like.
You need a method on your tree class that evaluates the tree. I suppose it'll assume that the "root" value of each tree node is a number (when the node is a leaf, i.e. when it has no children) or the name of an operator (When the node has children).
Your method will be recursive: the value of a tree-node with children is determined by (1) the value of its left subtree, (2) the value of its right subtree, and (3) the operator in its "root".
You'll probably want a table -- maybe stored in a dict -- mapping operator names like "+" to actual functions like operator.add (or, if you prefer, lambda x,y: x+y).

Categories