{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Search for AIMA 4th edition\n",
    "\n",
    "Implementation of search algorithms and search problems for AIMA.\n",
    "\n",
    "# Problems and Nodes\n",
    "\n",
    "We start by defining the abstract class for a `Problem`; specific problem domains will subclass this. To make it easier for algorithms that use a heuristic evaluation function, `Problem` has a default `h` function (uniformly zero), and subclasses can define their own default `h` function.\n",
    "\n",
    "We also define a `Node` in a search tree, and some functions on nodes: `expand` to generate successors; `path_actions` and `path_states`  to recover aspects of the path from the node.  "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 93,
   "metadata": {},
   "outputs": [],
   "source": [
    "%matplotlib inline\n",
    "import matplotlib.pyplot as plt\n",
    "import random\n",
    "import heapq\n",
    "import math\n",
    "import sys\n",
    "from collections import defaultdict, deque, Counter\n",
    "from itertools import combinations\n",
    "\n",
    "\n",
    "class Problem(object):\n",
    "    \"\"\"The abstract class for a formal problem. A new domain subclasses this,\n",
    "    overriding `actions` and `results`, and perhaps other methods.\n",
    "    The default heuristic is 0 and the default action cost is 1 for all states.\n",
    "    When yiou create an instance of a subclass, specify `initial`, and `goal` states \n",
    "    (or give an `is_goal` method) and perhaps other keyword args for the subclass.\"\"\"\n",
    "\n",
    "    def __init__(self, initial=None, goal=None, **kwds): \n",
    "        self.__dict__.update(initial=initial, goal=goal, **kwds) \n",
    "        \n",
    "    def actions(self, state):        raise NotImplementedError\n",
    "    def result(self, state, action): raise NotImplementedError\n",
    "    def is_goal(self, state):        return state == self.goal\n",
    "    def action_cost(self, s, a, s1): return 1\n",
    "    def h(self, node):               return 0\n",
    "    \n",
    "    def __str__(self):\n",
    "        return '{}({!r}, {!r})'.format(\n",
    "            type(self).__name__, self.initial, self.goal)\n",
    "    \n",
    "\n",
    "class Node:\n",
    "    \"A Node in a search tree.\"\n",
    "    def __init__(self, state, parent=None, action=None, path_cost=0):\n",
    "        self.__dict__.update(state=state, parent=parent, action=action, path_cost=path_cost)\n",
    "\n",
    "    def __repr__(self): return '<{}>'.format(self.state)\n",
    "    def __len__(self): return 0 if self.parent is None else (1 + len(self.parent))\n",
    "    def __lt__(self, other): return self.path_cost < other.path_cost\n",
    "    \n",
    "    \n",
    "failure = Node('failure', path_cost=math.inf) # Indicates an algorithm couldn't find a solution.\n",
    "cutoff  = Node('cutoff',  path_cost=math.inf) # Indicates iterative deepening search was cut off.\n",
    "    \n",
    "    \n",
    "def expand(problem, node):\n",
    "    \"Expand a node, generating the children nodes.\"\n",
    "    s = node.state\n",
    "    for action in problem.actions(s):\n",
    "        s1 = problem.result(s, action)\n",
    "        cost = node.path_cost + problem.action_cost(s, action, s1)\n",
    "        yield Node(s1, node, action, cost)\n",
    "        \n",
    "\n",
    "def path_actions(node):\n",
    "    \"The sequence of actions to get to this node.\"\n",
    "    if node.parent is None:\n",
    "        return []  \n",
    "    return path_actions(node.parent) + [node.action]\n",
    "\n",
    "\n",
    "def path_states(node):\n",
    "    \"The sequence of states to get to this node.\"\n",
    "    if node in (cutoff, failure, None): \n",
    "        return []\n",
    "    return path_states(node.parent) + [node.state]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Queues\n",
    "\n",
    "First-in-first-out and Last-in-first-out queues, and a `PriorityQueue`, which allows you to keep a collection of items, and continually remove from it the item with minimum `f(item)` score."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [],
   "source": [
    "FIFOQueue = deque\n",
    "\n",
    "LIFOQueue = list\n",
    "\n",
    "class PriorityQueue:\n",
    "    \"\"\"A queue in which the item with minimum f(item) is always popped first.\"\"\"\n",
    "\n",
    "    def __init__(self, items=(), key=lambda x: x): \n",
    "        self.key = key\n",
    "        self.items = [] # a heap of (score, item) pairs\n",
    "        for item in items:\n",
    "            self.add(item)\n",
    "         \n",
    "    def add(self, item):\n",
    "        \"\"\"Add item to the queuez.\"\"\"\n",
    "        pair = (self.key(item), item)\n",
    "        heapq.heappush(self.items, pair)\n",
    "\n",
    "    def pop(self):\n",
    "        \"\"\"Pop and return the item with min f(item) value.\"\"\"\n",
    "        return heapq.heappop(self.items)[1]\n",
    "    \n",
    "    def top(self): return self.items[0][1]\n",
    "\n",
    "    def __len__(self): return len(self.items)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Search Algorithms: Best-First\n",
    "\n",
    "Best-first search with various *f(n)* functions gives us different search algorithms. Note that A\\*, weighted A\\* and greedy search can be given a heuristic function, `h`, but if `h` is not supplied they use the problem's default `h` function (if the problem does not define one, it is taken as *h(n)* = 0)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 356,
   "metadata": {},
   "outputs": [],
   "source": [
    "def best_first_search(problem, f):\n",
    "    \"Search nodes with minimum f(node) value first.\"\n",
    "    node = Node(problem.initial)\n",
    "    frontier = PriorityQueue([node], key=f)\n",
    "    reached = {problem.initial: node}\n",
    "    while frontier:\n",
    "        node = frontier.pop()\n",
    "        if problem.is_goal(node.state):\n",
    "            return node\n",
    "        for child in expand(problem, node):\n",
    "            s = child.state\n",
    "            if s not in reached or child.path_cost < reached[s].path_cost:\n",
    "                reached[s] = child\n",
    "                frontier.add(child)\n",
    "    return failure\n",
    "\n",
    "\n",
    "def best_first_tree_search(problem, f):\n",
    "    \"A version of best_first_search without the `reached` table.\"\n",
    "    frontier = PriorityQueue([Node(problem.initial)], key=f)\n",
    "    while frontier:\n",
    "        node = frontier.pop()\n",
    "        if problem.is_goal(node.state):\n",
    "            return node\n",
    "        for child in expand(problem, node):\n",
    "            if not is_cycle(child):\n",
    "                frontier.add(child)\n",
    "    return failure\n",
    "\n",
    "\n",
    "def g(n): return n.path_cost\n",
    "\n",
    "\n",
    "def astar_search(problem, h=None):\n",
    "    \"\"\"Search nodes with minimum f(n) = g(n) + h(n).\"\"\"\n",
    "    h = h or problem.h\n",
    "    return best_first_search(problem, f=lambda n: g(n) + h(n))\n",
    "\n",
    "\n",
    "def astar_tree_search(problem, h=None):\n",
    "    \"\"\"Search nodes with minimum f(n) = g(n) + h(n), with no `reached` table.\"\"\"\n",
    "    h = h or problem.h\n",
    "    return best_first_tree_search(problem, f=lambda n: g(n) + h(n))\n",
    "\n",
    "\n",
    "def weighted_astar_search(problem, h=None, weight=1.4):\n",
    "    \"\"\"Search nodes with minimum f(n) = g(n) + weight * h(n).\"\"\"\n",
    "    h = h or problem.h\n",
    "    return best_first_search(problem, f=lambda n: g(n) + weight * h(n))\n",
    "\n",
    "        \n",
    "def greedy_bfs(problem, h=None):\n",
    "    \"\"\"Search nodes with minimum h(n).\"\"\"\n",
    "    h = h or problem.h\n",
    "    return best_first_search(problem, f=h)\n",
    "\n",
    "\n",
    "def uniform_cost_search(problem):\n",
    "    \"Search nodes with minimum path cost first.\"\n",
    "    return best_first_search(problem, f=g)\n",
    "\n",
    "\n",
    "def breadth_first_bfs(problem):\n",
    "    \"Search shallowest nodes in the search tree first; using best-first.\"\n",
    "    return best_first_search(problem, f=len)\n",
    "\n",
    "\n",
    "def depth_first_bfs(problem):\n",
    "    \"Search deepest nodes in the search tree first; using best-first.\"\n",
    "    return best_first_search(problem, f=lambda n: -len(n))\n",
    "\n",
    "\n",
    "def is_cycle(node, k=30):\n",
    "    \"Does this node form a cycle of length k or less?\"\n",
    "    def find_cycle(ancestor, k):\n",
    "        return (ancestor is not None and k > 0 and\n",
    "                (ancestor.state == node.state or find_cycle(ancestor.parent, k - 1)))\n",
    "    return find_cycle(node.parent, k)\n",
    "\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Other Search Algorithms\n",
    "\n",
    "Here are the other search algorithms:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 234,
   "metadata": {},
   "outputs": [],
   "source": [
    "def breadth_first_search(problem):\n",
    "    \"Search shallowest nodes in the search tree first.\"\n",
    "    node = Node(problem.initial)\n",
    "    if problem.is_goal(problem.initial):\n",
    "        return node\n",
    "    frontier = FIFOQueue([node])\n",
    "    reached = {problem.initial}\n",
    "    while frontier:\n",
    "        node = frontier.pop()\n",
    "        for child in expand(problem, node):\n",
    "            s = child.state\n",
    "            if problem.is_goal(s):\n",
    "                return child\n",
    "            if s not in reached:\n",
    "                reached.add(s)\n",
    "                frontier.appendleft(child)\n",
    "    return failure\n",
    "\n",
    "\n",
    "def iterative_deepening_search(problem):\n",
    "    \"Do depth-limited search with increasing depth limits.\"\n",
    "    for limit in range(1, sys.maxsize):\n",
    "        result = depth_limited_search(problem, limit)\n",
    "        if result != cutoff:\n",
    "            return result\n",
    "        \n",
    "        \n",
    "def depth_limited_search(problem, limit=10):\n",
    "    \"Search deepest nodes in the search tree first.\"\n",
    "    frontier = LIFOQueue([Node(problem.initial)])\n",
    "    result = failure\n",
    "    while frontier:\n",
    "        node = frontier.pop()\n",
    "        if problem.is_goal(node.state):\n",
    "            return node\n",
    "        elif len(node) >= limit:\n",
    "            result = cutoff\n",
    "        elif not is_cycle(node):\n",
    "            for child in expand(problem, node):\n",
    "                frontier.append(child)\n",
    "    return result\n",
    "\n",
    "\n",
    "def depth_first_recursive_search(problem, node=None):\n",
    "    if node is None: \n",
    "        node = Node(problem.initial)\n",
    "    if problem.is_goal(node.state):\n",
    "        return node\n",
    "    elif is_cycle(node):\n",
    "        return failure\n",
    "    else:\n",
    "        for child in expand(problem, node):\n",
    "            result = depth_first_recursive_search(problem, child)\n",
    "            if result:\n",
    "                return result\n",
    "        return failure"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 236,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "['N', 'I', 'V', 'U', 'B', 'F', 'S', 'O', 'Z', 'A', 'T', 'L']"
      ]
     },
     "execution_count": 236,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "path_states(depth_first_recursive_search(r2))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Bidirectional Best-First Search"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 412,
   "metadata": {},
   "outputs": [],
   "source": [
    "def bidirectional_best_first_search(problem_f, f_f, problem_b, f_b, terminated):\n",
    "    node_f = Node(problem_f.initial)\n",
    "    node_b = Node(problem_f.goal)\n",
    "    frontier_f, reached_f = PriorityQueue([node_f], key=f_f), {node_f.state: node_f}\n",
    "    frontier_b, reached_b = PriorityQueue([node_b], key=f_b), {node_b.state: node_b}\n",
    "    solution = failure\n",
    "    while frontier_f and frontier_b and not terminated(solution, frontier_f, frontier_b):\n",
    "        def S1(node, f):\n",
    "            return str(int(f(node))) + ' ' + str(path_states(node))\n",
    "        print('Bi:', S1(frontier_f.top(), f_f), S1(frontier_b.top(), f_b))\n",
    "        if f_f(frontier_f.top()) < f_b(frontier_b.top()):\n",
    "            solution = proceed('f', problem_f, frontier_f, reached_f, reached_b, solution)\n",
    "        else:\n",
    "            solution = proceed('b', problem_b, frontier_b, reached_b, reached_f, solution)\n",
    "    return solution\n",
    "\n",
    "def inverse_problem(problem):\n",
    "    if isinstance(problem, CountCalls):\n",
    "        return CountCalls(inverse_problem(problem._object))\n",
    "    else:\n",
    "        inv = copy.copy(problem)\n",
    "        inv.initial, inv.goal = inv.goal, inv.initial\n",
    "        return inv"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 413,
   "metadata": {},
   "outputs": [],
   "source": [
    "def bidirectional_uniform_cost_search(problem_f):\n",
    "    def terminated(solution, frontier_f, frontier_b):\n",
    "        n_f, n_b = frontier_f.top(), frontier_b.top()\n",
    "        return g(n_f) + g(n_b) > g(solution)\n",
    "    return bidirectional_best_first_search(problem_f, g, inverse_problem(problem_f), g, terminated)\n",
    "\n",
    "def bidirectional_astar_search(problem_f):\n",
    "    def terminated(solution, frontier_f, frontier_b):\n",
    "        nf, nb = frontier_f.top(), frontier_b.top()\n",
    "        return g(nf) + g(nb) > g(solution)\n",
    "    problem_f = inverse_problem(problem_f)\n",
    "    return bidirectional_best_first_search(problem_f, lambda n: g(n) + problem_f.h(n),\n",
    "                                           problem_b, lambda n: g(n) + problem_b.h(n), \n",
    "                                           terminated)\n",
    "   \n",
    "\n",
    "def proceed(direction, problem, frontier, reached, reached2, solution):\n",
    "    node = frontier.pop()\n",
    "    for child in expand(problem, node):\n",
    "        s = child.state\n",
    "        print('proceed', direction, S(child))\n",
    "        if s not in reached or child.path_cost < reached[s].path_cost:\n",
    "            frontier.add(child)\n",
    "            reached[s] = child\n",
    "            if s in reached2: # Frontiers collide; solution found\n",
    "                solution2 = (join_nodes(child, reached2[s]) if direction == 'f' else\n",
    "                             join_nodes(reached2[s], child))\n",
    "                #print('solution', path_states(solution2), solution2.path_cost, \n",
    "                # path_states(child), path_states(reached2[s]))\n",
    "                if solution2.path_cost < solution.path_cost:\n",
    "                    solution = solution2\n",
    "    return solution\n",
    "\n",
    "S = path_states\n",
    "\n",
    "#A-S-R + B-P-R => A-S-R-P + B-P\n",
    "def join_nodes(nf, nb):\n",
    "    \"\"\"Join the reverse of the backward node nb to the forward node nf.\"\"\"\n",
    "    #print('join', S(nf), S(nb))\n",
    "    join = nf\n",
    "    while nb.parent is not None:\n",
    "        cost = join.path_cost + nb.path_cost - nb.parent.path_cost\n",
    "        join = Node(nb.parent.state, join, nb.action, cost)\n",
    "        nb = nb.parent\n",
    "        #print('  now join', S(join), 'with nb', S(nb), 'parent', S(nb.parent))\n",
    "    return join\n",
    "    \n",
    "   "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#A , B = uniform_cost_search(r1), uniform_cost_search(r2)\n",
    "#path_states(A), path_states(B)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#path_states(append_nodes(A, B))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# TODO: RBFS"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Problem Domains\n",
    "\n",
    "Now we turn our attention to defining some problem domains as subclasses of `Problem`."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Route Finding Problems\n",
    "\n",
    "![](romania.png)\n",
    "\n",
    "In a `RouteProblem`, the states are names of \"cities\" (or other locations), like `'A'` for Arad. The actions are also city names; `'Z'` is the action to move to city `'Z'`. The layout of cities is given by a separate data structure, a `Map`, which is a graph where there are vertexes (cities), links between vertexes, distances (costs) of those links (if not specified, the default is 1 for every link), and optionally the 2D (x, y) location of each city can be specified. A `RouteProblem` takes this `Map` as input and allows actions to move between linked cities. The default heuristic is straight-line distance to the goal, or is uniformly zero if locations were not given."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 398,
   "metadata": {},
   "outputs": [],
   "source": [
    "class RouteProblem(Problem):\n",
    "    \"\"\"A problem to find a route between locations on a `Map`.\n",
    "    Create a problem with RouteProblem(start, goal, map=Map(...)}).\n",
    "    States are the vertexes in the Map graph; actions are destination states.\"\"\"\n",
    "    \n",
    "    def actions(self, state): \n",
    "        \"\"\"The places neighboring `state`.\"\"\"\n",
    "        return self.map.neighbors[state]\n",
    "    \n",
    "    def result(self, state, action):\n",
    "        \"\"\"Go to the `action` place, if the map says that is possible.\"\"\"\n",
    "        return action if action in self.map.neighbors[state] else state\n",
    "    \n",
    "    def action_cost(self, s, action, s1):\n",
    "        \"\"\"The distance (cost) to go from s to s1.\"\"\"\n",
    "        return self.map.distances[s, s1]\n",
    "    \n",
    "    def h(self, node):\n",
    "        \"Straight-line distance between state and the goal.\"\n",
    "        locs = self.map.locations\n",
    "        return straight_line_distance(locs[node.state], locs[self.goal])\n",
    "    \n",
    "    \n",
    "def straight_line_distance(A, B):\n",
    "    \"Straight-line distance between two points.\"\n",
    "    return sum(abs(a - b)**2 for (a, b) in zip(A, B)) ** 0.5"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Map:\n",
    "    \"\"\"A map of places in a 2D world: a graph with vertexes and links between them. \n",
    "    In `Map(links, locations)`, `links` can be either [(v1, v2)...] pairs, \n",
    "    or a {(v1, v2): distance...} dict. Optional `locations` can be {v1: (x, y)} \n",
    "    If `directed=False` then for every (v1, v2) link, we add a (v2, v1) link.\"\"\"\n",
    "\n",
    "    def __init__(self, links, locations=None, directed=False):\n",
    "        if not hasattr(links, 'items'): # Distances are 1 by default\n",
    "            links = {link: 1 for link in links}\n",
    "        if not directed:\n",
    "            for (v1, v2) in list(links):\n",
    "                links[v2, v1] = links[v1, v2]\n",
    "        self.distances = links\n",
    "        self.neighbors = multimap(links)\n",
    "        self.locations = locations or defaultdict(lambda: (0, 0))\n",
    "\n",
    "        \n",
    "def multimap(pairs) -> dict:\n",
    "    \"Given (key, val) pairs, make a dict of {key: [val,...]}.\"\n",
    "    result = defaultdict(list)\n",
    "    for key, val in pairs:\n",
    "        result[key].append(val)\n",
    "    return result"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 400,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Some specific RouteProblems\n",
    "\n",
    "romania = Map(\n",
    "    {('O', 'Z'):  71, ('O', 'S'): 151, ('A', 'Z'): 75, ('A', 'S'): 140, ('A', 'T'): 118, \n",
    "     ('L', 'T'): 111, ('L', 'M'):  70, ('D', 'M'): 75, ('C', 'D'): 120, ('C', 'R'): 146, \n",
    "     ('C', 'P'): 138, ('R', 'S'):  80, ('F', 'S'): 99, ('B', 'F'): 211, ('B', 'P'): 101, \n",
    "     ('B', 'G'):  90, ('B', 'U'):  85, ('H', 'U'): 98, ('E', 'H'):  86, ('U', 'V'): 142, \n",
    "     ('I', 'V'):  92, ('I', 'N'):  87, ('P', 'R'): 97},\n",
    "    {'A': ( 76, 497), 'B': (400, 327), 'C': (246, 285), 'D': (160, 296), 'E': (558, 294), \n",
    "     'F': (285, 460), 'G': (368, 257), 'H': (548, 355), 'I': (488, 535), 'L': (162, 379),\n",
    "     'M': (160, 343), 'N': (407, 561), 'O': (117, 580), 'P': (311, 372), 'R': (227, 412),\n",
    "     'S': (187, 463), 'T': ( 83, 414), 'U': (471, 363), 'V': (535, 473), 'Z': (92, 539)})\n",
    "\n",
    "\n",
    "r0 = RouteProblem('A', 'A', map=romania)\n",
    "r1 = RouteProblem('A', 'B', map=romania)\n",
    "r2 = RouteProblem('N', 'L', map=romania)\n",
    "r3 = RouteProblem('E', 'T', map=romania)\n",
    "r4 = RouteProblem('O', 'M', map=romania)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 232,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "['A', 'S', 'R', 'P', 'B']"
      ]
     },
     "execution_count": 232,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "path_states(uniform_cost_search(r1)) # Lowest-cost path from Arab to Bucharest"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 233,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "['A', 'S', 'F', 'B']"
      ]
     },
     "execution_count": 233,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "path_states(breadth_first_search(r1)) # Breadth-first: fewer steps, higher path cost"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Grid Problems\n",
    "\n",
    "A `GridProblem` involves navigating on a 2D grid, with some cells being impassible obstacles. By default you can move to any of the eight neighboring cells that are not obstacles (but in a problem instance you can supply a `directions=` keyword to change that). Again, the default heuristic is straight-line distance to the goal. States are `(x, y)` cell locations, such as `(4, 2)`, and actions are `(dx, dy)` cell movements, such as `(0, -1)`, which means leave the `x` coordinate alone, and decrement the `y` coordinate by 1."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "metadata": {},
   "outputs": [],
   "source": [
    "class GridProblem(Problem):\n",
    "    \"\"\"Finding a path on a 2D grid with obstacles. Obstacles are (x, y) cells.\"\"\"\n",
    "\n",
    "    def __init__(self, initial=(15, 30), goal=(130, 30), obstacles=(), **kwds):\n",
    "        Problem.__init__(self, initial=initial, goal=goal, \n",
    "                         obstacles=set(obstacles) - {initial, goal}, **kwds)\n",
    "\n",
    "    directions = [(-1, -1), (0, -1), (1, -1),\n",
    "                  (-1, 0),           (1,  0),\n",
    "                  (-1, +1), (0, +1), (1, +1)]\n",
    "    \n",
    "    def action_cost(self, s, action, s1): return straight_line_distance(s, s1)\n",
    "    \n",
    "    def h(self, node): return straight_line_distance(node.state, self.goal)\n",
    "                  \n",
    "    def result(self, state, action): \n",
    "        \"Both states and actions are represented by (x, y) pairs.\"\n",
    "        return action if action not in self.obstacles else state\n",
    "    \n",
    "    def actions(self, state):\n",
    "        \"\"\"You can move one cell in any of `directions` to a non-obstacle cell.\"\"\"\n",
    "        x, y = state\n",
    "        return {(x + dx, y + dy) for (dx, dy) in self.directions} - self.obstacles\n",
    "    \n",
    "class ErraticVacuum(Problem):\n",
    "    def actions(self, state): \n",
    "        return ['suck', 'forward', 'backward']\n",
    "    \n",
    "    def results(self, state, action): return self.table[action][state]\n",
    "    \n",
    "    table = dict(suck=    {1:{5,7}, 2:{4,8}, 3:{7}, 4:{2,4}, 5:{1,5}, 6:{8}, 7:{3,7}, 8:{6,8}},\n",
    "                 forward= {1:{2}, 2:{2}, 3:{4}, 4:{4}, 5:{6}, 6:{6}, 7:{8}, 8:{8}},\n",
    "                 backward={1:{1}, 2:{1}, 3:{3}, 4:{3}, 5:{5}, 6:{5}, 7:{7}, 8:{7}})"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 21,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Some grid routing problems\n",
    "\n",
    "# The following can be used to create obstacles:\n",
    "    \n",
    "def random_lines(X=range(15, 130), Y=range(60), N=150, lengths=range(6, 12)):\n",
    "    \"\"\"The set of cells in N random lines of the given lengths.\"\"\"\n",
    "    result = set()\n",
    "    for _ in range(N):\n",
    "        x, y = random.choice(X), random.choice(Y)\n",
    "        dx, dy = random.choice(((0, 1), (1, 0)))\n",
    "        result |= line(x, y, dx, dy, random.choice(lengths))\n",
    "    return result\n",
    "\n",
    "def line(x, y, dx, dy, length):\n",
    "    \"\"\"A line of `length` cells starting at (x, y) and going in (dx, dy) direction.\"\"\"\n",
    "    return {(x + i * dx, y + i * dy) for i in range(length)}\n",
    "\n",
    "random.seed(42) # To make this reproducible\n",
    "\n",
    "frame = line(-10, 20, 0, 1, 20) | line(150, 20, 0, 1, 20)\n",
    "cup = line(102, 44, -1, 0, 15) | line(102, 20, -1, 0, 20) | line(102, 44, 0, -1, 24)\n",
    "\n",
    "d1 = GridProblem(obstacles=random_lines(N=100) | frame)\n",
    "d2 = GridProblem(obstacles=random_lines(N=150) | frame)\n",
    "d3 = GridProblem(obstacles=random_lines(N=200) | frame)\n",
    "d4 = GridProblem(obstacles=random_lines(N=250) | frame)\n",
    "d5 = GridProblem(obstacles=random_lines(N=300) | frame)\n",
    "d6 = GridProblem(obstacles=cup | frame)\n",
    "d7 = GridProblem(obstacles=cup | frame | line(50, 35, 0, -1, 10) | line(60, 37, 0, -1, 17) | line(70, 31, 0, -1, 19))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 8 Puzzle Problems\n",
    "\n",
    "![](https://ece.uwaterloo.ca/~dwharder/aads/Algorithms/N_puzzles/images/puz3.png)\n",
    "\n",
    "A sliding tile puzzle where you can swap the blank with an adjacent piece, trying to reach a goal configuration. The cells are numbered 0 to 8, starting at the top left and going row by row left to right. The pieces are numebred 1 to 8, with 0 representing the blank. An action is the cell index number that is to be swapped with the blank (*not* the actual number to be swapped but the index into the state). So the diagram above left is the state `(5, 2, 7, 8, 4, 0, 1, 3, 6)`, and the action is `8`, because the cell number 8 (the 9th or last cell, the `6` in the bottom right) is swapped with the blank.\n",
    "\n",
    "There are two disjoint sets of states that cannot be reached from each other. One set has an even number of \"inversions\"; the other has an odd number. An inversion is when a piece in the state is larger than a piece that follows it.\n",
    "\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 397,
   "metadata": {},
   "outputs": [],
   "source": [
    "class EightPuzzle(Problem):\n",
    "    \"\"\" The problem of sliding tiles numbered from 1 to 8 on a 3x3 board,\n",
    "    where one of the squares is a blank, trying to reach a goal configuration.\n",
    "    A board state is represented as a tuple of length 9, where the element at index i \n",
    "    represents the tile number at index i, or 0 if for the empty square, e.g. the goal:\n",
    "        1 2 3\n",
    "        4 5 6 ==> (1, 2, 3, 4, 5, 6, 7, 8, 0)\n",
    "        7 8 _\n",
    "    \"\"\"\n",
    "\n",
    "    def __init__(self, initial, goal=(0, 1, 2, 3, 4, 5, 6, 7, 8)):\n",
    "        assert inversions(initial) % 2 == inversions(goal) % 2 # Parity check\n",
    "        self.initial, self.goal = initial, goal\n",
    "    \n",
    "    def actions(self, state):\n",
    "        \"\"\"The indexes of the squares that the blank can move to.\"\"\"\n",
    "        moves = ((1, 3),    (0, 2, 4),    (1, 5),\n",
    "                 (0, 4, 6), (1, 3, 5, 7), (2, 4, 8),\n",
    "                 (3, 7),    (4, 6, 8),    (7, 5))\n",
    "        blank = state.index(0)\n",
    "        return moves[blank]\n",
    "    \n",
    "    def result(self, state, action):\n",
    "        \"\"\"Swap the blank with the square numbered `action`.\"\"\"\n",
    "        s = list(state)\n",
    "        blank = state.index(0)\n",
    "        s[action], s[blank] = s[blank], s[action]\n",
    "        return tuple(s)\n",
    "    \n",
    "    def h1(self, node):\n",
    "        \"\"\"The misplaced tiles heuristic.\"\"\"\n",
    "        return hamming_distance(node.state, self.goal)\n",
    "    \n",
    "    def h2(self, node):\n",
    "        \"\"\"The Manhattan heuristic.\"\"\"\n",
    "        X = (0, 1, 2, 0, 1, 2, 0, 1, 2)\n",
    "        Y = (0, 0, 0, 1, 1, 1, 2, 2, 2)\n",
    "        return sum(abs(X[s] - X[g]) + abs(Y[s] - Y[g])\n",
    "                   for (s, g) in zip(node.state, self.goal) if s != 0)\n",
    "    \n",
    "    def h(self, node): return h2(self, node)\n",
    "    \n",
    "    \n",
    "def hamming_distance(A, B):\n",
    "    \"Number of positions where vectors A and B are different.\"\n",
    "    return sum(a != b for a, b in zip(A, B))\n",
    "    \n",
    "\n",
    "def inversions(board):\n",
    "    \"The number of times a piece is a smaller number than a following piece.\"\n",
    "    return sum((a > b and a != 0 and b != 0) for (a, b) in combinations(board, 2))\n",
    "    \n",
    "    \n",
    "def board8(board, fmt=(3 * '{} {} {}\\n')):\n",
    "    \"A string representing an 8-puzzle board\"\n",
    "    return fmt.format(*board).replace('0', '_')\n",
    "\n",
    "class Board(defaultdict):\n",
    "    empty = '.'\n",
    "    off = '#'\n",
    "    def __init__(self, board=None, width=8, height=8, to_move=None, **kwds):\n",
    "        if board is not None:\n",
    "            self.update(board)\n",
    "            self.width, self.height = (board.width, board.height) \n",
    "        else:\n",
    "            self.width, self.height = (width, height)\n",
    "        self.to_move = to_move\n",
    "\n",
    "    def __missing__(self, key):\n",
    "        x, y = key\n",
    "        if x < 0 or x >= self.width or y < 0 or y >= self.height:\n",
    "            return self.off\n",
    "        else:\n",
    "            return self.empty\n",
    "        \n",
    "    def __repr__(self):\n",
    "        def row(y): return ' '.join(self[x, y] for x in range(self.width))\n",
    "        return '\\n'.join(row(y) for y in range(self.height))\n",
    "            \n",
    "    def __hash__(self): \n",
    "        return hash(tuple(sorted(self.items()))) + hash(self.to_move)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 23,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Some specific EightPuzzle problems\n",
    "\n",
    "e1 = EightPuzzle((1, 4, 2, 0, 7, 5, 3, 6, 8))\n",
    "e2 = EightPuzzle((1, 2, 3, 4, 5, 6, 7, 8, 0))\n",
    "e3 = EightPuzzle((4, 0, 2, 5, 1, 3, 7, 8, 6))\n",
    "e4 = EightPuzzle((7, 2, 4, 5, 0, 6, 8, 3, 1))\n",
    "e5 = EightPuzzle((8, 6, 7, 2, 5, 4, 3, 0, 1))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 24,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "1 4 2\n",
      "_ 7 5\n",
      "3 6 8\n",
      "\n",
      "1 4 2\n",
      "3 7 5\n",
      "_ 6 8\n",
      "\n",
      "1 4 2\n",
      "3 7 5\n",
      "6 _ 8\n",
      "\n",
      "1 4 2\n",
      "3 _ 5\n",
      "6 7 8\n",
      "\n",
      "1 _ 2\n",
      "3 4 5\n",
      "6 7 8\n",
      "\n",
      "_ 1 2\n",
      "3 4 5\n",
      "6 7 8\n",
      "\n"
     ]
    }
   ],
   "source": [
    "# Solve an 8 puzzle problem and print out each state\n",
    "\n",
    "for s in path_states(astar_search(e1)):\n",
    "    print(board8(s))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Water Pouring Problems\n",
    "\n",
    "![](http://puzzles.nigelcoldwell.co.uk/images/water22.png)\n",
    "\n",
    "In a [water pouring problem](https://en.wikipedia.org/wiki/Water_pouring_puzzle) you are given a collection of jugs, each of which has a size (capacity) in, say, litres, and a current level of water (in litres). The goal is to measure out a certain level of water; it can appear in any of the jugs. For example, in the movie *Die Hard 3*, the heroes were faced with the task of making exactly 4 gallons from jugs of size 5 gallons and 3 gallons.) A state is represented by a tuple of current water levels, and the available actions are:\n",
    "- `(Fill, i)`: fill the `i`th jug all the way to the top (from a tap with unlimited water).\n",
    "- `(Dump, i)`: dump all the water out of the `i`th jug.\n",
    "- `(Pour, i, j)`: pour water from the `i`th jug into the `j`th jug until either the jug `i` is empty, or jug `j` is full, whichever comes first."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 25,
   "metadata": {},
   "outputs": [],
   "source": [
    "class PourProblem(Problem):\n",
    "    \"\"\"Problem about pouring water between jugs to achieve some water level.\n",
    "    Each state is a tuples of water levels. In the initialization, also provide a tuple of \n",
    "    jug sizes, e.g. PourProblem(initial=(0, 0), goal=4, sizes=(5, 3)), \n",
    "    which means two jugs of sizes 5 and 3, initially both empty, with the goal\n",
    "    of getting a level of 4 in either jug.\"\"\"\n",
    "    \n",
    "    def actions(self, state):\n",
    "        \"\"\"The actions executable in this state.\"\"\"\n",
    "        jugs = range(len(state))\n",
    "        return ([('Fill', i)    for i in jugs if state[i] < self.sizes[i]] +\n",
    "                [('Dump', i)    for i in jugs if state[i]] +\n",
    "                [('Pour', i, j) for i in jugs if state[i] for j in jugs if i != j])\n",
    "\n",
    "    def result(self, state, action):\n",
    "        \"\"\"The state that results from executing this action in this state.\"\"\"\n",
    "        result = list(state)\n",
    "        act, i, *_ = action\n",
    "        if act == 'Fill':   # Fill i to capacity\n",
    "            result[i] = self.sizes[i]\n",
    "        elif act == 'Dump': # Empty i\n",
    "            result[i] = 0\n",
    "        elif act == 'Pour': # Pour from i into j\n",
    "            j = action[2]\n",
    "            amount = min(state[i], self.sizes[j] - state[j])\n",
    "            result[i] -= amount\n",
    "            result[j] += amount\n",
    "        return tuple(result)\n",
    "\n",
    "    def is_goal(self, state):\n",
    "        \"\"\"True if the goal level is in any one of the jugs.\"\"\"\n",
    "        return self.goal in state"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "In a `GreenPourProblem`, the states and actions are the same, but instead of all actions costing 1, in these problems the cost of an action is the amount of water that flows from the tap. (There is an issue that non-*Fill* actions have 0 cost, which in general can lead to indefinitely long solutions, but in this problem there is a finite number of states, so we're ok.)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 26,
   "metadata": {},
   "outputs": [],
   "source": [
    "class GreenPourProblem(PourProblem): \n",
    "    \"\"\"A PourProblem in which the cost is the amount of water used.\"\"\"\n",
    "    def action_cost(self, s, action, s1):\n",
    "        \"The cost is the amount of water used.\"\n",
    "        act, i, *_ = action\n",
    "        return self.sizes[i] - s[i] if act == 'Fill' else 0"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 27,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Some specific PourProblems\n",
    "\n",
    "p1 = PourProblem((1, 1, 1), 13, sizes=(2, 16, 32))\n",
    "p2 = PourProblem((0, 0, 0), 21, sizes=(8, 11, 31))\n",
    "p3 = PourProblem((0, 0),     8, sizes=(7,9))\n",
    "p4 = PourProblem((0, 0, 0), 21, sizes=(8, 11, 31))\n",
    "p5 = PourProblem((0, 0),     4, sizes=(3, 5))\n",
    "\n",
    "g1 = GreenPourProblem((1, 1, 1), 13, sizes=(2, 16, 32))\n",
    "g2 = GreenPourProblem((0, 0, 0), 21, sizes=(8, 11, 31))\n",
    "g3 = GreenPourProblem((0, 0),     8, sizes=(7,9))\n",
    "g4 = GreenPourProblem((0, 0, 0), 21, sizes=(8, 11, 31))\n",
    "g5 = GreenPourProblem((0, 0),     4, sizes=(3, 5))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 28,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "([('Fill', 1), ('Pour', 1, 0), ('Dump', 0), ('Pour', 1, 0)],\n",
       " [(1, 1, 1), (1, 16, 1), (2, 15, 1), (0, 15, 1), (2, 13, 1)])"
      ]
     },
     "execution_count": 28,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# Solve the PourProblem of getting 13 in some jug, and show the actions and states\n",
    "soln = breadth_first_search(p1)\n",
    "path_actions(soln), path_states(soln)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Pancake Sorting Problems\n",
    "\n",
    "Given a stack of pancakes of various sizes, can you sort them into a stack of decreasing sizes, largest on bottom to smallest on top? You have a spatula with which you can flip the top `i` pancakes. This is shown below for `i = 3`; on the top the spatula grabs the first three pancakes; on the bottom we see them flipped:\n",
    "\n",
    "\n",
    "![](https://upload.wikimedia.org/wikipedia/commons/0/0f/Pancake_sort_operation.png)\n",
    "\n",
    "How many flips will it take to get the whole stack sorted? This is an interesting [problem](https://en.wikipedia.org/wiki/Pancake_sorting) that Bill Gates has [written about](https://people.eecs.berkeley.edu/~christos/papers/Bounds%20For%20Sorting%20By%20Prefix%20Reversal.pdf). A reasonable heuristic for this problem is the *gap heuristic*: if we look at neighboring pancakes, if, say, the 2nd smallest is next to the 3rd smallest, that's good; they should stay next to each other. But if the 2nd smallest is next to the 4th smallest, that's bad: we will require at least one move to separate them and insert the 3rd smallest between them. The gap heuristic counts the number of neighbors that have a gap like this. In our specification of the problem, pancakes are ranked by size: the smallest is `1`, the 2nd smallest `2`, and so on, and the representation of a state is a tuple of these rankings, from the top to the bottom pancake. Thus the goal state is always `(1, 2, ..., `*n*`)` and the initial (top) state in the diagram above is `(2, 1, 4, 6, 3, 5)`.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 29,
   "metadata": {},
   "outputs": [],
   "source": [
    "class PancakeProblem(Problem):\n",
    "    \"\"\"A PancakeProblem the goal is always `tuple(range(1, n+1))`, where the\n",
    "    initial state is a permutation of `range(1, n+1)`. An act is the index `i` \n",
    "    of the top `i` pancakes that will be flipped.\"\"\"\n",
    "    \n",
    "    def __init__(self, initial): \n",
    "        self.initial, self.goal = tuple(initial), tuple(sorted(initial))\n",
    "    \n",
    "    def actions(self, state): return range(2, len(state) + 1)\n",
    "\n",
    "    def result(self, state, i): return state[:i][::-1] + state[i:]\n",
    "    \n",
    "    def h(self, node):\n",
    "        \"The gap heuristic.\"\n",
    "        s = node.state\n",
    "        return sum(abs(s[i] - s[i - 1]) > 1 for i in range(1, len(s)))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 30,
   "metadata": {},
   "outputs": [],
   "source": [
    "c0 = PancakeProblem((2, 1, 4, 6, 3, 5))\n",
    "c1 = PancakeProblem((4, 6, 2, 5, 1, 3))\n",
    "c2 = PancakeProblem((1, 3, 7, 5, 2, 6, 4))\n",
    "c3 = PancakeProblem((1, 7, 2, 6, 3, 5, 4))\n",
    "c4 = PancakeProblem((1, 3, 5, 7, 9, 2, 4, 6, 8))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 31,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "[(2, 1, 4, 6, 3, 5),\n",
       " (6, 4, 1, 2, 3, 5),\n",
       " (5, 3, 2, 1, 4, 6),\n",
       " (4, 1, 2, 3, 5, 6),\n",
       " (3, 2, 1, 4, 5, 6),\n",
       " (1, 2, 3, 4, 5, 6)]"
      ]
     },
     "execution_count": 31,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# Solve a pancake problem\n",
    "path_states(astar_search(c0))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Jumping Frogs Puzzle\n",
    "\n",
    "In this puzzle (which also can be played as a two-player game), the initial state is a line of squares, with N pieces of one kind on the left, then one empty square, then N pieces of another kind on the right. The diagram below uses 2 blue toads and 2 red frogs; we will represent this as the string `'LL.RR'`. The goal is to swap the pieces, arriving at `'RR.LL'`. An `'L'` piece moves left-to-right, either sliding one space ahead to an empty space, or two spaces ahead if that space is empty and if there is an `'R'` in between to hop over. The `'R'` pieces move right-to-left analogously. An action will be an `(i, j)` pair meaning to swap the pieces at those indexes. The set of actions for the N = 2 position below is `{(1, 2), (3, 2)}`, meaning either the blue toad in position 1 or the red frog in position 3 can swap places with the blank in position 2.\n",
    "\n",
    "![](https://upload.wikimedia.org/wikipedia/commons/2/2f/ToadsAndFrogs.png)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 32,
   "metadata": {},
   "outputs": [],
   "source": [
    "class JumpingPuzzle(Problem):\n",
    "    \"\"\"Try to exchange L and R by moving one ahead or hopping two ahead.\"\"\"\n",
    "    def __init__(self, N=2):\n",
    "        self.initial = N*'L' + '.' + N*'R'\n",
    "        self.goal = self.initial[::-1]\n",
    "        \n",
    "    def actions(self, state):\n",
    "        \"\"\"Find all possible move or hop moves.\"\"\"\n",
    "        idxs = range(len(state))\n",
    "        return ({(i, i + 1) for i in idxs if state[i:i+2] == 'L.'}   # Slide\n",
    "               |{(i, i + 2) for i in idxs if state[i:i+3] == 'LR.'}  # Hop\n",
    "               |{(i + 1, i) for i in idxs if state[i:i+2] == '.R'}   # Slide\n",
    "               |{(i + 2, i) for i in idxs if state[i:i+3] == '.LR'}) # Hop\n",
    "\n",
    "    def result(self, state, action):\n",
    "        \"\"\"An action (i, j) means swap the pieces at positions i and j.\"\"\"\n",
    "        i, j = action\n",
    "        result = list(state)\n",
    "        result[i], result[j] = state[j], state[i]\n",
    "        return ''.join(result)\n",
    "    \n",
    "    def h(self, node): return hamming_distance(node.state, self.goal)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 33,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "{(1, 2), (3, 2)}"
      ]
     },
     "execution_count": 33,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "JumpingPuzzle(N=2).actions('LL.RR')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 34,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "['LLL.RRR',\n",
       " 'LLLR.RR',\n",
       " 'LL.RLRR',\n",
       " 'L.LRLRR',\n",
       " 'LRL.LRR',\n",
       " 'LRLRL.R',\n",
       " 'LRLRLR.',\n",
       " 'LRLR.RL',\n",
       " 'LR.RLRL',\n",
       " '.RLRLRL',\n",
       " 'R.LRLRL',\n",
       " 'RRL.LRL',\n",
       " 'RRLRL.L',\n",
       " 'RRLR.LL',\n",
       " 'RR.RLLL',\n",
       " 'RRR.LLL']"
      ]
     },
     "execution_count": 34,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "j3 = JumpingPuzzle(N=3)\n",
    "j9 = JumpingPuzzle(N=9)\n",
    "path_states(astar_search(j3))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Reporting Summary Statistics on Search Algorithms\n",
    "\n",
    "Now let's gather some metrics on how well each algorithm does.  We'll use `CountCalls` to wrap a `Problem` object in such a way that calls to its methods are delegated to the original problem, but each call increments a counter. Once we've solved the problem, we print out summary statistics."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 35,
   "metadata": {},
   "outputs": [],
   "source": [
    "class CountCalls:\n",
    "    \"\"\"Delegate all attribute gets to the object, and count them in ._counts\"\"\"\n",
    "    def __init__(self, obj):\n",
    "        self._object = obj\n",
    "        self._counts = Counter()\n",
    "        \n",
    "    def __getattr__(self, attr):\n",
    "        \"Delegate to the original object, after incrementing a counter.\"\n",
    "        self._counts[attr] += 1\n",
    "        return getattr(self._object, attr)\n",
    "\n",
    "        \n",
    "def report(searchers, problems, verbose=True):\n",
    "    \"\"\"Show summary statistics for each searcher (and on each problem unless verbose is false).\"\"\"\n",
    "    for searcher in searchers:\n",
    "        print(searcher.__name__ + ':')\n",
    "        total_counts = Counter()\n",
    "        for p in problems:\n",
    "            prob   = CountCalls(p)\n",
    "            soln   = searcher(prob)\n",
    "            counts = prob._counts; \n",
    "            counts.update(actions=len(soln), cost=soln.path_cost)\n",
    "            total_counts += counts\n",
    "            if verbose: report_counts(counts, str(p)[:40])\n",
    "        report_counts(total_counts, 'TOTAL\\n')\n",
    "        \n",
    "def report_counts(counts, name):\n",
    "    \"\"\"Print one line of the counts report.\"\"\"\n",
    "    print('{:9,d} nodes |{:9,d} goal |{:5.0f} cost |{:8,d} actions | {}'.format(\n",
    "          counts['result'], counts['is_goal'], counts['cost'], counts['actions'], name))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Here's a tiny report for uniform-cost search on the jug pouring problems:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 36,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "uniform_cost_search:\n",
      "      948 nodes |      109 goal |    4 cost |     112 actions | PourProblem((1, 1, 1), 13)\n",
      "    3,499 nodes |      389 goal |    9 cost |     397 actions | PourProblem((0, 0, 0), 21)\n",
      "      124 nodes |       30 goal |   14 cost |      43 actions | PourProblem((0, 0), 8)\n",
      "    3,499 nodes |      389 goal |    9 cost |     397 actions | PourProblem((0, 0, 0), 21)\n",
      "       52 nodes |       14 goal |    6 cost |      19 actions | PourProblem((0, 0), 4)\n",
      "    8,122 nodes |      931 goal |   42 cost |     968 actions | TOTAL\n",
      "\n"
     ]
    }
   ],
   "source": [
    "report([uniform_cost_search], [p1, p2, p3, p4, p5])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 37,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "uniform_cost_search:\n",
      "      948 nodes |      109 goal |    4 cost |     112 actions | PourProblem((1, 1, 1), 13)\n",
      "    1,696 nodes |      190 goal |   10 cost |     204 actions | GreenPourProblem((1, 1, 1), 13)\n",
      "    3,499 nodes |      389 goal |    9 cost |     397 actions | PourProblem((0, 0, 0), 21)\n",
      "    4,072 nodes |      454 goal |   21 cost |     463 actions | GreenPourProblem((0, 0, 0), 21)\n",
      "      124 nodes |       30 goal |   14 cost |      43 actions | PourProblem((0, 0), 8)\n",
      "      124 nodes |       30 goal |   35 cost |      45 actions | GreenPourProblem((0, 0), 8)\n",
      "    3,499 nodes |      389 goal |    9 cost |     397 actions | PourProblem((0, 0, 0), 21)\n",
      "    4,072 nodes |      454 goal |   21 cost |     463 actions | GreenPourProblem((0, 0, 0), 21)\n",
      "    3,499 nodes |      389 goal |    9 cost |     397 actions | PourProblem((0, 0, 0), 21)\n",
      "    4,072 nodes |      454 goal |   21 cost |     463 actions | GreenPourProblem((0, 0, 0), 21)\n",
      "    3,590 nodes |      719 goal |    7 cost |     725 actions | PancakeProblem((4, 6, 2, 5, 1, 3), (1, 2\n",
      "   30,204 nodes |    5,035 goal |    8 cost |   5,042 actions | PancakeProblem((1, 3, 7, 5, 2, 6, 4), (1\n",
      "   22,068 nodes |    3,679 goal |    6 cost |   3,684 actions | PancakeProblem((1, 7, 2, 6, 3, 5, 4), (1\n",
      "   81,467 nodes |   12,321 goal |  174 cost |  12,435 actions | TOTAL\n",
      "\n",
      "breadth_first_search:\n",
      "      596 nodes |      597 goal |    4 cost |      73 actions | PourProblem((1, 1, 1), 13)\n",
      "      596 nodes |      597 goal |   15 cost |      73 actions | GreenPourProblem((1, 1, 1), 13)\n",
      "    2,618 nodes |    2,619 goal |    9 cost |     302 actions | PourProblem((0, 0, 0), 21)\n",
      "    2,618 nodes |    2,619 goal |   32 cost |     302 actions | GreenPourProblem((0, 0, 0), 21)\n",
      "      120 nodes |      121 goal |   14 cost |      42 actions | PourProblem((0, 0), 8)\n",
      "      120 nodes |      121 goal |   36 cost |      42 actions | GreenPourProblem((0, 0), 8)\n",
      "    2,618 nodes |    2,619 goal |    9 cost |     302 actions | PourProblem((0, 0, 0), 21)\n",
      "    2,618 nodes |    2,619 goal |   32 cost |     302 actions | GreenPourProblem((0, 0, 0), 21)\n",
      "    2,618 nodes |    2,619 goal |    9 cost |     302 actions | PourProblem((0, 0, 0), 21)\n",
      "    2,618 nodes |    2,619 goal |   32 cost |     302 actions | GreenPourProblem((0, 0, 0), 21)\n",
      "    2,951 nodes |    2,952 goal |    7 cost |     598 actions | PancakeProblem((4, 6, 2, 5, 1, 3), (1, 2\n",
      "   25,945 nodes |   25,946 goal |    8 cost |   4,333 actions | PancakeProblem((1, 3, 7, 5, 2, 6, 4), (1\n",
      "    5,975 nodes |    5,976 goal |    6 cost |   1,002 actions | PancakeProblem((1, 7, 2, 6, 3, 5, 4), (1\n",
      "   52,011 nodes |   52,024 goal |  213 cost |   7,975 actions | TOTAL\n",
      "\n"
     ]
    }
   ],
   "source": [
    "report((uniform_cost_search, breadth_first_search), \n",
    "       (p1, g1, p2, g2, p3, g3, p4, g4, p4, g4, c1, c2, c3)) "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Comparing heuristics\n",
    "\n",
    "First, let's look at the eight puzzle problems, and compare three different heuristics the Manhattan heuristic, the less informative misplaced tiles heuristic, and the uninformed (i.e. *h* = 0) breadth-first search:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 38,
   "metadata": {
    "scrolled": false
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "breadth_first_search:\n",
      "       81 nodes |       82 goal |    5 cost |      35 actions | EightPuzzle((1, 4, 2, 0, 7, 5, 3, 6, 8),\n",
      "  160,948 nodes |  160,949 goal |   22 cost |  59,960 actions | EightPuzzle((1, 2, 3, 4, 5, 6, 7, 8, 0),\n",
      "  218,263 nodes |  218,264 goal |   23 cost |  81,829 actions | EightPuzzle((4, 0, 2, 5, 1, 3, 7, 8, 6),\n",
      "  418,771 nodes |  418,772 goal |   26 cost | 156,533 actions | EightPuzzle((7, 2, 4, 5, 0, 6, 8, 3, 1),\n",
      "  448,667 nodes |  448,668 goal |   27 cost | 167,799 actions | EightPuzzle((8, 6, 7, 2, 5, 4, 3, 0, 1),\n",
      "1,246,730 nodes |1,246,735 goal |  103 cost | 466,156 actions | TOTAL\n",
      "\n",
      "astar_misplaced_tiles:\n",
      "       17 nodes |        7 goal |    5 cost |      11 actions | EightPuzzle((1, 4, 2, 0, 7, 5, 3, 6, 8),\n",
      "   23,407 nodes |    8,726 goal |   22 cost |   8,747 actions | EightPuzzle((1, 2, 3, 4, 5, 6, 7, 8, 0),\n",
      "   38,632 nodes |   14,433 goal |   23 cost |  14,455 actions | EightPuzzle((4, 0, 2, 5, 1, 3, 7, 8, 6),\n",
      "  124,324 nodes |   46,553 goal |   26 cost |  46,578 actions | EightPuzzle((7, 2, 4, 5, 0, 6, 8, 3, 1),\n",
      "  156,111 nodes |   58,475 goal |   27 cost |  58,501 actions | EightPuzzle((8, 6, 7, 2, 5, 4, 3, 0, 1),\n",
      "  342,491 nodes |  128,194 goal |  103 cost | 128,292 actions | TOTAL\n",
      "\n",
      "astar_search:\n",
      "       15 nodes |        6 goal |    5 cost |      10 actions | EightPuzzle((1, 4, 2, 0, 7, 5, 3, 6, 8),\n",
      "    3,614 nodes |    1,349 goal |   22 cost |   1,370 actions | EightPuzzle((1, 2, 3, 4, 5, 6, 7, 8, 0),\n",
      "    5,373 nodes |    2,010 goal |   23 cost |   2,032 actions | EightPuzzle((4, 0, 2, 5, 1, 3, 7, 8, 6),\n",
      "   10,832 nodes |    4,086 goal |   26 cost |   4,111 actions | EightPuzzle((7, 2, 4, 5, 0, 6, 8, 3, 1),\n",
      "   11,669 nodes |    4,417 goal |   27 cost |   4,443 actions | EightPuzzle((8, 6, 7, 2, 5, 4, 3, 0, 1),\n",
      "   31,503 nodes |   11,868 goal |  103 cost |  11,966 actions | TOTAL\n",
      "\n"
     ]
    }
   ],
   "source": [
    "def astar_misplaced_tiles(problem): return astar_search(problem, h=problem.h1)\n",
    "\n",
    "report([breadth_first_search, astar_misplaced_tiles, astar_search], \n",
    "       [e1, e2, e3, e4, e5])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We see that all three algorithms get cost-optimal solutions, but the better the heuristic, the fewer nodes explored. \n",
    "Compared to the uninformed search, the misplaced tiles heuristic explores about 1/4 the number of nodes, and the Manhattan heuristic needs just 2%.\n",
    "\n",
    "Next, we can show the value of the gap heuristic for pancake sorting problems:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 39,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "astar_search:\n",
      "    1,285 nodes |      258 goal |    7 cost |     264 actions | PancakeProblem((4, 6, 2, 5, 1, 3), (1, 2\n",
      "    3,804 nodes |      635 goal |    8 cost |     642 actions | PancakeProblem((1, 3, 7, 5, 2, 6, 4), (1\n",
      "      294 nodes |       50 goal |    6 cost |      55 actions | PancakeProblem((1, 7, 2, 6, 3, 5, 4), (1\n",
      "    2,256 nodes |      283 goal |    9 cost |     291 actions | PancakeProblem((1, 3, 5, 7, 9, 2, 4, 6, \n",
      "    7,639 nodes |    1,226 goal |   30 cost |   1,252 actions | TOTAL\n",
      "\n",
      "uniform_cost_search:\n",
      "    3,590 nodes |      719 goal |    7 cost |     725 actions | PancakeProblem((4, 6, 2, 5, 1, 3), (1, 2\n",
      "   30,204 nodes |    5,035 goal |    8 cost |   5,042 actions | PancakeProblem((1, 3, 7, 5, 2, 6, 4), (1\n",
      "   22,068 nodes |    3,679 goal |    6 cost |   3,684 actions | PancakeProblem((1, 7, 2, 6, 3, 5, 4), (1\n",
      "2,271,792 nodes |  283,975 goal |    9 cost | 283,983 actions | PancakeProblem((1, 3, 5, 7, 9, 2, 4, 6, \n",
      "2,327,654 nodes |  293,408 goal |   30 cost | 293,434 actions | TOTAL\n",
      "\n"
     ]
    }
   ],
   "source": [
    "report([astar_search, uniform_cost_search], [c1, c2, c3, c4])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We need to explore 300 times more nodes without the heuristic.\n",
    "\n",
    "# Comparing graph search and tree search\n",
    "\n",
    "Keeping the *reached* table in `best_first_search` allows us to do a graph search, where we notice when we reach a state by two different paths, rather than a tree search, where we have duplicated effort. The *reached* table consumes space and also saves time. How much time? In part it depends on how good the heuristics are at focusing the search.  Below we show that on some pancake and eight puzzle problems, the tree search expands roughly twice as many nodes (and thus takes roughly twice as much time):"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 188,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "astar_search:\n",
      "       15 nodes |        6 goal |    5 cost |      10 actions | EightPuzzle((1, 4, 2, 0, 7, 5, 3, 6, 8),\n",
      "    3,614 nodes |    1,349 goal |   22 cost |   1,370 actions | EightPuzzle((1, 2, 3, 4, 5, 6, 7, 8, 0),\n",
      "    5,373 nodes |    2,010 goal |   23 cost |   2,032 actions | EightPuzzle((4, 0, 2, 5, 1, 3, 7, 8, 6),\n",
      "   10,832 nodes |    4,086 goal |   26 cost |   4,111 actions | EightPuzzle((7, 2, 4, 5, 0, 6, 8, 3, 1),\n",
      "       15 nodes |        6 goal |  418 cost |       9 actions | RouteProblem('A', 'B')\n",
      "       34 nodes |       15 goal |  910 cost |      23 actions | RouteProblem('N', 'L')\n",
      "       33 nodes |       14 goal |  805 cost |      21 actions | RouteProblem('E', 'T')\n",
      "       20 nodes |        9 goal |  445 cost |      13 actions | RouteProblem('O', 'M')\n",
      "   19,936 nodes |    7,495 goal | 2654 cost |   7,589 actions | TOTAL\n",
      "\n",
      "astar_tree_search:\n",
      "       15 nodes |        6 goal |    5 cost |      10 actions | EightPuzzle((1, 4, 2, 0, 7, 5, 3, 6, 8),\n",
      "    5,384 nodes |    2,000 goal |   22 cost |   2,021 actions | EightPuzzle((1, 2, 3, 4, 5, 6, 7, 8, 0),\n",
      "    9,116 nodes |    3,404 goal |   23 cost |   3,426 actions | EightPuzzle((4, 0, 2, 5, 1, 3, 7, 8, 6),\n",
      "   19,084 nodes |    7,185 goal |   26 cost |   7,210 actions | EightPuzzle((7, 2, 4, 5, 0, 6, 8, 3, 1),\n",
      "       15 nodes |        6 goal |  418 cost |       9 actions | RouteProblem('A', 'B')\n",
      "       47 nodes |       19 goal |  910 cost |      27 actions | RouteProblem('N', 'L')\n",
      "       46 nodes |       18 goal |  805 cost |      25 actions | RouteProblem('E', 'T')\n",
      "       24 nodes |       10 goal |  445 cost |      14 actions | RouteProblem('O', 'M')\n",
      "   33,731 nodes |   12,648 goal | 2654 cost |  12,742 actions | TOTAL\n",
      "\n"
     ]
    }
   ],
   "source": [
    "report([astar_search, astar_tree_search], [e1, e2, e3, e4, r1, r2, r3, r4])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Comparing different weighted search values\n",
    "\n",
    "Below we report on problems using these four algorithms:\n",
    "\n",
    "|Algorithm|*f*|Optimality|\n",
    "|:---------|---:|:----------:|\n",
    "|Greedy best-first search | *f = h*|nonoptimal|\n",
    "|Extra weighted A* search | *f = g + 2 &times; h*|nonoptimal|\n",
    "|Weighted A* search | *f = g + 1.4 &times; h*|nonoptimal|\n",
    "|A* search | *f = g + h*|optimal|\n",
    "|Uniform-cost search | *f = g*|optimal|\n",
    "\n",
    "We will see that greedy best-first search (which ranks nodes solely by the heuristic) explores the fewest number of nodes, but has the highest path costs. Weighted A* search explores twice as many nodes (on this problem set) but gets 10% better path costs. A* is optimal, but explores more nodes, and uniform-cost is also optimal, but explores an order of magnitude more nodes."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 41,
   "metadata": {
    "scrolled": false
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "greedy_bfs:\n",
      "        0 nodes |        1 goal |    0 cost |       0 actions | RouteProblem('A', 'A')\n",
      "        9 nodes |        4 goal |  450 cost |       6 actions | RouteProblem('A', 'B')\n",
      "       29 nodes |       12 goal |  910 cost |      20 actions | RouteProblem('N', 'L')\n",
      "       19 nodes |        8 goal |  837 cost |      14 actions | RouteProblem('E', 'T')\n",
      "       14 nodes |        6 goal |  572 cost |      10 actions | RouteProblem('O', 'M')\n",
      "       15 nodes |        6 goal |    5 cost |      10 actions | EightPuzzle((1, 4, 2, 0, 7, 5, 3, 6, 8),\n",
      "      909 nodes |      138 goal |  136 cost |     258 actions | GridProblem((15, 30), (130, 30))\n",
      "      974 nodes |      147 goal |  152 cost |     277 actions | GridProblem((15, 30), (130, 30))\n",
      "    5,146 nodes |    4,984 goal |   99 cost |   5,082 actions | JumpingPuzzle('LLLLLLLLL.RRRRRRRRR', 'RR\n",
      "    1,569 nodes |      568 goal |   58 cost |     625 actions | EightPuzzle((1, 2, 3, 4, 5, 6, 7, 8, 0),\n",
      "    1,424 nodes |      257 goal |  164 cost |     406 actions | GridProblem((15, 30), (130, 30))\n",
      "    1,899 nodes |      342 goal |  153 cost |     470 actions | GridProblem((15, 30), (130, 30))\n",
      "   18,239 nodes |    2,439 goal |  134 cost |   2,564 actions | GridProblem((15, 30), (130, 30))\n",
      "   18,329 nodes |    2,460 goal |  152 cost |   2,594 actions | GridProblem((15, 30), (130, 30))\n",
      "      287 nodes |      109 goal |   33 cost |     141 actions | EightPuzzle((4, 0, 2, 5, 1, 3, 7, 8, 6),\n",
      "    1,128 nodes |      408 goal |   46 cost |     453 actions | EightPuzzle((7, 2, 4, 5, 0, 6, 8, 3, 1),\n",
      "   49,990 nodes |   11,889 goal | 3901 cost |  12,930 actions | TOTAL\n",
      "\n",
      "extra_weighted_astar_search:\n",
      "        0 nodes |        1 goal |    0 cost |       0 actions | RouteProblem('A', 'A')\n",
      "        9 nodes |        4 goal |  450 cost |       6 actions | RouteProblem('A', 'B')\n",
      "       29 nodes |       12 goal |  910 cost |      20 actions | RouteProblem('N', 'L')\n",
      "       23 nodes |        9 goal |  805 cost |      16 actions | RouteProblem('E', 'T')\n",
      "       18 nodes |        8 goal |  445 cost |      12 actions | RouteProblem('O', 'M')\n",
      "       15 nodes |        6 goal |    5 cost |      10 actions | EightPuzzle((1, 4, 2, 0, 7, 5, 3, 6, 8),\n",
      "    1,575 nodes |      239 goal |  136 cost |     357 actions | GridProblem((15, 30), (130, 30))\n",
      "    1,384 nodes |      231 goal |  133 cost |     349 actions | GridProblem((15, 30), (130, 30))\n",
      "   10,990 nodes |   10,660 goal |   99 cost |  10,758 actions | JumpingPuzzle('LLLLLLLLL.RRRRRRRRR', 'RR\n",
      "    1,720 nodes |      633 goal |   24 cost |     656 actions | EightPuzzle((1, 2, 3, 4, 5, 6, 7, 8, 0),\n",
      "    9,282 nodes |    1,412 goal |  163 cost |   1,551 actions | GridProblem((15, 30), (130, 30))\n",
      "    1,354 nodes |      228 goal |  134 cost |     345 actions | GridProblem((15, 30), (130, 30))\n",
      "   16,024 nodes |    2,098 goal |  129 cost |   2,214 actions | GridProblem((15, 30), (130, 30))\n",
      "   16,950 nodes |    2,237 goal |  140 cost |   2,359 actions | GridProblem((15, 30), (130, 30))\n",
      "    1,908 nodes |      709 goal |   25 cost |     733 actions | EightPuzzle((4, 0, 2, 5, 1, 3, 7, 8, 6),\n",
      "    1,312 nodes |      489 goal |   30 cost |     518 actions | EightPuzzle((7, 2, 4, 5, 0, 6, 8, 3, 1),\n",
      "   62,593 nodes |   18,976 goal | 3628 cost |  19,904 actions | TOTAL\n",
      "\n",
      "weighted_astar_search:\n",
      "        0 nodes |        1 goal |    0 cost |       0 actions | RouteProblem('A', 'A')\n",
      "        9 nodes |        4 goal |  450 cost |       6 actions | RouteProblem('A', 'B')\n",
      "       32 nodes |       14 goal |  910 cost |      22 actions | RouteProblem('N', 'L')\n",
      "       29 nodes |       12 goal |  805 cost |      19 actions | RouteProblem('E', 'T')\n",
      "       18 nodes |        8 goal |  445 cost |      12 actions | RouteProblem('O', 'M')\n",
      "       15 nodes |        6 goal |    5 cost |      10 actions | EightPuzzle((1, 4, 2, 0, 7, 5, 3, 6, 8),\n",
      "    1,631 nodes |      236 goal |  128 cost |     350 actions | GridProblem((15, 30), (130, 30))\n",
      "    1,706 nodes |      275 goal |  131 cost |     389 actions | GridProblem((15, 30), (130, 30))\n",
      "   10,990 nodes |   10,660 goal |   99 cost |  10,758 actions | JumpingPuzzle('LLLLLLLLL.RRRRRRRRR', 'RR\n",
      "    2,082 nodes |      771 goal |   22 cost |     792 actions | EightPuzzle((1, 2, 3, 4, 5, 6, 7, 8, 0),\n",
      "    8,385 nodes |    1,266 goal |  154 cost |   1,396 actions | GridProblem((15, 30), (130, 30))\n",
      "    1,400 nodes |      229 goal |  133 cost |     344 actions | GridProblem((15, 30), (130, 30))\n",
      "   12,122 nodes |    1,572 goal |  124 cost |   1,686 actions | GridProblem((15, 30), (130, 30))\n",
      "   24,129 nodes |    3,141 goal |  127 cost |   3,255 actions | GridProblem((15, 30), (130, 30))\n",
      "    3,960 nodes |    1,475 goal |   25 cost |   1,499 actions | EightPuzzle((4, 0, 2, 5, 1, 3, 7, 8, 6),\n",
      "    1,992 nodes |      748 goal |   26 cost |     773 actions | EightPuzzle((7, 2, 4, 5, 0, 6, 8, 3, 1),\n",
      "   68,500 nodes |   20,418 goal | 3585 cost |  21,311 actions | TOTAL\n",
      "\n",
      "astar_search:\n",
      "        0 nodes |        1 goal |    0 cost |       0 actions | RouteProblem('A', 'A')\n",
      "       15 nodes |        6 goal |  418 cost |       9 actions | RouteProblem('A', 'B')\n",
      "       34 nodes |       15 goal |  910 cost |      23 actions | RouteProblem('N', 'L')\n",
      "       33 nodes |       14 goal |  805 cost |      21 actions | RouteProblem('E', 'T')\n",
      "       20 nodes |        9 goal |  445 cost |      13 actions | RouteProblem('O', 'M')\n",
      "       15 nodes |        6 goal |    5 cost |      10 actions | EightPuzzle((1, 4, 2, 0, 7, 5, 3, 6, 8),\n",
      "   26,711 nodes |    3,620 goal |  127 cost |   3,734 actions | GridProblem((15, 30), (130, 30))\n",
      "   12,932 nodes |    1,822 goal |  124 cost |   1,936 actions | GridProblem((15, 30), (130, 30))\n",
      "   10,991 nodes |   10,661 goal |   99 cost |  10,759 actions | JumpingPuzzle('LLLLLLLLL.RRRRRRRRR', 'RR\n",
      "    3,614 nodes |    1,349 goal |   22 cost |   1,370 actions | EightPuzzle((1, 2, 3, 4, 5, 6, 7, 8, 0),\n",
      "   62,509 nodes |    8,729 goal |  154 cost |   8,859 actions | GridProblem((15, 30), (130, 30))\n",
      "   15,190 nodes |    2,276 goal |  133 cost |   2,391 actions | GridProblem((15, 30), (130, 30))\n",
      "   25,303 nodes |    3,196 goal |  124 cost |   3,310 actions | GridProblem((15, 30), (130, 30))\n",
      "   32,572 nodes |    4,149 goal |  127 cost |   4,263 actions | GridProblem((15, 30), (130, 30))\n",
      "    5,373 nodes |    2,010 goal |   23 cost |   2,032 actions | EightPuzzle((4, 0, 2, 5, 1, 3, 7, 8, 6),\n",
      "   10,832 nodes |    4,086 goal |   26 cost |   4,111 actions | EightPuzzle((7, 2, 4, 5, 0, 6, 8, 3, 1),\n",
      "  206,144 nodes |   41,949 goal | 3543 cost |  42,841 actions | TOTAL\n",
      "\n",
      "uniform_cost_search:\n",
      "        0 nodes |        1 goal |    0 cost |       0 actions | RouteProblem('A', 'A')\n",
      "       30 nodes |       13 goal |  418 cost |      16 actions | RouteProblem('A', 'B')\n",
      "       42 nodes |       19 goal |  910 cost |      27 actions | RouteProblem('N', 'L')\n",
      "       44 nodes |       20 goal |  805 cost |      27 actions | RouteProblem('E', 'T')\n",
      "       30 nodes |       12 goal |  445 cost |      16 actions | RouteProblem('O', 'M')\n",
      "      124 nodes |       46 goal |    5 cost |      50 actions | EightPuzzle((1, 4, 2, 0, 7, 5, 3, 6, 8),\n",
      "  355,452 nodes |   44,984 goal |  127 cost |  45,098 actions | GridProblem((15, 30), (130, 30))\n",
      "  326,962 nodes |   41,650 goal |  124 cost |  41,764 actions | GridProblem((15, 30), (130, 30))\n",
      "   10,992 nodes |   10,662 goal |   99 cost |  10,760 actions | JumpingPuzzle('LLLLLLLLL.RRRRRRRRR', 'RR\n",
      "  214,952 nodes |   79,187 goal |   22 cost |  79,208 actions | EightPuzzle((1, 2, 3, 4, 5, 6, 7, 8, 0),\n",
      "  558,084 nodes |   70,738 goal |  154 cost |  70,868 actions | GridProblem((15, 30), (130, 30))\n",
      "  370,370 nodes |   47,243 goal |  133 cost |  47,358 actions | GridProblem((15, 30), (130, 30))\n",
      "  349,062 nodes |   43,693 goal |  124 cost |  43,807 actions | GridProblem((15, 30), (130, 30))\n",
      "  366,996 nodes |   45,970 goal |  127 cost |  46,084 actions | GridProblem((15, 30), (130, 30))\n",
      "  300,925 nodes |  112,082 goal |   23 cost | 112,104 actions | EightPuzzle((4, 0, 2, 5, 1, 3, 7, 8, 6),\n",
      "  457,766 nodes |  171,571 goal |   26 cost | 171,596 actions | EightPuzzle((7, 2, 4, 5, 0, 6, 8, 3, 1),\n",
      "3,311,831 nodes |  667,891 goal | 3543 cost | 668,783 actions | TOTAL\n",
      "\n"
     ]
    }
   ],
   "source": [
    "def extra_weighted_astar_search(problem): return weighted_astar_search(problem, weight=2)\n",
    "    \n",
    "report((greedy_bfs, extra_weighted_astar_search, weighted_astar_search, astar_search, uniform_cost_search), \n",
    "       (r0, r1, r2, r3, r4, e1, d1, d2, j9, e2, d3, d4, d6, d7, e3, e4))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We see that greedy search expands the fewest nodes, but has the highest path costs. In contrast, A\\* gets optimal path costs, but expands 4 or 5 times more nodes. Weighted A* is a good compromise, using half the compute time as A\\*, and achieving path costs within  1% or 2% of optimal. Uniform-cost is optimal, but is an order of magnitude slower than A\\*.\n",
    "\n",
    "# Comparing  many search algorithms\n",
    "\n",
    "Finally, we compare a host of algorihms (even the slow ones) on some of the easier problems:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 42,
   "metadata": {
    "scrolled": false
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "astar_search:\n",
      "      948 nodes |      109 goal |    4 cost |     112 actions | PourProblem((1, 1, 1), 13)\n",
      "    1,696 nodes |      190 goal |   10 cost |     204 actions | GreenPourProblem((1, 1, 1), 13)\n",
      "    3,499 nodes |      389 goal |    9 cost |     397 actions | PourProblem((0, 0, 0), 21)\n",
      "    4,072 nodes |      454 goal |   21 cost |     463 actions | GreenPourProblem((0, 0, 0), 21)\n",
      "      124 nodes |       30 goal |   14 cost |      43 actions | PourProblem((0, 0), 8)\n",
      "      124 nodes |       30 goal |   35 cost |      45 actions | GreenPourProblem((0, 0), 8)\n",
      "    3,499 nodes |      389 goal |    9 cost |     397 actions | PourProblem((0, 0, 0), 21)\n",
      "    4,072 nodes |      454 goal |   21 cost |     463 actions | GreenPourProblem((0, 0, 0), 21)\n",
      "        0 nodes |        1 goal |    0 cost |       0 actions | RouteProblem('A', 'A')\n",
      "       15 nodes |        6 goal |  418 cost |       9 actions | RouteProblem('A', 'B')\n",
      "       34 nodes |       15 goal |  910 cost |      23 actions | RouteProblem('N', 'L')\n",
      "       33 nodes |       14 goal |  805 cost |      21 actions | RouteProblem('E', 'T')\n",
      "       20 nodes |        9 goal |  445 cost |      13 actions | RouteProblem('O', 'M')\n",
      "       15 nodes |        6 goal |    5 cost |      10 actions | EightPuzzle((1, 4, 2, 0, 7, 5, 3, 6, 8),\n",
      "   18,151 nodes |    2,096 goal | 2706 cost |   2,200 actions | TOTAL\n",
      "\n",
      "uniform_cost_search:\n",
      "      948 nodes |      109 goal |    4 cost |     112 actions | PourProblem((1, 1, 1), 13)\n",
      "    1,696 nodes |      190 goal |   10 cost |     204 actions | GreenPourProblem((1, 1, 1), 13)\n",
      "    3,499 nodes |      389 goal |    9 cost |     397 actions | PourProblem((0, 0, 0), 21)\n",
      "    4,072 nodes |      454 goal |   21 cost |     463 actions | GreenPourProblem((0, 0, 0), 21)\n",
      "      124 nodes |       30 goal |   14 cost |      43 actions | PourProblem((0, 0), 8)\n",
      "      124 nodes |       30 goal |   35 cost |      45 actions | GreenPourProblem((0, 0), 8)\n",
      "    3,499 nodes |      389 goal |    9 cost |     397 actions | PourProblem((0, 0, 0), 21)\n",
      "    4,072 nodes |      454 goal |   21 cost |     463 actions | GreenPourProblem((0, 0, 0), 21)\n",
      "        0 nodes |        1 goal |    0 cost |       0 actions | RouteProblem('A', 'A')\n",
      "       30 nodes |       13 goal |  418 cost |      16 actions | RouteProblem('A', 'B')\n",
      "       42 nodes |       19 goal |  910 cost |      27 actions | RouteProblem('N', 'L')\n",
      "       44 nodes |       20 goal |  805 cost |      27 actions | RouteProblem('E', 'T')\n",
      "       30 nodes |       12 goal |  445 cost |      16 actions | RouteProblem('O', 'M')\n",
      "      124 nodes |       46 goal |    5 cost |      50 actions | EightPuzzle((1, 4, 2, 0, 7, 5, 3, 6, 8),\n",
      "   18,304 nodes |    2,156 goal | 2706 cost |   2,260 actions | TOTAL\n",
      "\n",
      "breadth_first_search:\n",
      "      596 nodes |      597 goal |    4 cost |      73 actions | PourProblem((1, 1, 1), 13)\n",
      "      596 nodes |      597 goal |   15 cost |      73 actions | GreenPourProblem((1, 1, 1), 13)\n",
      "    2,618 nodes |    2,619 goal |    9 cost |     302 actions | PourProblem((0, 0, 0), 21)\n",
      "    2,618 nodes |    2,619 goal |   32 cost |     302 actions | GreenPourProblem((0, 0, 0), 21)\n",
      "      120 nodes |      121 goal |   14 cost |      42 actions | PourProblem((0, 0), 8)\n",
      "      120 nodes |      121 goal |   36 cost |      42 actions | GreenPourProblem((0, 0), 8)\n",
      "    2,618 nodes |    2,619 goal |    9 cost |     302 actions | PourProblem((0, 0, 0), 21)\n",
      "    2,618 nodes |    2,619 goal |   32 cost |     302 actions | GreenPourProblem((0, 0, 0), 21)\n",
      "        0 nodes |        1 goal |    0 cost |       0 actions | RouteProblem('A', 'A')\n",
      "       18 nodes |       19 goal |  450 cost |      10 actions | RouteProblem('A', 'B')\n",
      "       42 nodes |       43 goal | 1085 cost |      27 actions | RouteProblem('N', 'L')\n",
      "       36 nodes |       37 goal |  837 cost |      22 actions | RouteProblem('E', 'T')\n",
      "       30 nodes |       31 goal |  445 cost |      16 actions | RouteProblem('O', 'M')\n",
      "       81 nodes |       82 goal |    5 cost |      35 actions | EightPuzzle((1, 4, 2, 0, 7, 5, 3, 6, 8),\n",
      "   12,111 nodes |   12,125 goal | 2973 cost |   1,548 actions | TOTAL\n",
      "\n",
      "breadth_first_bfs:\n",
      "      948 nodes |      109 goal |    4 cost |     112 actions | PourProblem((1, 1, 1), 13)\n",
      "    1,062 nodes |      124 goal |   15 cost |     127 actions | GreenPourProblem((1, 1, 1), 13)\n",
      "    3,499 nodes |      389 goal |    9 cost |     397 actions | PourProblem((0, 0, 0), 21)\n",
      "    3,757 nodes |      420 goal |   24 cost |     428 actions | GreenPourProblem((0, 0, 0), 21)\n",
      "      124 nodes |       30 goal |   14 cost |      43 actions | PourProblem((0, 0), 8)\n",
      "      124 nodes |       30 goal |   36 cost |      43 actions | GreenPourProblem((0, 0), 8)\n",
      "    3,499 nodes |      389 goal |    9 cost |     397 actions | PourProblem((0, 0, 0), 21)\n",
      "    3,757 nodes |      420 goal |   24 cost |     428 actions | GreenPourProblem((0, 0, 0), 21)\n",
      "        0 nodes |        1 goal |    0 cost |       0 actions | RouteProblem('A', 'A')\n",
      "       28 nodes |       12 goal |  450 cost |      14 actions | RouteProblem('A', 'B')\n",
      "       55 nodes |       24 goal |  910 cost |      32 actions | RouteProblem('N', 'L')\n",
      "       51 nodes |       22 goal |  837 cost |      28 actions | RouteProblem('E', 'T')\n",
      "       40 nodes |       16 goal |  445 cost |      20 actions | RouteProblem('O', 'M')\n",
      "      124 nodes |       46 goal |    5 cost |      50 actions | EightPuzzle((1, 4, 2, 0, 7, 5, 3, 6, 8),\n",
      "   17,068 nodes |    2,032 goal | 2782 cost |   2,119 actions | TOTAL\n",
      "\n",
      "iterative_deepening_search:\n",
      "    6,133 nodes |    6,118 goal |    4 cost |     822 actions | PourProblem((1, 1, 1), 13)\n",
      "    6,133 nodes |    6,118 goal |   15 cost |     822 actions | GreenPourProblem((1, 1, 1), 13)\n",
      "  288,706 nodes |  288,675 goal |    9 cost |  36,962 actions | PourProblem((0, 0, 0), 21)\n",
      "  288,706 nodes |  288,675 goal |   62 cost |  36,962 actions | GreenPourProblem((0, 0, 0), 21)\n",
      "    3,840 nodes |    3,824 goal |   14 cost |     949 actions | PourProblem((0, 0), 8)\n",
      "    3,840 nodes |    3,824 goal |   36 cost |     949 actions | GreenPourProblem((0, 0), 8)\n",
      "  288,706 nodes |  288,675 goal |    9 cost |  36,962 actions | PourProblem((0, 0, 0), 21)\n",
      "  288,706 nodes |  288,675 goal |   62 cost |  36,962 actions | GreenPourProblem((0, 0, 0), 21)\n",
      "        0 nodes |        1 goal |    0 cost |       0 actions | RouteProblem('A', 'A')\n",
      "       27 nodes |       25 goal |  450 cost |      13 actions | RouteProblem('A', 'B')\n",
      "      167 nodes |      173 goal |  910 cost |      82 actions | RouteProblem('N', 'L')\n",
      "      117 nodes |      120 goal |  837 cost |      56 actions | RouteProblem('E', 'T')\n",
      "      108 nodes |      109 goal |  572 cost |      44 actions | RouteProblem('O', 'M')\n",
      "      116 nodes |      118 goal |    5 cost |      47 actions | EightPuzzle((1, 4, 2, 0, 7, 5, 3, 6, 8),\n",
      "1,175,305 nodes |1,175,130 goal | 2985 cost | 151,632 actions | TOTAL\n",
      "\n",
      "depth_limited_search:\n",
      "    4,433 nodes |    4,374 goal |   10 cost |     627 actions | PourProblem((1, 1, 1), 13)\n",
      "    4,433 nodes |    4,374 goal |   30 cost |     627 actions | GreenPourProblem((1, 1, 1), 13)\n",
      "   37,149 nodes |   37,106 goal |   10 cost |   4,753 actions | PourProblem((0, 0, 0), 21)\n",
      "   37,149 nodes |   37,106 goal |   54 cost |   4,753 actions | GreenPourProblem((0, 0, 0), 21)\n",
      "      452 nodes |      453 goal |  inf cost |     110 actions | PourProblem((0, 0), 8)\n",
      "      452 nodes |      453 goal |  inf cost |     110 actions | GreenPourProblem((0, 0), 8)\n",
      "   37,149 nodes |   37,106 goal |   10 cost |   4,753 actions | PourProblem((0, 0, 0), 21)\n",
      "   37,149 nodes |   37,106 goal |   54 cost |   4,753 actions | GreenPourProblem((0, 0, 0), 21)\n",
      "        0 nodes |        1 goal |    0 cost |       0 actions | RouteProblem('A', 'A')\n",
      "       17 nodes |        8 goal |  733 cost |      14 actions | RouteProblem('A', 'B')\n",
      "       40 nodes |       38 goal |  910 cost |      26 actions | RouteProblem('N', 'L')\n",
      "       29 nodes |       23 goal |  992 cost |      20 actions | RouteProblem('E', 'T')\n",
      "       35 nodes |       29 goal |  895 cost |      22 actions | RouteProblem('O', 'M')\n",
      "      351 nodes |      349 goal |    5 cost |     138 actions | EightPuzzle((1, 4, 2, 0, 7, 5, 3, 6, 8),\n",
      "  158,838 nodes |  158,526 goal |  inf cost |  20,706 actions | TOTAL\n",
      "\n",
      "greedy_bfs:\n",
      "      948 nodes |      109 goal |    4 cost |     112 actions | PourProblem((1, 1, 1), 13)\n",
      "    1,696 nodes |      190 goal |   10 cost |     204 actions | GreenPourProblem((1, 1, 1), 13)\n",
      "    3,499 nodes |      389 goal |    9 cost |     397 actions | PourProblem((0, 0, 0), 21)\n",
      "    4,072 nodes |      454 goal |   21 cost |     463 actions | GreenPourProblem((0, 0, 0), 21)\n",
      "      124 nodes |       30 goal |   14 cost |      43 actions | PourProblem((0, 0), 8)\n",
      "      124 nodes |       30 goal |   35 cost |      45 actions | GreenPourProblem((0, 0), 8)\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "    3,499 nodes |      389 goal |    9 cost |     397 actions | PourProblem((0, 0, 0), 21)\n",
      "    4,072 nodes |      454 goal |   21 cost |     463 actions | GreenPourProblem((0, 0, 0), 21)\n",
      "        0 nodes |        1 goal |    0 cost |       0 actions | RouteProblem('A', 'A')\n",
      "        9 nodes |        4 goal |  450 cost |       6 actions | RouteProblem('A', 'B')\n",
      "       29 nodes |       12 goal |  910 cost |      20 actions | RouteProblem('N', 'L')\n",
      "       19 nodes |        8 goal |  837 cost |      14 actions | RouteProblem('E', 'T')\n",
      "       14 nodes |        6 goal |  572 cost |      10 actions | RouteProblem('O', 'M')\n",
      "       15 nodes |        6 goal |    5 cost |      10 actions | EightPuzzle((1, 4, 2, 0, 7, 5, 3, 6, 8),\n",
      "   18,120 nodes |    2,082 goal | 2897 cost |   2,184 actions | TOTAL\n",
      "\n",
      "weighted_astar_search:\n",
      "      948 nodes |      109 goal |    4 cost |     112 actions | PourProblem((1, 1, 1), 13)\n",
      "    1,696 nodes |      190 goal |   10 cost |     204 actions | GreenPourProblem((1, 1, 1), 13)\n",
      "    3,499 nodes |      389 goal |    9 cost |     397 actions | PourProblem((0, 0, 0), 21)\n",
      "    4,072 nodes |      454 goal |   21 cost |     463 actions | GreenPourProblem((0, 0, 0), 21)\n",
      "      124 nodes |       30 goal |   14 cost |      43 actions | PourProblem((0, 0), 8)\n",
      "      124 nodes |       30 goal |   35 cost |      45 actions | GreenPourProblem((0, 0), 8)\n",
      "    3,499 nodes |      389 goal |    9 cost |     397 actions | PourProblem((0, 0, 0), 21)\n",
      "    4,072 nodes |      454 goal |   21 cost |     463 actions | GreenPourProblem((0, 0, 0), 21)\n",
      "        0 nodes |        1 goal |    0 cost |       0 actions | RouteProblem('A', 'A')\n",
      "        9 nodes |        4 goal |  450 cost |       6 actions | RouteProblem('A', 'B')\n",
      "       32 nodes |       14 goal |  910 cost |      22 actions | RouteProblem('N', 'L')\n",
      "       29 nodes |       12 goal |  805 cost |      19 actions | RouteProblem('E', 'T')\n",
      "       18 nodes |        8 goal |  445 cost |      12 actions | RouteProblem('O', 'M')\n",
      "       15 nodes |        6 goal |    5 cost |      10 actions | EightPuzzle((1, 4, 2, 0, 7, 5, 3, 6, 8),\n",
      "   18,137 nodes |    2,090 goal | 2738 cost |   2,193 actions | TOTAL\n",
      "\n",
      "extra_weighted_astar_search:\n",
      "      948 nodes |      109 goal |    4 cost |     112 actions | PourProblem((1, 1, 1), 13)\n",
      "    1,696 nodes |      190 goal |   10 cost |     204 actions | GreenPourProblem((1, 1, 1), 13)\n",
      "    3,499 nodes |      389 goal |    9 cost |     397 actions | PourProblem((0, 0, 0), 21)\n",
      "    4,072 nodes |      454 goal |   21 cost |     463 actions | GreenPourProblem((0, 0, 0), 21)\n",
      "      124 nodes |       30 goal |   14 cost |      43 actions | PourProblem((0, 0), 8)\n",
      "      124 nodes |       30 goal |   35 cost |      45 actions | GreenPourProblem((0, 0), 8)\n",
      "    3,499 nodes |      389 goal |    9 cost |     397 actions | PourProblem((0, 0, 0), 21)\n",
      "    4,072 nodes |      454 goal |   21 cost |     463 actions | GreenPourProblem((0, 0, 0), 21)\n",
      "        0 nodes |        1 goal |    0 cost |       0 actions | RouteProblem('A', 'A')\n",
      "        9 nodes |        4 goal |  450 cost |       6 actions | RouteProblem('A', 'B')\n",
      "       29 nodes |       12 goal |  910 cost |      20 actions | RouteProblem('N', 'L')\n",
      "       23 nodes |        9 goal |  805 cost |      16 actions | RouteProblem('E', 'T')\n",
      "       18 nodes |        8 goal |  445 cost |      12 actions | RouteProblem('O', 'M')\n",
      "       15 nodes |        6 goal |    5 cost |      10 actions | EightPuzzle((1, 4, 2, 0, 7, 5, 3, 6, 8),\n",
      "   18,128 nodes |    2,085 goal | 2738 cost |   2,188 actions | TOTAL\n",
      "\n"
     ]
    }
   ],
   "source": [
    "report((astar_search, uniform_cost_search,  breadth_first_search, breadth_first_bfs, \n",
    "        iterative_deepening_search, depth_limited_search, greedy_bfs, \n",
    "        weighted_astar_search, extra_weighted_astar_search), \n",
    "       (p1, g1, p2, g2, p3, g3, p4, g4, r0, r1, r2, r3, r4, e1))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "This confirms some of the things we already knew: A* and uniform-cost search are optimal, but the others are not. A* explores fewer nodes than uniform-cost. "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Visualizing Reached States\n",
    "\n",
    "I would like to draw a picture of the state space, marking the states that have been reached by the search.\n",
    "Unfortunately, the *reached* variable is inaccessible inside `best_first_search`, so I will define a new version of `best_first_search` that is identical except that it declares *reached* to be `global`. I can then define `plot_grid_problem` to plot the obstacles of a `GridProblem`, along with the initial and goal states, the solution path, and the states reached during a search."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 45,
   "metadata": {},
   "outputs": [],
   "source": [
    "def best_first_search(problem, f):\n",
    "    \"Search nodes with minimum f(node) value first.\"\n",
    "    global reached # <<<<<<<<<<< Only change here\n",
    "    node = Node(problem.initial)\n",
    "    frontier = PriorityQueue([node], key=f)\n",
    "    reached = {problem.initial: node}\n",
    "    while frontier:\n",
    "        node = frontier.pop()\n",
    "        if problem.is_goal(node.state):\n",
    "            return node\n",
    "        for child in expand(problem, node):\n",
    "            s = child.state\n",
    "            if s not in reached or child.path_cost < reached[s].path_cost:\n",
    "                reached[s] = child\n",
    "                frontier.add(child)\n",
    "    return failure\n",
    "\n",
    "\n",
    "def plot_grid_problem(grid, solution, reached=(), title='Search', show=True):\n",
    "    \"Use matplotlib to plot the grid, obstacles, solution, and reached.\"\n",
    "    reached = list(reached)\n",
    "    plt.figure(figsize=(16, 10))\n",
    "    plt.axis('off'); plt.axis('equal')\n",
    "    plt.scatter(*transpose(grid.obstacles), marker='s', color='darkgrey')\n",
    "    plt.scatter(*transpose(reached), 1**2, marker='.', c='blue')\n",
    "    plt.scatter(*transpose(path_states(solution)), marker='s', c='blue')\n",
    "    plt.scatter(*transpose([grid.initial]), 9**2, marker='D', c='green')\n",
    "    plt.scatter(*transpose([grid.goal]), 9**2, marker='8', c='red')\n",
    "    if show: plt.show()\n",
    "    print('{} {} search: {:.1f} path cost, {:,d} states reached'\n",
    "          .format(' ' * 10, title, solution.path_cost, len(reached)))\n",
    "    \n",
    "def plots(grid, weights=(1.4, 2)): \n",
    "    \"\"\"Plot the results of 4 heuristic search algorithms for this grid.\"\"\"\n",
    "    solution = astar_search(grid)\n",
    "    plot_grid_problem(grid, solution, reached, 'A* search')\n",
    "    for weight in weights:\n",
    "        solution = weighted_astar_search(grid, weight=weight)\n",
    "        plot_grid_problem(grid, solution, reached, '(b) Weighted ({}) A* search'.format(weight))\n",
    "    solution = greedy_bfs(grid)\n",
    "    plot_grid_problem(grid, solution, reached, 'Greedy best-first search')\n",
    "    \n",
    "def transpose(matrix): return list(zip(*matrix))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 46,
   "metadata": {
    "scrolled": false
   },
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 29304 characters]\n",
      "text/plain": [
       "<Figure size 1152x720 with 1 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "           A* search search: 154.2 path cost, 7,418 states reached\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 16320 characters]\n",
      "text/plain": [
       "<Figure size 1152x720 with 1 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "           (b) Weighted (1.4) A* search search: 154.2 path cost, 944 states reached\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 15648 characters]\n",
      "text/plain": [
       "<Figure size 1152x720 with 1 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "           (b) Weighted (2) A* search search: 162.8 path cost, 782 states reached\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 13780 characters]\n",
      "text/plain": [
       "<Figure size 1152x720 with 1 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "           Greedy best-first search search: 164.5 path cost, 448 states reached\n"
     ]
    }
   ],
   "source": [
    "plots(d3)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 47,
   "metadata": {
    "scrolled": false
   },
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 18960 characters]\n",
      "text/plain": [
       "<Figure size 1152x720 with 1 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "           A* search search: 133.0 path cost, 2,196 states reached\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 14044 characters]\n",
      "text/plain": [
       "<Figure size 1152x720 with 1 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "           (b) Weighted (1.4) A* search search: 133.0 path cost, 440 states reached\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 13764 characters]\n",
      "text/plain": [
       "<Figure size 1152x720 with 1 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "           (b) Weighted (2) A* search search: 134.2 path cost, 418 states reached\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 14564 characters]\n",
      "text/plain": [
       "<Figure size 1152x720 with 1 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "           Greedy best-first search search: 153.0 path cost, 502 states reached\n"
     ]
    }
   ],
   "source": [
    "plots(d4)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# The cost of weighted A* search\n",
    "\n",
    "Now I want to try a much simpler grid problem, `d6`, with only a few obstacles. We see that A* finds the optimal path, skirting below the obstacles. Weighterd A* with a weight of 1.4 finds the same optimal path while exploring only 1/3 the number of states. But weighted A* with weight 2 takes the slightly longer path above the obstacles, because that path allowed it to stay closer to the goal in straight-line distance, which it over-weights. And greedy best-first search has a bad showing, not deviating from its path towards the goal until it is almost inside the cup made by the obstacles."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 48,
   "metadata": {
    "scrolled": false
   },
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 11728 characters]\n",
      "text/plain": [
       "<Figure size 1152x720 with 1 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "           A* search search: 124.1 path cost, 3,305 states reached\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 8728 characters]\n",
      "text/plain": [
       "<Figure size 1152x720 with 1 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "           (b) Weighted (1.4) A* search search: 124.1 path cost, 975 states reached\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 8800 characters]\n",
      "text/plain": [
       "<Figure size 1152x720 with 1 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "           (b) Weighted (2) A* search search: 128.6 path cost, 879 states reached\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 8160 characters]\n",
      "text/plain": [
       "<Figure size 1152x720 with 1 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "           Greedy best-first search search: 133.9 path cost, 758 states reached\n"
     ]
    }
   ],
   "source": [
    "plots(d6)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "In the next problem, `d7`, we see a similar story. the optimal path found by A*, and we see that again weighted A* with weight 1.4 does great and with weight 2 ends up erroneously going below the first two barriers, and then makes another mistake by reversing direction back towards the goal and passing above the third barrier. Again, greedy best-first makes bad decisions all around."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 49,
   "metadata": {
    "scrolled": false
   },
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 13896 characters]\n",
      "text/plain": [
       "<Figure size 1152x720 with 1 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "           A* search search: 127.4 path cost, 4,058 states reached\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 10532 characters]\n",
      "text/plain": [
       "<Figure size 1152x720 with 1 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "           (b) Weighted (1.4) A* search search: 127.4 path cost, 1,289 states reached\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 10668 characters]\n",
      "text/plain": [
       "<Figure size 1152x720 with 1 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "           (b) Weighted (2) A* search search: 140.4 path cost, 982 states reached\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 9644 characters]\n",
      "text/plain": [
       "<Figure size 1152x720 with 1 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "           Greedy best-first search search: 151.6 path cost, 826 states reached\n"
     ]
    }
   ],
   "source": [
    "plots(d7)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Nondeterministic Actions\n",
    "\n",
    "To handle problems with nondeterministic problems, we'll replace the `result` method with `results`, which returns a collection of possible result states. We'll represent the solution to a problem not with a `Node`, but with a plan that consist of two types of component: sequences of actions, like `['forward', 'suck']`, and condition actions, like\n",
    "`{5: ['forward', 'suck'], 7: []}`, which says that if we end up in state 5, then do `['forward', 'suck']`, but if we end up in state 7, then do the empty sequence of actions."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 50,
   "metadata": {},
   "outputs": [],
   "source": [
    "def and_or_search(problem):\n",
    "    \"Find a plan for a problem that has nondterministic actions.\"\n",
    "    return or_search(problem, problem.initial, [])\n",
    "    \n",
    "def or_search(problem, state, path):\n",
    "    \"Find a sequence of actions to reach goal from state, without repeating states on path.\"\n",
    "    if problem.is_goal(state): return []\n",
    "    if state in path: return failure # check for loops\n",
    "    for action in problem.actions(state):\n",
    "        plan = and_search(problem, problem.results(state, action), [state] + path)\n",
    "        if plan != failure:\n",
    "            return [action] + plan\n",
    "    return failure\n",
    "\n",
    "def and_search(problem, states, path):\n",
    "    \"Plan for each of the possible states we might end up in.\"\n",
    "    if len(states) == 1: \n",
    "        return or_search(problem, next(iter(states)), path)\n",
    "    plan = {}\n",
    "    for s in states:\n",
    "        plan[s] = or_search(problem, s, path)\n",
    "        if plan[s] == failure: return failure\n",
    "    return [plan]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 51,
   "metadata": {},
   "outputs": [],
   "source": [
    "class MultiGoalProblem(Problem):\n",
    "    \"\"\"A version of `Problem` with a colllection of `goals` instead of one `goal`.\"\"\"\n",
    "    \n",
    "    def __init__(self, initial=None, goals=(), **kwds): \n",
    "        self.__dict__.update(initial=initial, goals=goals, **kwds)\n",
    "        \n",
    "    def is_goal(self, state): return state in self.goals\n",
    "    \n",
    "class ErraticVacuum(MultiGoalProblem):\n",
    "    \"\"\"In this 2-location vacuum problem, the suck action in a dirty square will either clean up that square,\n",
    "    or clean up both squares. A suck action in a clean square will either do nothing, or\n",
    "    will deposit dirt in that square. Forward and backward actions are deterministic.\"\"\"\n",
    "    \n",
    "    def actions(self, state): \n",
    "        return ['suck', 'forward', 'backward']\n",
    "    \n",
    "    def results(self, state, action): return self.table[action][state]\n",
    "    \n",
    "    table = {'suck':{1:{5,7}, 2:{4,8}, 3:{7}, 4:{2,4}, 5:{1,5}, 6:{8}, 7:{3,7}, 8:{6,8}},\n",
    "             'forward': {1:{2}, 2:{2}, 3:{4}, 4:{4}, 5:{6}, 6:{6}, 7:{8}, 8:{8}},\n",
    "             'backward': {1:{1}, 2:{1}, 3:{3}, 4:{3}, 5:{5}, 6:{5}, 7:{7}, 8:{7}}}"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Let's find a plan to get from state 1 to the goal of no dirt (states 7 or 8):"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 52,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "['suck', {5: ['forward', 'suck'], 7: []}]"
      ]
     },
     "execution_count": 52,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "and_or_search(ErraticVacuum(1, {7, 8}))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "This plan says \"First suck, and if we end up in state 5, go forward and suck again; if we end up in state 7, do nothing because that is a goal.\"\n",
    "\n",
    "Here are the plans to get to a goal state starting from any one of the 8 states:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 53,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "{1: ['suck', {5: ['forward', 'suck'], 7: []}],\n",
       " 2: ['suck', {8: [], 4: ['backward', 'suck']}],\n",
       " 3: ['suck'],\n",
       " 4: ['backward', 'suck'],\n",
       " 5: ['forward', 'suck'],\n",
       " 6: ['suck'],\n",
       " 7: [],\n",
       " 8: []}"
      ]
     },
     "execution_count": 53,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "{s: and_or_search(ErraticVacuum(s, {7,8})) \n",
    " for s in range(1, 9)}"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Comparing Algorithms on EightPuzzle Problems of Different Lengths"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 54,
   "metadata": {},
   "outputs": [],
   "source": [
    "from functools import lru_cache\n",
    "\n",
    "def build_table(table, depth, state, problem):\n",
    "    if depth > 0 and state not in table:\n",
    "        problem.initial = state\n",
    "        table[state] = len(astar_search(problem))\n",
    "        for a in problem.actions(state):\n",
    "            build_table(table, depth - 1, problem.result(state, a), problem)\n",
    "    return table\n",
    "\n",
    "def invert_table(table):\n",
    "    result = defaultdict(list)\n",
    "    for key, val in table.items():\n",
    "        result[val].append(key)\n",
    "    return result\n",
    "\n",
    "goal = (0, 1, 2, 3, 4, 5, 6, 7, 8)\n",
    "table8 = invert_table(build_table({}, 25, goal, EightPuzzle(goal)))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 78,
   "metadata": {
    "scrolled": false
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "2.6724"
      ]
     },
     "execution_count": 78,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "def report8(table8, M, Ds=range(2, 25, 2), searchers=(breadth_first_search, astar_misplaced_tiles, astar_search)):\n",
    "    \"Make a table of average nodes generated and effective branching factor\"\n",
    "    for d in Ds:\n",
    "        line = [d]\n",
    "        N = min(M, len(table8[d]))\n",
    "        states = random.sample(table8[d], N)\n",
    "        for searcher in searchers:\n",
    "            nodes = 0\n",
    "            for s in states:\n",
    "                problem = CountCalls(EightPuzzle(s))\n",
    "                searcher(problem)\n",
    "                nodes += problem._counts['result']\n",
    "            nodes = int(round(nodes/N))\n",
    "            line.append(nodes)\n",
    "        line.extend([ebf(d, n) for n in line[1:]])\n",
    "        print('{:2} & {:6} & {:5} & {:5} && {:.2f} & {:.2f} & {:.2f}'\n",
    "              .format(*line))\n",
    "\n",
    "        \n",
    "def ebf(d, N, possible_bs=[b/100 for b in range(100, 300)]):\n",
    "    \"Effective Branching Factor\"\n",
    "    return min(possible_bs, key=lambda b: abs(N - sum(b**i for i in range(1, d+1))))\n",
    "\n",
    "def edepth_reduction(d, N, b=2.67):\n",
    "    \n",
    "    \n",
    "\n",
    "from statistics import mean \n",
    "\n",
    "def random_state():\n",
    "    x = list(range(9))\n",
    "    random.shuffle(x)\n",
    "    return tuple(x)\n",
    "\n",
    "meanbf = mean(len(e3.actions(random_state())) for _ in range(10000))\n",
    "meanbf"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 72,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "{0: 1,\n",
       " 1: 2,\n",
       " 2: 4,\n",
       " 3: 8,\n",
       " 4: 16,\n",
       " 5: 20,\n",
       " 6: 36,\n",
       " 7: 60,\n",
       " 8: 87,\n",
       " 9: 123,\n",
       " 10: 175,\n",
       " 11: 280,\n",
       " 12: 397,\n",
       " 13: 656,\n",
       " 14: 898,\n",
       " 15: 1452,\n",
       " 16: 1670,\n",
       " 17: 2677,\n",
       " 18: 2699,\n",
       " 19: 4015,\n",
       " 20: 3472,\n",
       " 21: 4672,\n",
       " 22: 3311,\n",
       " 23: 3898,\n",
       " 24: 1945,\n",
       " 25: 1796,\n",
       " 26: 621,\n",
       " 27: 368,\n",
       " 28: 63,\n",
       " 29: 19,\n",
       " 30: 0}"
      ]
     },
     "execution_count": 72,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "{n: len(v) for (n, v) in table30.items()}"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 67,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "CPU times: user 24min 7s, sys: 11.6 s, total: 24min 19s\n",
      "Wall time: 24min 44s\n"
     ]
    }
   ],
   "source": [
    "%time table30 = invert_table(build_table({}, 30, goal, EightPuzzle(goal)))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 68,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      " 2 &      5 &     6 &     6 && 1.79 & 2.00 & 2.00\n",
      " 4 &     33 &    12 &    12 && 2.06 & 1.49 & 1.49\n",
      " 6 &    128 &    24 &    19 && 2.01 & 1.42 & 1.34\n",
      " 8 &    368 &    48 &    31 && 1.91 & 1.40 & 1.30\n",
      "10 &   1033 &   116 &    48 && 1.85 & 1.43 & 1.27\n",
      "12 &   2672 &   279 &    84 && 1.80 & 1.45 & 1.28\n",
      "14 &   6783 &   678 &   174 && 1.77 & 1.47 & 1.31\n",
      "16 &  17270 &  1683 &   364 && 1.74 & 1.48 & 1.32\n",
      "18 &  41558 &  4102 &   751 && 1.72 & 1.49 & 1.34\n",
      "20 &  91493 &  9905 &  1318 && 1.69 & 1.50 & 1.34\n",
      "22 & 175921 & 22955 &  2548 && 1.66 & 1.50 & 1.34\n",
      "24 & 290082 & 53039 &  5733 && 1.62 & 1.50 & 1.36\n",
      "CPU times: user 6min, sys: 3.63 s, total: 6min 4s\n",
      "Wall time: 6min 13s\n"
     ]
    }
   ],
   "source": [
    "%time report8(table30, 20, range(26, 31, 2))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 70,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "26 & 395355 & 110372 & 10080 && 1.58 & 1.50 & 1.35\n",
      "28 & 463234 & 202565 & 22055 && 1.53 & 1.49 & 1.36\n"
     ]
    },
    {
     "ename": "ZeroDivisionError",
     "evalue": "division by zero",
     "output_type": "error",
     "traceback": [
      "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
      "\u001b[0;31mZeroDivisionError\u001b[0m                         Traceback (most recent call last)",
      "\u001b[0;32m<timed eval>\u001b[0m in \u001b[0;36m<module>\u001b[0;34m()\u001b[0m\n",
      "\u001b[0;32m<ipython-input-69-674fb01939fb>\u001b[0m in \u001b[0;36mreport8\u001b[0;34m(table8, M, Ds, searchers)\u001b[0m\n\u001b[1;32m     11\u001b[0m                 \u001b[0msearcher\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mproblem\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m     12\u001b[0m                 \u001b[0mnodes\u001b[0m \u001b[0;34m+=\u001b[0m \u001b[0mproblem\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_counts\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m'result'\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 13\u001b[0;31m             \u001b[0mnodes\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mint\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mround\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mnodes\u001b[0m\u001b[0;34m/\u001b[0m\u001b[0mN\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m     14\u001b[0m             \u001b[0mline\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mappend\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mnodes\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m     15\u001b[0m         \u001b[0mline\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mextend\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mebf\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0md\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mn\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mn\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mline\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
      "\u001b[0;31mZeroDivisionError\u001b[0m: division by zero"
     ]
    }
   ],
   "source": [
    "%time report8(table30, 20, range(26, 31, 2))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 315,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "0 116 116 ['A']\n",
      "140 0 140 ['A', 'S']\n",
      "0 83 83 ['A']\n",
      "118 0 118 ['A', 'T']\n",
      "0 45 45 ['A']\n",
      "75 0 75 ['A', 'Z']\n",
      "0 176 176 ['B']\n",
      "101 92 193 ['B', 'P']\n",
      "211 0 211 ['B', 'F']\n",
      "0 77 77 ['B']\n",
      "90 0 90 ['B', 'G']\n",
      "0 100 100 ['B']\n",
      "101 0 101 ['B', 'P']\n",
      "0 80 80 ['B']\n",
      "85 0 85 ['B', 'U']\n",
      "0 87 87 ['C']\n",
      "120 0 120 ['C', 'D']\n",
      "0 109 109 ['C']\n",
      "138 0 138 ['C', 'P']\n",
      "0 128 128 ['C']\n",
      "146 0 146 ['C', 'R']\n",
      "0 47 47 ['D']\n",
      "75 0 75 ['D', 'M']\n",
      "0 62 62 ['E']\n",
      "86 0 86 ['E', 'H']\n",
      "0 98 98 ['F']\n",
      "99 0 99 ['F', 'S']\n",
      "0 77 77 ['H']\n",
      "98 0 98 ['H', 'U']\n",
      "0 85 85 ['I']\n",
      "87 0 87 ['I', 'N']\n",
      "0 78 78 ['I']\n",
      "92 0 92 ['I', 'V']\n",
      "0 36 36 ['L']\n",
      "70 0 70 ['L', 'M']\n",
      "0 86 86 ['L']\n",
      "111 0 111 ['L', 'T']\n",
      "0 136 136 ['O']\n",
      "151 0 151 ['O', 'S']\n",
      "0 48 48 ['O']\n",
      "71 0 71 ['O', 'Z']\n",
      "0 93 93 ['P']\n",
      "97 0 97 ['P', 'R']\n",
      "0 65 65 ['R']\n",
      "80 0 80 ['R', 'S']\n",
      "0 127 127 ['U']\n",
      "142 0 142 ['U', 'V']\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "(1.2698088530709188, 1.2059558858330393)"
      ]
     },
     "execution_count": 315,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "from itertools import combinations\n",
    "from statistics import median, mean\n",
    "\n",
    "# Detour index for Romania\n",
    "\n",
    "L = romania.locations\n",
    "def ratio(a, b): return astar_search(RouteProblem(a, b, map=romania)).path_cost / sld(L[a], L[b])\n",
    "nums = [ratio(a, b) for a,b in combinations(L, 2) if b in r1.actions(a)]\n",
    "mean(nums), median(nums) # 1.7, 1.6 # 1.26, 1.2 for adjacent cities"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 300,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "<function __main__.straight_line_distance(A, B)>"
      ]
     },
     "execution_count": 300,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "sld"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.7.0"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
