"""Search (Chapters 3-4) The way to use this code is to subclass Problem to create a class of problems, then create problem instances and solve them with calls to the various search functions.""" from .utils import ( is_in, memoize, print_table, Stack, FIFOQueue, PriorityQueue, name ) import sys infinity = float('inf') # ______________________________________________________________________________ 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 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 "" % (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)] def child_node(self, problem, action): "[Figure 3.10]" next = problem.result(self.state, action) return Node(next, self, action, problem.path_cost(self.path_cost, self.state, action, next)) def solution(self): "Return the sequence of actions to go from the root to this node." 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_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): return hash(self.state) # ______________________________________________________________________________ # Uninformed Search algorithms def tree_search(problem, frontier): """Search through the successors of a problem to find a goal. The argument frontier should be an empty queue. Don't worry about repeated paths to a state. [Figure 3.7]""" frontier.append(Node(problem.initial)) while frontier: node = frontier.pop() if problem.goal_test(node.state): return node frontier.extend(node.expand(problem)) return None def graph_search(problem, frontier): """Search through the successors of a problem to find a goal. The argument frontier should be an empty queue. If two paths reach a state, only use the first one. [Figure 3.7]""" frontier.append(Node(problem.initial)) explored = set() while frontier: node = frontier.pop() if problem.goal_test(node.state): return node explored.add(node.state) frontier.extend(child for child in node.expand(problem) if child.state not in explored and child not in frontier) return None def breadth_first_tree_search(problem): "Search the shallowest nodes in the search tree first." return tree_search(problem, FIFOQueue()) def depth_first_tree_search(problem): "Search the deepest nodes in the search tree first." return tree_search(problem, Stack()) def depth_first_graph_search(problem): "Search the deepest nodes in the search tree first." return graph_search(problem, Stack()) def breadth_first_search(problem): "[Figure 3.11]" node = Node(problem.initial) if problem.goal_test(node.state): return node frontier = FIFOQueue() frontier.append(node) explored = set() while frontier: node = frontier.pop() explored.add(node.state) for child in node.expand(problem): if child.state not in explored and child not in frontier: if problem.goal_test(child.state): return child frontier.append(child) return None def best_first_graph_search(problem, f): """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.""" f = memoize(f, 'f') node = Node(problem.initial) if problem.goal_test(node.state): return node frontier = PriorityQueue(min, f) frontier.append(node) explored = set() while frontier: node = frontier.pop() if problem.goal_test(node.state): return node explored.add(node.state) for child in node.expand(problem): if child.state not in explored and child not in frontier: frontier.append(child) elif child in frontier: incumbent = frontier[child] if f(child) < f(incumbent): # del frontier[incumbent] frontier.append(child) return None def uniform_cost_search(problem): "[Figure 3.14]" return best_first_graph_search(problem, lambda node: node.path_cost) def depth_limited_search(problem, limit=50): "[Figure 3.17]" def recursive_dls(node, problem, limit): if problem.goal_test(node.state): return node elif limit == 0: return 'cutoff' else: cutoff_occurred = False for child in node.expand(problem): result = recursive_dls(child, problem, limit - 1) if result == 'cutoff': cutoff_occurred = True elif result is not None: return result return 'cutoff' if cutoff_occurred else None # Body of depth_limited_search: return recursive_dls(Node(problem.initial), problem, limit) def iterative_deepening_search(problem): "[Figure 3.18]" for depth in range(sys.maxsize): result = depth_limited_search(problem, depth) if result != 'cutoff': return result # ______________________________________________________________________________ # Informed (Heuristic) Search greedy_best_first_graph_search = best_first_graph_search # Greedy best-first search is accomplished by specifying f(n) = h(n). def astar_search(problem, h=None): """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.""" h = memoize(h or problem.h, 'h') return best_first_graph_search(problem, lambda n: n.path_cost + h(n)) # ______________________________________________________________________________ # Other search algorithms def recursive_best_first_search(problem, h=None): "[Figure 3.26]" h = memoize(h or problem.h, 'h') def RBFS(problem, node, flimit): if problem.goal_test(node.state): return node, 0 # (The second value is immaterial) successors = node.expand(problem) if len(successors) == 0: return None, infinity for s in successors: s.f = max(s.path_cost + h(s), node.f) while True: # Order by lowest f value successors.sort(key=lambda x: x.f) best = successors[0] if best.f > flimit: return None, best.f if len(successors) > 1: alternative = successors[1].f else: alternative = infinity result, best.f = RBFS(problem, best, min(flimit, alternative)) if result is not None: return result, best.f node = Node(problem.initial) node.f = h(node) result, bestf = RBFS(problem, node, infinity) return result # ______________________________________________________________________________ # Code to compare searchers on various problems. class InstrumentedProblem(Problem): """Delegates to a problem, and keeps statistics.""" def __init__(self, problem): self.problem = problem self.succs = self.goal_tests = self.states = 0 self.found = None def actions(self, state): self.succs += 1 return self.problem.actions(state) def result(self, state, action): self.states += 1 return self.problem.result(state, action) def goal_test(self, state): self.goal_tests += 1 result = self.problem.goal_test(state) if result: self.found = state return result def path_cost(self, c, state1, action, state2): return self.problem.path_cost(c, state1, action, state2) def value(self, state): return self.problem.value(state) def __getattr__(self, attr): return getattr(self.problem, attr) def __repr__(self): return '<%4d/%4d/%4d/%s>' % (self.succs, self.goal_tests, self.states, str(self.found)[:4]) def compare_searchers(problems, header, searchers=[breadth_first_tree_search, breadth_first_search, depth_first_graph_search, iterative_deepening_search, depth_limited_search, recursive_best_first_search]): def do(searcher, problem): p = InstrumentedProblem(problem) searcher(p) return p table = [[name(s)] + [do(s, p) for p in problems] for s in searchers] print_table(table, header)