123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368 |
- """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 "<Node %s>" % (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)
|