| 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 searchfunctions."""from .utils import (    is_in, memoize, print_table, Stack, FIFOQueue, PriorityQueue, name)import sysinfinity = 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 algorithmsdef 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 Nonedef 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 Nonedef 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 Nonedef 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 Nonedef 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) Searchgreedy_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 algorithmsdef 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)
 |