<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:media="http://search.yahoo.com/mrss/"><channel><title><![CDATA[Ghost API]]></title><description><![CDATA[Thoughts, stories and ideas.]]></description><link>https://api.loudpumpkins.com/</link><image><url>https://api.loudpumpkins.com/favicon.png</url><title>Ghost API</title><link>https://api.loudpumpkins.com/</link></image><generator>Ghost 5.82</generator><lastBuildDate>Thu, 30 Apr 2026 07:55:32 GMT</lastBuildDate><atom:link href="https://api.loudpumpkins.com/rss/" rel="self" type="application/rss+xml"/><ttl>60</ttl><item><title><![CDATA[Heuristic Search Algorithms]]></title><description><![CDATA[<p>In the last article, we saw how a <strong>problem-solving </strong>agent could use search trees to look ahead and find a sequence of actions that will solve a problem, but we faced a drastic issue: <strong>uninformed search trees grow too fast!</strong></p><p>This article will see how we can pick more promising</p>]]></description><link>https://api.loudpumpkins.com/heuristic-search-algorithms/</link><guid isPermaLink="false">668369f017478208e8b21beb</guid><dc:creator><![CDATA[Loud Pumpkins]]></dc:creator><pubDate>Tue, 02 Jul 2024 02:50:58 GMT</pubDate><media:content url="https://api.loudpumpkins.com/content/images/2024/07/featured.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://api.loudpumpkins.com/content/images/2024/07/featured.jpg" alt="Heuristic Search Algorithms"><p>In the last article, we saw how a <strong>problem-solving </strong>agent could use search trees to look ahead and find a sequence of actions that will solve a problem, but we faced a drastic issue: <strong>uninformed search trees grow too fast!</strong></p><p>This article will see how we can pick more promising paths first and put aside the least likely ones for last, known as a <strong>best-first </strong>search or <strong>uniform-cost</strong> search. The idea is that while breadth-first search spreads out in waves of uniform depths (depth 1 -&gt; depth 2 -&gt; &#x2026; ), best-first search spreads out in waves of uniform evaluations ( f(n) ).</p><h2 id="best-first-search"><strong>Best-first Search</strong></h2><p>A very general approach in which we choose a node, n, with <strong>minimum </strong>values of some <strong>evaluation function</strong>, f(n). On each iteration, we choose a node on the frontier with a <strong>minimum f(n)</strong> value, return it if its state is a goal state, and otherwise expand it to generate child nodes. Each child node is added to the frontier if it has not been reached before or is re-added if reached from a less costly path. By employing different evaluation functions, we get different specific algorithms, which we discuss in this article.</p><pre><code class="language-python">def best_first_search(problem: Problem, f: Callable) -&gt; Optional[Node]:
   &quot;&quot;&quot;
   Takes in an implemented `Problem` and returns a `Node` once a solution is
   found or `None` otherwise. Uses a priority queue to always explore nodes
   in the frontier with the lowest f(n) value.

   This algorithm will track reached/explored nodes and will insert a node into
   the frontier if that node has not been reached before, or if the new path has
   a lower cost. To avoid duplicates in the frontier, use an A* search with a
   monotonic heuristic.

   :param problem: Implemented Problem class
   :param f: Callable evaluation function that takes in 1 argument; Node
   :return: Node or None
   &quot;&quot;&quot;
   explored = {}
   frontier = []

   root = Node(problem.initial_state)
   counter = itertools.count()  # tie breaker for heapq
   heapq.heappush(frontier, (f(root), next(counter), root))

   while frontier:
      node: Node = heapq.heappop(frontier)[2]
      if problem.is_goal(node.state):
         return node
      for child in problem.expand(node):
         if child.state in explored \
            and f(child) &gt;= explored[child.state].path_cost:
            continue
         heapq.heappush(frontier, (f(child), next(counter), child))
         explored[node.state] = child
   return None</code></pre><h2 id="heuristic-function"><strong>Heuristic Function</strong></h2><p>A heuristic function will take a node and <strong>evaluate how close it is to the solution</strong>. It uses domain-specific clues to estimate the cheapest path from the given node to a goal. We typically want heuristics that <strong>underestimate </strong>the actual cost ( optimistic heuristic ) rather than <strong>overestimating </strong>( pessimistic heuristic ). If the heuristic is too high, then we don&#x2019;t explore nodes, and we might miss a low-cost solution, but if the estimate is too low, then we tend to expand nodes and then later discard them when the actual cost for the node starts adding up.</p><p>Recall our fictional map of towns from the last article, for which we will create an example of an optimistic heuristic function.</p><figure class="kg-card kg-image-card"><img src="https://api.loudpumpkins.com/content/images/2024/07/state_space_graph-2.jpg" class="kg-image" alt="Heuristic Search Algorithms" loading="lazy" width="1045" height="675" srcset="https://api.loudpumpkins.com/content/images/size/w600/2024/07/state_space_graph-2.jpg 600w, https://api.loudpumpkins.com/content/images/size/w1000/2024/07/state_space_graph-2.jpg 1000w, https://api.loudpumpkins.com/content/images/2024/07/state_space_graph-2.jpg 1045w" sizes="(min-width: 720px) 720px"></figure><p>Our heuristic will return the straight line distance between any node and the goal from that map. And we use that distance as an <strong>estimate </strong>to evaluate a node. It is <strong>optimistic </strong>because a straight line between two points will always be less than or equal to any other path.</p><figure class="kg-card kg-image-card"><img src="https://api.loudpumpkins.com/content/images/2024/07/heuristic_map.jpg" class="kg-image" alt="Heuristic Search Algorithms" loading="lazy" width="1053" height="628" srcset="https://api.loudpumpkins.com/content/images/size/w600/2024/07/heuristic_map.jpg 600w, https://api.loudpumpkins.com/content/images/size/w1000/2024/07/heuristic_map.jpg 1000w, https://api.loudpumpkins.com/content/images/2024/07/heuristic_map.jpg 1053w" sizes="(min-width: 720px) 720px"></figure><h2 id="greedy-best-first-search"><strong>Greedy Best-first Search</strong></h2><p>A <strong>greedy best-first search</strong> is a form of best-first search that expands the node with the lowest heuristic value or, in other words, the node that appears to be the most promising. And recall that a <strong>best-first search</strong> algorithm will pick the node with the lowest evaluation function. So, a greedy best-first search is a best-first search where <strong>f(n) = h(n)</strong>.</p><p>As an example, we will look for a path from A to Z using the greedy approach.</p><p>Our heuristic values:</p>
<!--kg-card-begin: html-->
<table style="box-sizing: inherit; border-spacing: 0px; border-collapse: collapse; background-color: transparent; margin: 0px 0px 1.5em; width: 1408px;"><tbody style="box-sizing: inherit;"><tr style="box-sizing: inherit;"><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">h(A) = 366</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">h(E) = 380</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">h(J) = 100</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">h(N) = 80</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">h(R) = 226</td></tr><tr style="box-sizing: inherit;"><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">h(B) = 374</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">h(F) = 176</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">h(K) = 160</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">h(O) = 77</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">h(S) = 161</td></tr><tr style="box-sizing: inherit;"><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">h(C) = 253</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">h(G) = 193</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">h(L) = 241</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">h(P) = 199</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">h(T) = 234</td></tr><tr style="box-sizing: inherit;"><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">h(D) = 329</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">h(H) = 244</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">h(M) = 242</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">h(Q) = 151</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">h(Z) = 0</td></tr></tbody></table>
<!--kg-card-end: html-->
<p>The algorithm will create a search tree internally, just like uninformed searches. Still, I find it easier to superimpose the search tree onto the state graph to demonstrate better how the frontier is expanded closer and closer toward the goal.</p><figure class="kg-card kg-image-card"><img src="https://api.loudpumpkins.com/content/images/2024/07/greedy_best-first_search_graph.jpg" class="kg-image" alt="Heuristic Search Algorithms" loading="lazy" width="1156" height="301" srcset="https://api.loudpumpkins.com/content/images/size/w600/2024/07/greedy_best-first_search_graph.jpg 600w, https://api.loudpumpkins.com/content/images/size/w1000/2024/07/greedy_best-first_search_graph.jpg 1000w, https://api.loudpumpkins.com/content/images/2024/07/greedy_best-first_search_graph.jpg 1156w" sizes="(min-width: 720px) 720px"></figure><p>We first expand our initial node A, and we get three successor nodes; B, C and D. Looking at our heuristic values table, we determine that C is the closest to the goal, so we expand it and add its successors to the frontier. We continue to do so until we reach the goal. Notice that the algorithm did not expand a single node that is not on the path to the solution. However, the solution found is <strong>not the optimal solution</strong>. The optimal path is &#x2018;go_to_C&#x2019;, &#x2018;go_to_G&#x2019;, &#x2018;go_to_J&#x2019;, &#x2018;go_to_Z&#x2019;, but our algorithm did not find it. Greedy best-first search is not concerned with the cost already incurred nor the cost to reach a promising node. It just expands the most promising node from the frontier, and this is why it&#x2019;s called a <strong>greedy approach</strong>.</p><p>When using this approach, we keep track of all visited nodes, and so, space and time complexity is equal to the size of the state space. And, assuming that the state space is finite, greedy search is a <strong>complete</strong> search algorithm.</p><h2 id="a-search"><strong>A* search</strong></h2><p>A more common approach to solving search problems is the <strong>A-star search</strong>, which is a best-first search that employs the heuristic function as well, but unlike greedy best-first search, A-star search will include the cost incurred to reach a node in conjunction with the heuristic value. And so, its evaluation function is <strong>f(n) = g(n) + h(n)</strong> where g(n) is the cost to reach a node n from the initial state and h(n) is the heuristic value of that same node.</p><p>In other words,<strong> the evaluation function returns the exact cost to reach node n plus an estimate of the cost remaining to reach the goal from node n</strong>.</p><p>We will do another example using the same heuristic table and search problem as we did for the greedy best-first search, but this time using A* search.</p><figure class="kg-card kg-image-card"><img src="https://api.loudpumpkins.com/content/images/2024/07/a_star_search_graph.jpg" class="kg-image" alt="Heuristic Search Algorithms" loading="lazy" width="1095" height="667" srcset="https://api.loudpumpkins.com/content/images/size/w600/2024/07/a_star_search_graph.jpg 600w, https://api.loudpumpkins.com/content/images/size/w1000/2024/07/a_star_search_graph.jpg 1000w, https://api.loudpumpkins.com/content/images/2024/07/a_star_search_graph.jpg 1095w" sizes="(min-width: 720px) 720px"></figure><p>There are a few things to note here. Contrary to the greedy search, A-star&#x2019;s frontier expands conservatively yet steadily towards the goal. Also, notice that we had to explore more nodes to find a solution because A* search is careful. It also perceived the less optimal solution when it expanded node F (4th graph) with an f(n) value of 450, but another node, J, saw a possible path to the goal with an f(n) value of 416. And so, the algorithm explored node J before returning a solution that it had already found. Sure enough, the path through J found another, better solution. In fact, it&#x2019;s the cost-optimal solution.</p><p>Whether A* is cost-optimal depends entirely on two properties of the heuristic function selected. It needs to be <strong>admissible</strong> and <strong>monotonic</strong>.</p><h3 id="admissibility"><strong>Admissibility</strong></h3><p>An <strong>admissible heuristic</strong> is one that never overestimates the cost to reach a goal. And as long as your heuristic is admissible, you will get a cost-optimal solution. The intuition behind it is that an admissible heuristic will always return the <strong>minimum cost</strong>, and at the very least, you will need to spend g(n) + h(n) to go down path n. If you found a solution that costs less than all the evaluation values in the frontier, you are guaranteed to have found the optimal solution. This can be shown more formally through a <strong>proof by contradiction</strong>.</p><p>Suppose an optimal solution exists with a cost C*, but our algorithm returned another solution with a cost C, greater than C*. Then that means a node n that is on the optimal path has not been expanded. And because we are using a best-first search algorithm, it must mean that the evaluation value of that node was greater than C, and thus, greater than C* (otherwise, we would have expanded it). For this proof, we denote c(n, z) as the optimal cost from node n to the goal.</p><figure class="kg-card kg-image-card"><img src="https://api.loudpumpkins.com/content/images/2024/07/image-2.png" class="kg-image" alt="Heuristic Search Algorithms" loading="lazy" width="703" height="90" srcset="https://api.loudpumpkins.com/content/images/size/w600/2024/07/image-2.png 600w, https://api.loudpumpkins.com/content/images/2024/07/image-2.png 703w"></figure><p>by A* definition, we get</p><figure class="kg-card kg-image-card"><img src="https://api.loudpumpkins.com/content/images/2024/07/image-3.png" class="kg-image" alt="Heuristic Search Algorithms" loading="lazy" width="671" height="60" srcset="https://api.loudpumpkins.com/content/images/size/w600/2024/07/image-3.png 600w, https://api.loudpumpkins.com/content/images/2024/07/image-3.png 671w"></figure><p>because h(n) is admissible, we get</p><figure class="kg-card kg-image-card"><img src="https://api.loudpumpkins.com/content/images/2024/07/image-4.png" class="kg-image" alt="Heuristic Search Algorithms" loading="lazy" width="656" height="110" srcset="https://api.loudpumpkins.com/content/images/size/w600/2024/07/image-4.png 600w, https://api.loudpumpkins.com/content/images/2024/07/image-4.png 656w"></figure><p>because n is on the optimal path, the cost to reach n in conjunction with the optimal cost to reach the goal from n, we get the optimal cost, C*.</p><figure class="kg-card kg-image-card"><img src="https://api.loudpumpkins.com/content/images/2024/07/image-5.png" class="kg-image" alt="Heuristic Search Algorithms" loading="lazy" width="621" height="124" srcset="https://api.loudpumpkins.com/content/images/size/w600/2024/07/image-5.png 600w, https://api.loudpumpkins.com/content/images/2024/07/image-5.png 621w"></figure><p>The first and last lines form a contradiction, so it is not possible to have a node on the optimal path unexplored, and thus, impossible to return a suboptimal solution.</p><h3 id="monotonicity"><strong>Monotonicity</strong></h3><p>A stronger property of a heuristic function is <strong>monotonicity</strong> or sometimes called <strong>consistency</strong>. A heuristic is monotonic if the evaluation function is monotonically increasing as we explore a path. Or in other words, it means that if my heuristic evaluates a node at a particular value, not only will it be an optimistic estimate, but it is also the minimum cost to pursue a solution through that node.</p><p>More formally, we have:</p><figure class="kg-card kg-image-card"><img src="https://api.loudpumpkins.com/content/images/2024/07/image-6.png" class="kg-image" alt="Heuristic Search Algorithms" loading="lazy" width="658" height="66" srcset="https://api.loudpumpkins.com/content/images/size/w600/2024/07/image-6.png 600w, https://api.loudpumpkins.com/content/images/2024/07/image-6.png 658w"></figure><p>Where n is any node and n&#x2019; is any of its successors. This inequality must hold for every node, so it&#x2019;s true for its successors n&#x2019; as well. And so we have:</p><figure class="kg-card kg-image-card"><img src="https://api.loudpumpkins.com/content/images/2024/07/image-7.png" class="kg-image" alt="Heuristic Search Algorithms" loading="lazy" width="727" height="72" srcset="https://api.loudpumpkins.com/content/images/size/w600/2024/07/image-7.png 600w, https://api.loudpumpkins.com/content/images/2024/07/image-7.png 727w" sizes="(min-width: 720px) 720px"></figure><p>And from here, you can see a pattern. Once h(n) is evaluated, it can only increase or stay the same. Hence the name monotonic.</p><h3 id="admissible-vs-monotonic"><strong>Admissible vs monotonic</strong></h3><p>Every monotonic heuristic is admissible, but not every admissible heuristic is monotonic. And so, just like an admissible heuristic, a monotonic heuristic will return a cost-optimal solution. But in addition, with a monotonic heuristic, the first time we reach a node, it will be on an optimal path, so we never have to re-add a node to the frontier.</p><p>If it cannot be shown that a heuristic is monotonically increasing, then we cannot guarantee that every node in the frontier has the lowest evaluation value. We may reach a node in the frontier from a different path with a lower cost. Suppose a heuristic is admissible but not monotonic. In that case, we have to keep track of all the nodes in the frontier and update their values, parent and successors when we find a better path, or we must introduce duplicate nodes in our frontier. For this reason, it&#x2019;s best to use a monotonic heuristic if possible.</p><h3 id="inadmissible-heuristics"><strong>Inadmissible heuristics</strong></h3><p>As powerful as an A* search can be, it can sometimes have a high space and time complexity. An admissible heuristic cannot take risks because it needs to guarantee a minimal cost. This restriction can return conservative heuristic values and rank all successors equally. In such a case, an A* search can turn into a breadth-first search. But if we are willing to accept the possibility of a suboptimal solution, we may use an inadmissible heuristic. A heuristic that may overestimate the cost but does a much better job of estimating a solution&#x2019;s cost.</p><h2 id="inventing-heuristic-functions"><strong>Inventing heuristic functions</strong></h2><p>Sometimes inventing a good heuristic can be challenging, so I want to pass along a helpful tip before I end this article. Take an 8-puzzle problem as an example.</p><p>For those not familiar with an 8-puzzle problem, the premise is simple. You are presented with a board that can fit nine tiles, but only eight are present. You must rearrange the tiles until the numbers are sorted, like in the goal image below.</p><figure class="kg-card kg-image-card"><img src="https://api.loudpumpkins.com/content/images/2024/07/8Puzzle.jpg" class="kg-image" alt="Heuristic Search Algorithms" loading="lazy" width="1031" height="174" srcset="https://api.loudpumpkins.com/content/images/size/w600/2024/07/8Puzzle.jpg 600w, https://api.loudpumpkins.com/content/images/size/w1000/2024/07/8Puzzle.jpg 1000w, https://api.loudpumpkins.com/content/images/2024/07/8Puzzle.jpg 1031w" sizes="(min-width: 720px) 720px"></figure><p>There is only one rule:</p><p><strong>You can move tile X to position Y if Y is adjacent to X and if Y is empty.</strong></p><p>A concrete implementation of an 8-Puzzle problem</p><pre><code class="language-python">class Puzzle(Problem):
   &quot;&quot;&quot;
   The values of the state member variables of instances of this class are
   represented as a string as follows:

   &quot;12345678_&quot; where &apos;_&apos; represents the empty tile.

   The inital state of the Puzzle problem is completely random.
   &quot;&quot;&quot;

   def actions(self, state):
      &quot;&quot;&quot;
      Actions labeled with &apos;U&apos;, &apos;D&apos;, &apos;L&apos; or &apos;R&apos; indicate the direction taken
      by the blank space.

      eg: +-------+      +-------+
         | _ 2 3 |      | 1 2 3 |
         | 1 4 6 | -D-&gt; | _ 4 6 |
         | 7 5 8 |      | 7 5 8 |
         +-------+      +-------+
      &quot;&quot;&quot;
      actions = []
      index = state.index(&apos;_&apos;)
      if int(index / 3) &gt; 0:
         # &quot;move up allowed&quot;
         actions.append(&apos;U&apos;)

      if int(index / 3) &lt; 2:
         # &quot;move down allowed&quot;
         actions.append(&apos;D&apos;)

      if index % 3 &gt; 0:
         # &quot;move left allowed&quot;
         actions.append(&apos;L&apos;)

      if index % 3 &lt; 2:
         # &quot;move right allowed&quot;
         actions.append(&apos;R&apos;)

      return actions

   def is_goal(self, state):
      &quot;&quot;&quot;
      Return True if the problem is solved.  I.e.: human and all items are on
      the right side.
      &quot;&quot;&quot;
      return state == &apos;12345678_&apos;

   def transition(self, state, action):
      &quot;&quot;&quot;
      Paths labeled with &apos;U&apos;, &apos;D&apos;, &apos;L&apos; or &apos;R&apos; indicate the direction taken by the
      blank space.

      eg: +-------+      +-------+
         | _ 2 3 |      | 1 2 3 |
         | 1 4 6 | -D-&gt; | _ 4 6 |
         | 7 5 8 |      | 7 5 8 |
         +-------+      +-------+
      &quot;&quot;&quot;
      index = state.index(&apos;_&apos;)
      if action == &apos;U&apos;:
         # &quot;move up&quot;
         new_state = list(state)
         new_state[index - 3], new_state[index] = new_state[index], new_state[index - 3]
         return &apos;&apos;.join(new_state)

      if action == &apos;D&apos;:
         # &quot;move down&quot;
         new_state = list(state)
         new_state[index + 3], new_state[index] = new_state[index], new_state[index + 3]
         return &apos;&apos;.join(new_state)

      if action == &apos;L&apos;:
         # &quot;move left&quot;
         new_state = list(state)
         new_state[index - 1], new_state[index] = new_state[index], new_state[index - 1]
         return &apos;&apos;.join(new_state)

      if action == &apos;R&apos;:
         # &quot;move right&quot;
         new_state = list(state)
         new_state[index + 1], new_state[index] = new_state[index], new_state[index + 1]
         return &apos;&apos;.join(new_state)</code></pre><p>One method to create a heuristic for this problem is to use what&#x2019;s known as a relaxed problem approach. We relax the restrictions and open more edges between nodes in the state graph. The added edges will simplify the computation needed to find a solution, and we use the solution to the simplified problem as a heuristic to our original problem. Because we are exploring the same state graph but with more paths, we are guaranteed to find an optimistic solution. For example, with the 8-puzzle problem, we can remove the restriction that Y must be adjacent to X and Y must be empty. The relaxed problem becomes:</p><p><strong>You can move tile X to position Y </strong><s>if Y is adjacent to X and if Y is empty</s>.</p><p>This heuristic is called the &#x2018;misplaced tiles&#x2019; heuristic. It will always underestimate the true cost because it is synonymous with you just lifting a tile and placing it where it belongs. For each tile that isn&#x2019;t where it is supposed to be, it will cost you at least one action to move it.</p><p>Or, we can remove the restriction that Y must be empty, and we get:</p><p><strong>You can move tile X to position Y if Y is adjacent to X </strong><s>and if Y is empty</s>.</p><p>The result is a heuristic that measures the sum of the distances of the tiles from their goal positions. Because this heuristic has more restrictions than the misplaced tiles heuristic, it is better at estimating the true cost, and thus, will find a solution quicker. But because it has fewer restrictions than the original problem, it is still an admissible heuristic.</p><p>Solving an 8-puzzle using a misplaced tiles heuristic:</p><pre><code class="language-python">def misplaced_tiles_heuristic(node: Node) -&gt; int:
   &quot;&quot;&quot;
   Return the heuristic value of a given node. The heuristic will count how
   many tiles are not in their designated position which indicates a minimum
   number of moves needed to solve the problem.

   :param node: Node
   :return: int
   &quot;&quot;&quot;
   heuristic = -1
   for (e, i) in zip(node.state, &apos;12345678*&apos;):
      heuristic += int(e == i)
   return heuristic</code></pre><p>Solution:</p><pre><code class="language-python">initial_state = &apos;_13425786&apos;
print(&apos;Initial State:&apos;)
print_puzzle(initial_state)
problem = Puzzle(initial_state)

node = a_star_search(problem, misplaced_tiles_heuristic)
if node is not None:
   solution = node.solution()
   print(&quot;Solution:&quot;, &quot;&quot;.join(solution))
else:
   print(&quot;No solution found&quot;)</code></pre><pre><code class="language-python">Initial State:
 +-------+
 | _ 1 3 |
 | 4 2 5 |
 | 7 8 6 |
 +-------+
Solution: RDRD</code></pre>]]></content:encoded></item><item><title><![CDATA[What is an intelligent agent]]></title><description><![CDATA[<h1 id="the-fundamentals-of-an-intelligent-program"><strong>The fundamentals of an intelligent program.</strong></h1><p>What is AI?</p><p>Not in the philosophical sense, &#x201C;Can only humans think? Is AI about making machines act like they are thinking? Or does thought transcend the human species, and biological organisms?&#x201D;</p><p>No. I mean the practical definition. What is AI for</p>]]></description><link>https://api.loudpumpkins.com/what-is-an-intelligent-agent/</link><guid isPermaLink="false">668368c817478208e8b21bd0</guid><dc:creator><![CDATA[Loud Pumpkins]]></dc:creator><pubDate>Tue, 02 Jul 2024 02:45:40 GMT</pubDate><media:content url="https://api.loudpumpkins.com/content/images/2024/07/1_intelligent_agent.png" medium="image"/><content:encoded><![CDATA[<h1 id="the-fundamentals-of-an-intelligent-program"><strong>The fundamentals of an intelligent program.</strong></h1><img src="https://api.loudpumpkins.com/content/images/2024/07/1_intelligent_agent.png" alt="What is an intelligent agent"><p>What is AI?</p><p>Not in the philosophical sense, &#x201C;Can only humans think? Is AI about making machines act like they are thinking? Or does thought transcend the human species, and biological organisms?&#x201D;</p><p>No. I mean the practical definition. What is AI for someone trying to solve a real-world problem?</p><p>In practice, AI is a set of design principles for building <strong>successful</strong> systems that can be called <strong>intelligent,</strong> where &#x2018;successful&#x2019; and &#x2018;intelligent&#x2019; are left for you to decide. Those systems are known as <strong>intelligent agents.</strong></p><p>An <strong>agent </strong>is anything that can be viewed as perceiving its <strong>environment</strong> through <strong>sensors</strong> and acting upon that environment through <strong>actuators</strong>. Think of a robot for example as the agent, the cameras, microphones, thermometers are the sensors, and the stepper motors controlling the robot&#x2019;s various components are the actuators.</p><p>The environment could be anything you define to be relevant to the agent. It could be the actual world we live in, or it could be a small set of states. It&#x2019;s the part that <strong>percepts</strong> come from and <strong>actions</strong> sent to.</p><p>The agent will rely on the percepts perceived by the sensors and its own built-in <strong>knowledge base</strong> to decide what action to take. It may have to analyze its entire percept sequence to make an intelligent decision. This mapping from percepts to actions is what&#x2019;s known as an <strong>agent function</strong>.</p><p>The simplest agent implementation of this agent function is to hard code all possible combinations of percepts and all possible combinations of sequences of those percepts and manually assign an action to each one. This implementation of the agent function is called an agent program. It is one way to implement the agent function. Actually, it&#x2019;s a terrible way to implement an agent function for any nontrivial problem, but it is a valid agent program.</p><p>In fact, the discussion of various ways to implement various agent functions is at the core of this site. But before we start solving problems using intelligent agents, we must first fully define the problem.</p><h2 id="the-task-environment-and-its-properties"><strong>The task environment and its properties</strong></h2><p>The task environment is essentially the problem that our agent is trying to solve. For example, if we were building a self-driving vehicle, we would specify it as such:</p>
<!--kg-card-begin: html-->
<table style="box-sizing: inherit; border-spacing: 0px; border-collapse: collapse; background-color: transparent; margin: 0px 0px 1.5em; width: 1408px;"><tbody style="box-sizing: inherit;"><tr style="box-sizing: inherit;"><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;"><strong style="box-sizing: inherit; font-weight: bold;">Agent</strong></td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;"><strong style="box-sizing: inherit; font-weight: bold;"><a class="glossaryLink" aria-describedby="tt" data-cmtooltip="&lt;div class=glossaryItemTitle&gt;performance measure&lt;/div&gt;&lt;div class=glossaryItemBody&gt;&amp;lt;!-- wp:paragraph --&amp;gt;An agent will generate a sequence of actions according to the percepts it receives. Some sequences are better then others and it&amp;#039;s the performance measure that evaluates any given sequence of environment states.&amp;lt;br/&amp;gt;&amp;lt;!-- /wp:paragraph --&amp;gt;&lt;/div&gt;" href="https://loudpumpkins.com/glossary/performance-measure/?ref=api.loudpumpkins.com" data-gt-translate-attributes="[{&quot;attribute&quot;:&quot;data-cmtooltip&quot;, &quot;format&quot;:&quot;html&quot;}]" style="box-sizing: inherit; background-color: transparent; color: rgb(0, 0, 0); text-decoration: none !important; outline: 0px; border-bottom: 1px none rgb(0, 0, 0); box-shadow: rgb(255, 0, 60) 0px -2px 0px inset;">Performance Measure</a></strong></td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;"><strong style="box-sizing: inherit; font-weight: bold;">Environment</strong></td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;"><strong style="box-sizing: inherit; font-weight: bold;">Actuators</strong></td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;"><strong style="box-sizing: inherit; font-weight: bold;">Sensors</strong></td></tr><tr style="box-sizing: inherit;"><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">Self-driving vehicle</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">Safe, legal, comfortable ride, minimize fuel consumption, maximize time efficiency.</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">Roads, other vehicles, pedestrians, debris, wildlife, weather.</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">Steering column, engine throttle, brake cylinder, signals, horn.</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">Cameras, radar, speedometer, GPS, engine sensors, microphones.</td></tr></tbody></table>
<!--kg-card-end: html-->
<p>The <strong>performance measure </strong>is what describes good scenarios and what we want our actions to lead to. In our example, some desirable qualities for the self-driving car are to be safe, legal, comfortable, etc. Note that some desired qualities conflict (such as &#x2018;safe, yet fast&#x2019;), so the agent will need to find means to maximize time efficiency without putting us in danger.</p><p>The <strong>environment</strong> in our example is anything that our vehicle would care about. The roads, other vehicles, pedestrians, wildlife, the weather, etc. Note that elements of the environment may be simple objects (e.g. road debris), but it may be other intelligent agents (e.g. other vehicles).</p><p>And finally, the <strong>actuators </strong>and <strong>sensors</strong> are the different components that let the vehicle interact with the world.</p><p>The range of task environments that might arise in AI is vast, but they do tend to share common properties which we can use to categorize environments. Once an environment is categorized, we can pick known algorithms that are suited to solve them or we can build upon a family of existing techniques.</p><h3 id="1fully-observable-vs-partially-observable"><strong>1- FULLY OBSERVABLE vs. PARTIALLY OBSERVABLE</strong></h3><p>If an agent&#x2019;s sensors have access to the <strong>state</strong> of all the relevant elements <strong>at all times</strong>, then it is a fully observable environment. On the other hand, a partially observable environment has access to a portion of the states of the environment. Fully observable environments are easier to handle as you do not need to maintain a record of states and infer probabilities of states of relevant components. You might even have an unobservable environment which can still be solved (with increased difficulty) if you have a way to measure performance.</p><p>Examples</p><p>Observable: [Chess] &#x2013; The agent knows precisely where each piece is located, who owns it, who&#x2019;s turn it is and the agent has access to this information at any point in time.</p><p>Partially observable: [Self-driving vehicle] &#x2013; The agent has complete access to <strong>some</strong> of the information such as the current speed, a view of its immediate surrounding, weather conditions, road conditions, but it does not know if another vehicle is approaching around the corner, or what traffic is like a few intersections away.</p><h3 id="2single-agent-vs-multiagent"><strong>2- SINGLE-AGENT vs. MULTIAGENT</strong></h3><p>Are other agents involved in the task environment? In a sudoku puzzle, no. But in a game of chess; absolutely. Simple enough, but sometimes it may not be clear if some elements of the task environment should be agents or simple objects. For example, in the self-driving vehicle environment, other vehicles are also agents. But what about pedestrians? Animals? Or fire hydrants?</p><p>For that, we observe the <strong>performance measure</strong> of those elements. Other vehicles have the same desired outcome of &#x2018;safety&#x2019;, and thus, working together to &#x2018;not collide in one another&#x2019; is for each other&#x2019;s benefit. Our agent and other vehicles form a <strong>cooperative multiagent environment</strong>. In turn, when two elements work towards maximizing their performance measure minimizing the other elements performance measure, such as in a game of chess, it is known as a <strong>competitive multiagent environment</strong>.</p><h3 id="3deterministic-vs-nondeterministic"><strong>3- DETERMINISTIC vs. NONDETERMINISTIC</strong></h3><p>If the agent is aware with absolute certainty of the effects of all its actions on the environment and the following state, given any current state, then it&#x2019;s a deterministic environment. But if the agent does not know with certainty what might happen next, it is a nondeterministic environment. Some textbooks will refer to the word &#x2018;stochastic&#x2019; as a synonym for &#x2018;nondeterministic&#x2019;. Furthermore, some textbooks will make a distinction between &#x2018;stochastic&#x2019; and &#x2018;nondeterministic&#x2019; and define a task environment to be &#x2018;stochastic&#x2019; if it explicitly deals with probabilities. (&#x201C;There is a 60% probability of a collision given X&#x201D; vs &#x201C;This action might lead to a collision&#x201D;)</p><p>Examples</p><p>Deterministic: [Chess] &#x2013; The agent knows precisely what actions it can take and their consequences. It also knows of all the actions the competing agent can take and their consequences.</p><p>Nondeterministic: [Self-driving vehicles] &#x2013; Just like most real-world problems, the agent never really knows how the vehicle will react when controlled. The brakes may malfunction, the engine may seize, the tires may slip. The agent is able to make good predictions, but not with absolute certainty.</p><h3 id="4episodic-vs-sequential"><strong>4- EPISODIC vs. SEQUENTIAL</strong></h3><p>In an <strong>episodic </strong>task environment, the agent&#x2019;s decisions are based on the <strong>present percepts</strong>. They are not influenced by past states or decisions and will not impact future states nor decisions. Think of a facial detection AI. Every image fed to the AI is an atomic event that is not influenced by previous images nor future ones. In contrast, a <strong>sequential</strong> environment <strong>has long-term consequences</strong> for present decisions. Sequential environments add another level of complexity because the agent now needs to look ahead before taking an action.</p><h3 id="5static-vs-dynamic"><strong>5- STATIC vs. DYNAMIC</strong></h3><p>Can the environment change while the agent is processing? If so, it is a <strong>dynamic</strong> environment. A self-driving car&#x2019;s environment will constantly change in real-time whether the agent has deliberated an action yet or not. Time is of the essence. In a <strong>static</strong> environment, time essential stops while the agent is thinking. For example, in chess, the state of the board will not change until the agent takes an action. It is not to say that time is not part of the environment. It can and likely will be part of the performance measure. Swordfish (the leading chess agent), will search about 20-30 flops (half moves) ahead to satisfy its &#x2018;time&#x2019; performance measure.</p><h3 id="6discrete-vs-continuous"><strong>6- DISCRETE vs. CONTINUOUS</strong></h3><p>This property refers to the <strong>range of values</strong> the agent&#x2019;s percepts, actions and states can hold. In the chess example, each percept, state, and action are <strong>distinct and finite</strong>. This is known as a <strong>discrete</strong> environment. In contrast, a self-driving vehicle&#x2019;s percepts are continuous (speed, time, video feed, audio feed, etc.) and so are the actions (steering column angle, pressure on the engine throttle, etc.). Even the environment itself is continuous (with respect to time). The self-driving vehicle agent is part of a <strong>continuous</strong> environment.</p><h3 id="7known-vs-unknown"><strong>7- KNOWN vs. UNKNOWN</strong></h3><p>Although it is a property of the task environment, <strong>known</strong> and <strong>unknown</strong> task environments refer to the agent&#x2019;s knowledge of the environment. You can think of it as &#x2018;understood&#x2019;. An environment might be a fully observable environment, but completely unknown. For example, an agent interacting with a game whose rules are unknown. The agent is usually forced to experiment and learn the rules given some performance measure. In contrast, in a known environment, the consequences of each action are known (or the probability of a consequence in a stochastic environment).</p><h3 id="summary"><strong>Summary</strong></h3><p>Those are the fundamental properties of the task environments. It is interesting to note that the hardest problem to solve with intelligent agents is one that is <strong>partially observable, multiagent, nondeterministic, sequential, dynamic, continuous,</strong> and <strong>unknown</strong>. This almost perfectly describes the self-driving vehicle agent (except for the unknown; the consequences of our actions are known), yet, engineers all over the world are making great progress in this field!</p><p>Examples (omitted known/unknown as it depends on implementation):</p>
<!--kg-card-begin: html-->
<table style="box-sizing: inherit; border-spacing: 0px; border-collapse: collapse; background-color: transparent; margin: 0px 0px 1.5em; width: 1408px;"><tbody style="box-sizing: inherit;"><tr style="box-sizing: inherit;"><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;"><strong style="box-sizing: inherit; font-weight: bold;">Task Environment</strong></td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;"><strong style="box-sizing: inherit; font-weight: bold;">Observable</strong></td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;"><strong style="box-sizing: inherit; font-weight: bold;">Agents</strong></td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;"><strong style="box-sizing: inherit; font-weight: bold;">Deterministic</strong></td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;"><strong style="box-sizing: inherit; font-weight: bold;">Episodic</strong></td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;"><strong style="box-sizing: inherit; font-weight: bold;">Static</strong></td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;"><strong style="box-sizing: inherit; font-weight: bold;">Discrete</strong></td></tr><tr style="box-sizing: inherit;"><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">Chess</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">Fully</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">Multi</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">Deterministic</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">Sequential</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">Static</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">Discrete</td></tr><tr style="box-sizing: inherit;"><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">Poker</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">Partially</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">Multi</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">Stochastic</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">Sequential</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">Static</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">Discrete</td></tr><tr style="box-sizing: inherit;"><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">Image analysis</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">Fully</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">Single</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">Deterministic</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">Episodic</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">Static</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">Continuous</td></tr><tr style="box-sizing: inherit;"><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">Refinery controller</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">Partially</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">Single</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">Stochastic</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">Sequential</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">Dynamic</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">Continuous</td></tr></tbody></table>
<!--kg-card-end: html-->
<p>Now that we have discussed different types of agent environments, we delve into the last section of this article.</p><h2 id="the-different-types-of-agent-programs"><strong>The different types of agent programs</strong></h2><p>The agent program (alongside the agent architecture) is what we are really interested in. It&#x2019;s the <strong>solution</strong> to our problem. In fact, designing agent programs is at the center of modern AI. They typically follow this simple structure: a percept comes in, the program is executed, and an action comes out (&#x2018;do nothing&#x2019; is a valid action). Note that contrary to the agent function, the <strong>agent program will only take one percept at a time</strong> to process.</p><p>There are four basic types of agent programs.</p><h3 id="1simple-reflex-agents"><strong>1- Simple reflex agents</strong></h3><p>The simplest kind of agent is the simple reflex agent. These agents select actions on the basis of the current percept, ignoring past or possible future percepts.</p><figure class="kg-card kg-gallery-card kg-width-wide"><div class="kg-gallery-container"><div class="kg-gallery-row"><div class="kg-gallery-image"><img src="https://api.loudpumpkins.com/content/images/2024/07/1_intelligent_agent-1.png" width="747" height="554" loading="lazy" alt="What is an intelligent agent" srcset="https://api.loudpumpkins.com/content/images/size/w600/2024/07/1_intelligent_agent-1.png 600w, https://api.loudpumpkins.com/content/images/2024/07/1_intelligent_agent-1.png 747w" sizes="(min-width: 720px) 720px"></div><div class="kg-gallery-image"><img src="https://api.loudpumpkins.com/content/images/2024/07/Screenshot-2024-04-21-221305.png" width="565" height="350" loading="lazy" alt="What is an intelligent agent"></div></div></div></figure><p>Note that the &#x2018;interpreter&#x2019; in our example is purely conceptual. It may be implemented using logical gates (e.g. a vending machine), neural networks with activation functions used to match percepts to an interpretation, or just &#x2018;if &#x2013; then &#x2013;&#x2019; clauses.</p><h3 id="2model-based-reflex-agents"><strong>2- Model-based reflex agents</strong></h3><p>In some situations, a simple if &#x2013; then &#x2013; approach is not enough. An agent may find it beneficial to act differently given the same percept at two different points in time. For example, if someone offers you water, you may or may not accept it depending on your current thirst levels. The model-based reflex agent adds an <strong>internal state</strong> to the agent. &#x2018;If water is offered and state is &#x201C;thirsty&#x201D;, accept water&#x2019;. Additionally, the model-based reflex agent <strong>keeps track of previous percepts</strong>. This is particularly helpful in a partially observable environment. The agent now has access to historical data to reference upon for some of the currently unobservable aspects of the environment.</p><p>Updating its internal state and the state of objects/agents around it requires two kinds of knowledge to be programmed into the model in some form. First, the agent needs to know &#x2018;how the world works&#x2019;, &#x2018;how the world changes over time&#x2019;. For example, a self-driving vehicle needs to know what happens when it opens the engine throttle or turns the steering column. This is called a <strong>transition model</strong>. Second, the agent needs some information about how the state of the world is reflected in the agent&#x2019;s percepts. Or in other words, &#x2018;how to make sense of the world it sees&#x2019;. This is called a <strong>sensor model</strong>.</p><p>Together, they allow the agent to keep track of its state, the environment, and other objects/agents.</p><figure class="kg-card kg-gallery-card kg-width-wide"><div class="kg-gallery-container"><div class="kg-gallery-row"><div class="kg-gallery-image"><img src="https://api.loudpumpkins.com/content/images/2024/07/3_model_based_reflex_agent.png" width="733" height="525" loading="lazy" alt="What is an intelligent agent" srcset="https://api.loudpumpkins.com/content/images/size/w600/2024/07/3_model_based_reflex_agent.png 600w, https://api.loudpumpkins.com/content/images/2024/07/3_model_based_reflex_agent.png 733w" sizes="(min-width: 720px) 720px"></div><div class="kg-gallery-image"><img src="https://api.loudpumpkins.com/content/images/2024/07/Screenshot-2024-04-21-221447.png" width="632" height="235" loading="lazy" alt="What is an intelligent agent" srcset="https://api.loudpumpkins.com/content/images/size/w600/2024/07/Screenshot-2024-04-21-221447.png 600w, https://api.loudpumpkins.com/content/images/2024/07/Screenshot-2024-04-21-221447.png 632w"></div></div></div></figure><h3 id="3goal-based-agents"><strong>3- Goal-based agents</strong></h3><p>Knowing the current state of the environment is not always enough. Take chess for example. Knowing the rules of the game is not enough to succeed. You also need &#x2018;<strong>goal information&#x2019;</strong> in this scenario which describes a desirable situation. Such as &#x201C;don&#x2019;t lose the king&#x201D; or &#x201C;get the opponent&#x2019;s king&#x201D;. This type of agent can easily be combined with a model-based agent with the difference being in the decision-making process. Where the model-based agent will map a state and percept to an action, the goal-based agent will decide on an action based on which action (or sequence of actions) will help achieve a goal. And so, a goal-based chess agent can sacrifice its own piece to protect its king not because this behaviour was mapped, but because it is the only action it predicts will achieve its goal of not losing the king.</p><figure class="kg-card kg-image-card"><img src="https://api.loudpumpkins.com/content/images/2024/07/4_goal_based_agent.png" class="kg-image" alt="What is an intelligent agent" loading="lazy" width="733" height="581" srcset="https://api.loudpumpkins.com/content/images/size/w600/2024/07/4_goal_based_agent.png 600w, https://api.loudpumpkins.com/content/images/2024/07/4_goal_based_agent.png 733w" sizes="(min-width: 720px) 720px"></figure><h3 id="4utility-based-agents"><strong>4- Utility-based agents</strong></h3><p>Sometimes, achieving a goal is not enough. Sometimes we want to achieve said goal faster, cheaper, safer, reliably, etc. Think of google maps; we don&#x2019;t want just some route to our destination, we want the best route to our destination. A goal-based agent will happily take you on a journey around the country as long as it gets you to your destination eventually.</p><p>What this agent type adds is an implementation of the performance measure called the <strong>utility function</strong>. It will weigh and balance all our desired qualities and rank a scenario. This is particularly helpful when we have conflicting goals (such as a &#x2018;fast&#x2019; yet &#x2018;safe&#x2019; self-driving vehicle), the utility function specifies the appropriate tradeoff. Second, when there are several goals that the agent can aim to achieve, the utility function can pick the path with the highest probability of success or importance. Or both.</p><figure class="kg-card kg-image-card"><img src="https://api.loudpumpkins.com/content/images/2024/07/5_utility_based_agent.png" class="kg-image" alt="What is an intelligent agent" loading="lazy" width="733" height="645" srcset="https://api.loudpumpkins.com/content/images/size/w600/2024/07/5_utility_based_agent.png 600w, https://api.loudpumpkins.com/content/images/2024/07/5_utility_based_agent.png 733w" sizes="(min-width: 720px) 720px"></figure><p>And this concludes our lesson about intelligent agents. Next, we will learn about solving problems by searching.</p>]]></content:encoded></item><item><title><![CDATA[Solving Problems by Searching]]></title><description><![CDATA[<p>In this article, we will see how a <strong>problem-solving</strong> agent can use search trees to look ahead and find a sequence of actions that will solve our problem.</p><p>This method works particularly well in environments that are deterministic, fully observable, <strong>static</strong>, <strong>discrete</strong>, and <strong>known</strong>. Search trees can be used to</p>]]></description><link>https://api.loudpumpkins.com/solving-problems-by-searching/</link><guid isPermaLink="false">6683658b17478208e8b21b94</guid><dc:creator><![CDATA[Loud Pumpkins]]></dc:creator><pubDate>Tue, 02 Jul 2024 02:40:21 GMT</pubDate><media:content url="https://api.loudpumpkins.com/content/images/2024/07/state_space_graph.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://api.loudpumpkins.com/content/images/2024/07/state_space_graph.jpg" alt="Solving Problems by Searching"><p>In this article, we will see how a <strong>problem-solving</strong> agent can use search trees to look ahead and find a sequence of actions that will solve our problem.</p><p>This method works particularly well in environments that are deterministic, fully observable, <strong>static</strong>, <strong>discrete</strong>, and <strong>known</strong>. Search trees can be used to solve multi-agent environments, which we will discuss in another article. In this article, we will only cover single-agent environments. Also, search problems must have <strong>atomic states</strong>; states with no internal structure or components.</p><p>We will start by introducing systematic algorithms, also known as <strong>uninformed algorithms</strong>, and in the next article, we will move on to intelligent algorithms, also known as <strong>informed algorithms</strong>.</p><p>Problem-solving agents are currently used in many real-world situations. Path finding in video games, routing video streams in computer networks, and finding optimal driving directions are amongst the first examples that come to mind. Those are known as <strong>route-finding problems</strong>.</p><p>But they are also used for <strong>touring problems</strong> (travelling salesperson problem) where a salesman needs to travel to every city most efficiently, &#xA0;<strong>VLSI layout problems </strong>where millions of transistors need to be strategically placed on a single chip to minimize area and signal delay (propagation delay). They are even used in robot navigation and automatic assembly sequencing.</p><h2 id="search-problems"><strong>Search Problems</strong></h2><p>We will start with a simple <strong>route-finding search problem</strong>.</p><p>Imagine that you have the following map of your local surroundings and that you need to build an agent that will find a path from your current location A to your goal destination Z.</p><figure class="kg-card kg-image-card"><img src="https://api.loudpumpkins.com/content/images/2024/07/state_space_graph-1.jpg" class="kg-image" alt="Solving Problems by Searching" loading="lazy" width="1045" height="675" srcset="https://api.loudpumpkins.com/content/images/size/w600/2024/07/state_space_graph-1.jpg 600w, https://api.loudpumpkins.com/content/images/size/w1000/2024/07/state_space_graph-1.jpg 1000w, https://api.loudpumpkins.com/content/images/2024/07/state_space_graph-1.jpg 1045w" sizes="(min-width: 720px) 720px"></figure><p>Before we continue, we will <strong>formally define the problem</strong> to make our lives easier by defining:</p><ul><li>The <strong>state space</strong>: A set of all possible states that the environment can be in.</li><li>The <strong>initial state</strong>: The initial state that the environment starts in (the &#x201C;root&#x201D; of the tree).</li><li>The <strong>goal state</strong>: A set of goal states or one goal state.</li><li>The <strong>actions</strong>: All the actions available to the agent. Given a state s, Actions(s) returns a finite set of actions that can be executed.</li><li>The <strong>transition model</strong>: A transition model which describes what each action does. Result(s, a) returns the state that results from doing action <em>a</em> in state <em>s</em>.</li><li>The <strong>action cost function</strong>: A function that takes in a state, action, and the resultant state to determine the cost of an action. This can be a measure of time, resources, or work.</li></ul><pre><code class="language-bash">State space = { A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, Z }
Initial state = A
Goal state = Z
actions = {
    Actions(A) = { go_to_B, go_to_C, go_to_D }
    Actions(B) = { go_to_A, go_to_E }
    Actions(C) = { go_to_A, go_to_E, go_to_G, go_to_F }
    Actions(D) = { go_to_A, go_to_H }
    Actions(E) = { go_to_B, go_to_C }
    [ &#x2026; ]
}
transition model = {
    Result(A, go_to_B) = B
    Result(A, go_to_C) = C
    Result(A, go_to_D) = D
    Result(B, go_to_A) = A
    Result(B, go_to_E) = E
    [ &#x2026; ]
}
action cost function = {
    ActionCost(A, go_to_B, B) = 75
    ActionCost(A, go_to_C, C) = 140
    ActionCost(A, go_to_D, D) = 118
    ActionCost(B, go_to_A, A) = 75
    ActionCost(B, go_to_E, E) = 71
    [ &#x2026; ]
}</code></pre><p>And we will define an abstract Problem class as such:</p><pre><code class="language-python">class Problem(object):
   &quot;&quot;&quot;
   An abstract search problem with core functionality that needs to be
   implemented.
   &quot;&quot;&quot;

   def __init__(self, initial_state):
      self.initial_state = initial_state

   def actions(self, state):
      &quot;&quot;&quot;
      Generate a list of actions that can be taken given any state.

      :param state: Any: Same datatype as the `state` in `Node`
      :return: list
      &quot;&quot;&quot;
      raise NotImplemented(&quot;Abstract class method not implemented&quot;)

   def is_goal(self, state):
      &quot;&quot;&quot;
      Test the given state to see if a goal has been reached. The `state` is
      of the same data type as the state in `Node` from `search_problem.util`

      :param state: Any: Same datatype as the `state` in `Node`
      :return: bool
      &quot;&quot;&quot;
      raise NotImplemented(&quot;Abstract class method not implemented&quot;)

   def transition(self, state, action):
      &quot;&quot;&quot;
      Generate the resultant state given any state and a valid action.

      :param state: Any: Same datatype as the `state` in `Node`
      :param action: Any
      :return: result_state: Any: Same datatype as the `state` in `Node`
      &quot;&quot;&quot;
      raise NotImplemented(&quot;Abstract class method not implemented&quot;)

   def expand(self, node: Node):
      &quot;&quot;&quot;
      Expand the given node with a state, return a list of successor nodes
      using the problem&apos;s actions and transitions.

      :param node: Node
      :return: list[Node]
      &quot;&quot;&quot;
      return [Node(self.transition(node.state, action), node, action) for action in
              self.actions(node.state)]</code></pre><p>Recall that our objective is to find a path from A to Z where each letter is a fictional city or town. A solution is found once a path from the initial state to one of the goal states is found. And so, a <strong>path</strong> is just a <strong>sequence of actions</strong>.</p><p>By the way, you may wonder if the state space should be so abstract as to include only towns and cities. Surely we would want to include different intersections within each city or town. And the answer is: it depends.</p><p>The choice of a good abstraction involves removing as much detail as possible while retaining validity and ensuring that the abstract actions can be carried out. Too many real-world details will completely overwhelm the intelligent agent.</p><h2 id="search-algorithms"><strong>Search Algorithms</strong></h2><p>A search algorithm takes a search problem as input and returns a solution or an indication of failure. In a single-agent environment, the search algorithm will form a <strong>search tree</strong> that will superimpose the <strong>state space graph</strong>, forming various paths from the initial state, trying to find a path that reaches a goal state. Each <strong>node</strong> in the search tree corresponds to a state in the state space and the <strong>edges </strong>in the search tree correspond to actions. The <strong>root </strong>of the tree corresponds to the initial state of the problem.</p><p>It is important to understand the distinction between a state space graph and a search tree. The state space will only have as many nodes as there are states in the state space and only as many edges as there are actions, but the search tree will always have the initial state as the root node, potentially an infinite number of nodes (if we do not keep track of repeated states or cycles), but most importantly, each node in the search tree has a unique path back to the root.</p><p>The first step a search algorithm can do is <strong>expand</strong> the root node, by considering the available actions for that state through the given transition model. This will <strong>generate</strong> new nodes called <strong>successor nodes</strong> or <strong>child nodes</strong>.</p><p>Each generated node will record its parent node and will be placed in the <strong>frontier </strong>of the search tree (unexplored nodes).</p><figure class="kg-card kg-image-card"><img src="https://api.loudpumpkins.com/content/images/2024/07/simple_root_node_expanded_with_a_view_of_the_frontier.jpg" class="kg-image" alt="Solving Problems by Searching" loading="lazy" width="530" height="211"></figure><p>Now we must choose which of these three child nodes to consider next. This is the essence of search &#x2013; following up on one option now and putting the others aside for later.</p><h2 id="uninformed-search-algorithms"><strong>Uninformed search algorithms</strong></h2><p>An uninformed search algorithm is given no clue about how close a state is to the goal(s). It is a systematic way of exploring state space graphs and we have two strategies: breadth-first search and depth-first search.</p><h3 id="breadth-first-search-first-in-first-out-queue"><strong>Breadth-first Search (first-in-first-out / queue)</strong></h3><p>In breadth-first search, the root node is expanded first, then all its successors, and then all their successors, and so on until a goal state is found or the entire graph has been traversed.</p><figure class="kg-card kg-image-card"><img src="https://api.loudpumpkins.com/content/images/2024/07/search_tree_pattern_of_a_breadth_first_search.jpg" class="kg-image" alt="Solving Problems by Searching" loading="lazy" width="1096" height="601" srcset="https://api.loudpumpkins.com/content/images/size/w600/2024/07/search_tree_pattern_of_a_breadth_first_search.jpg 600w, https://api.loudpumpkins.com/content/images/size/w1000/2024/07/search_tree_pattern_of_a_breadth_first_search.jpg 1000w, https://api.loudpumpkins.com/content/images/2024/07/search_tree_pattern_of_a_breadth_first_search.jpg 1096w" sizes="(min-width: 720px) 720px"></figure><pre><code class="language-python">def breadth_first_search(problem: Problem) -&gt; Optional[Node]:
   frontier = queue.SimpleQueue()
   frontier.put(Node(problem.initial_state))

   while not frontier.empty():
      node: Node = frontier.get()
      if problem.is_goal(node.state):
         return node
      for child in problem.expand(node):
         frontier.put(child)
   return None</code></pre><p>This strategy is implemented quite regularly because it is a <strong>complete</strong> search algorithm. Meaning that if a solution exists, this algorithm will find it. Also, it will return the solution with the fewest number of actions. And if all actions have the same cost, the solution is also a <strong>cost optimal</strong> solution.</p><p>However, this algorithm has an exponential <strong>time complexity</strong> and <strong>space complexity.</strong> Meaning that it is expensive on time and memory.</p><p>Imagine a problem where, on average, each node has ten possible actions and the solution is fifteen levels deep in the search tree.</p><p>Then we will need to generate 1 node for the root node, 10 more to expand the root node, 10 more for each of those 10 nodes, and so on 15 times. That gives us:</p><figure class="kg-card kg-image-card"><img src="https://api.loudpumpkins.com/content/images/2024/07/image.png" class="kg-image" alt="Solving Problems by Searching" loading="lazy" width="664" height="66" srcset="https://api.loudpumpkins.com/content/images/size/w600/2024/07/image.png 600w, https://api.loudpumpkins.com/content/images/2024/07/image.png 664w"></figure><p>And if each node is allocated just a kilobyte and takes just a millisecond to process, that&#x2019;s over <strong>1000 petabytes</strong> of RAM and over <strong>35 000 years.</strong></p><p>We can calculate the time and space complexity more abstractly by declaring a variable &#x2018;d&#x2019; as the depth of the solution and &#x2018;b&#x2019; as the branching factor (average actions per node):</p><figure class="kg-card kg-image-card"><img src="https://api.loudpumpkins.com/content/images/2024/07/image-1.png" class="kg-image" alt="Solving Problems by Searching" loading="lazy" width="644" height="89" srcset="https://api.loudpumpkins.com/content/images/size/w600/2024/07/image-1.png 600w, https://api.loudpumpkins.com/content/images/2024/07/image-1.png 644w"></figure><p>For this reason, this algorithm is typically not used to solve non-trivial problems.</p><h3 id="depth-first-search-last-in-first-out-stack"><strong>Depth-first Search (last-in-first-out / stack)</strong></h3><p>Depth-first search algorithms will always expand the deepest node in the frontier first. Implemented with a stack (last-in-first-out). The search proceeds to expand nodes deeper and deeper until the deepest level of the search tree, where the nodes have no successors and then &#x201C;backs up&#x201D; to the next deepest node.</p><figure class="kg-card kg-image-card"><img src="https://api.loudpumpkins.com/content/images/2024/07/search_tree_pattern_of_a_depth_first_search.jpg" class="kg-image" alt="Solving Problems by Searching" loading="lazy" width="1093" height="707" srcset="https://api.loudpumpkins.com/content/images/size/w600/2024/07/search_tree_pattern_of_a_depth_first_search.jpg 600w, https://api.loudpumpkins.com/content/images/size/w1000/2024/07/search_tree_pattern_of_a_depth_first_search.jpg 1000w, https://api.loudpumpkins.com/content/images/2024/07/search_tree_pattern_of_a_depth_first_search.jpg 1093w" sizes="(min-width: 720px) 720px"></figure><pre><code class="language-python">def depth_first_search(problem: Problem) -&gt; Optional[Node]:
   frontier = queue.LifoQueue()
   frontier.put(Node(problem.initial_state))

   while not frontier.empty():
      node: Node = frontier.get()
      if problem.is_goal(node.state):
         return node
      for child in problem.expand(node):
         frontier.put(child)
   return None</code></pre><p>This method is also complete like the breadth-first search, but it has the added benefit of <strong>reduced space complexity </strong>which comes at a cost of forgoing the optimal solutions.</p><p>The space complexity is drastically reduced from being exponentially bound to linearly bound. We now only explore one branch at a time, so our frontier includes only the successors of one node per level in the tree. Meaning that the space complexity of depth-first search is O(bm) where &#x2018;b&#x2019; is the branching factor and &#x2018;m&#x2019; is the maximum depth of the tree.</p><h3 id="cycles-and-redundant-paths"><strong>Cycles and Redundant Paths</strong></h3><p>We saw how both of the uninformed searches are <strong>complete</strong>, but that is only true if the state graph has <strong>no cycles</strong>. Looking at our original route-finding problem&#x2019;s state graph, we can see how an algorithm can easily get stuck in an infinite loop if it were to visit A &gt; B &gt; E &gt; C &gt; A &gt; B &gt; E &gt; C &gt; A or will add <strong>redundant paths</strong>. This is especially problematic in undirected graphs like ours as each node will have an edge back to its parent; creating a duplicate sub search tree of the parent node.</p><p>We have a couple of approaches we can take to avoid redundant paths and cycles.</p><p>The first one is to remember all states we have already visited and store them in a set of &#x2018;explored nodes&#x2019;.</p><pre><code class="language-python">def breadth_first_search_with_reached(problem: Problem) -&gt; Optional[Node]:
   explored = set()
   explored.add(problem.initial_state)
   frontier = queue.SimpleQueue()
   frontier.put(Node(problem.initial_state))

   while not frontier.empty():
      node: Node = frontier.get()
      if problem.is_goal(node.state):
         return node
      for child in problem.expand(node):
         if child.state not in explored:
            explored.add(child.state)
            frontier.put(child)
   return None</code></pre><p>However, storing all states in memory can lead to failure just as we saw for breadth-first search algorithms. So, as a compromise, instead of holding a set of explored nodes, we can traverse the chain of parent nodes to see if a given successor node has already been explored in this particular path. This will avoid cycles, and thus infinite loops, but it can still have the same node explored multiple times from different paths.</p><pre><code class="language-python">def depth_first_search_with_reached(problem: Problem) -&gt; Optional[Node]:
   frontier = queue.LifoQueue()
   frontier.put(Node(problem.initial_state))

   while not frontier.empty():
      node: Node = frontier.get()
      if problem.is_goal(node.state):
         return node
      for child in problem.expand(node):
         if not child.explored(child.state):
            frontier.put(child)
   return None</code></pre><h3 id="depth-limited-and-iterative-deepening-search"><strong>Depth-limited and Iterative Deepening Search</strong></h3><p>To keep depth-first search algorithms from travelling too far down a certain path. We can define a limit &#x2018;l&#x2019; and refuse to expand nodes on that level. This allows us to not worry about infinite cycles, but of course, sometimes we may fail to find a solution if our choice of &#x2018;l&#x2019; is too low.</p><pre><code class="language-python">def depth_limited_search(problem: Problem, depth: int) -&gt; Optional[Node]:
   frontier = queue.LifoQueue()
   frontier.put(Node(problem.initial_state))

   while not frontier.empty():
      node: Node = frontier.get()
      if problem.is_goal(node.state):
         return node
      path = node.path_to_root()
      for child in problem.expand(node):
         if any(filter(lambda n: n.state == child.state, path)) \
            or len(path) &gt; depth:
            # this node has either been visited or max depth reached - stop
            continue
         frontier.put(child)
   return None</code></pre><p>This algorithm has its place in solving search problems, but it is particularly helpful when used in conjunction with an <strong>iterative deepening search</strong> algorithm.</p><p>Iterative deepening solves the problem of picking the right value of &#x2018;l&#x2019; by trying all values sequentially from 0 to infinity.</p><pre><code class="language-python">def iterative_deepening_search(problem: Problem) -&gt; Optional[Node]:
   for e in range(sys.maxsize):
      result = depth_limited_search(problem, e)
      if isinstance(result, Node):
         return result</code></pre><p>Just like depth-first search and breadth-first search, iterative deepening search is <strong>complete on finite state spaces</strong> (assuming the graph is acyclic or that cycles are handled somehow), and just like breadth-first search, it will return a solution with the fewest number of actions, thus, <strong>cost optimal</strong> if all actions have the same cost. But unlike breadth-first search, it has the space complexity of depth-first search.</p><p>This essentially combines the best of both uninformed algorithms into one.</p><p>Admittedly, iterative deepening search seems wasteful at first because each call to the depth limited search will rebuild the entire search tree. But recall that the growth rate of a search tree is exponential. Meaning that each level has &#x2018;b&#x2019; times as many nodes as the level before it. So most of the computation is in the last level anyway.</p><p>For example, take a search problem with an average branching factor of just six with a depth limit of six.</p>
<!--kg-card-begin: html-->
<table style="box-sizing: inherit; border-spacing: 0px; border-collapse: collapse; background-color: transparent; margin: 0px 0px 1.5em; width: 1408px;"><tbody style="box-sizing: inherit;"><tr style="box-sizing: inherit;"><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">Algorithm</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">Depth 0</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">Depth 1</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">Depth 2</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">Depth 3</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">Depth 4</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">Depth 5</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">Depth 6</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">Total</td></tr><tr style="box-sizing: inherit;"><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;"><strong style="box-sizing: inherit; font-weight: bold;">BFS</strong></td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">1</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">6</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">36</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">216</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">1296</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">7776</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">54432</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;"><strong style="box-sizing: inherit; font-weight: bold;">63763</strong></td></tr><tr style="box-sizing: inherit;"><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">DLS(0)</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">1</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">&#xA0;</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">&#xA0;</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">&#xA0;</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">&#xA0;</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">&#xA0;</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">&#xA0;</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">1</td></tr><tr style="box-sizing: inherit;"><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">DLS(1)</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">1</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">6</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">&#xA0;</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">&#xA0;</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">&#xA0;</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">&#xA0;</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">&#xA0;</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">7</td></tr><tr style="box-sizing: inherit;"><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">DLS(2)</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">1</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">6</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">36</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">&#xA0;</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">&#xA0;</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">&#xA0;</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">&#xA0;</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">43</td></tr><tr style="box-sizing: inherit;"><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">DLS(3)</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">1</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">6</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">36</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">216</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">&#xA0;</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">&#xA0;</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">&#xA0;</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">259</td></tr><tr style="box-sizing: inherit;"><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">DLS(4)</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">1</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">6</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">36</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">216</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">1296</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">&#xA0;</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">&#xA0;</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">1555</td></tr><tr style="box-sizing: inherit;"><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">DLS(5)</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">1</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">6</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">36</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">216</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">1296</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">7776</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">&#xA0;</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">9331</td></tr><tr style="box-sizing: inherit;"><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">DLS(6)</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">1</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">6</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">36</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">216</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">1296</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">7776</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">54432</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">63763</td></tr><tr style="box-sizing: inherit;"><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;"><strong style="box-sizing: inherit; font-weight: bold;">IDS(6)</strong></td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">7</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">36</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">180</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">864</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">3888</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">15552</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;">54432</td><td style="box-sizing: inherit; padding: 0.5em; border: 1px solid;"><strong style="box-sizing: inherit; font-weight: bold;">74959</strong></td></tr></tbody></table>
<!--kg-card-end: html-->
<p>Looking at the table, we can see that in the worst case, breadth-first search generates 63763 nodes and iterative deepening generates 74959 nodes. Not bad, right?</p><p>Note that the difference is even smaller with a larger branching factor. In real-world problems, such as chess, branching factors can easily reach 20+.</p><h2 id="concrete-implementation-of-a-search-problem"><strong>Concrete Implementation of a Search Problem</strong></h2><p>As an example, we will implement a &#x201C;cabbage, goat, and wolf&#x201D; problem which involves a person, travelling with a wolf, a goat and a cabbage that finds himself at a river. There is a single small boat to afford</p><p>passage across the river. The boat can hold the person and only one of the wolf, goat or cabbage. The person must ferry his animals and vegetables across the river making as many trips as necessary to do so. However, if the goat is left unattended with the cabbage, the goat will eat the cabbage. Similarly, if the wolf is left unattended with the goat, the wolf will eat the goat. How can the person ferry all items across the river without anything being eaten?</p><p>The search tree:</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://api.loudpumpkins.com/content/images/2024/07/cgw_graph.jpg" class="kg-image" alt="Solving Problems by Searching" loading="lazy" width="1801" height="725" srcset="https://api.loudpumpkins.com/content/images/size/w600/2024/07/cgw_graph.jpg 600w, https://api.loudpumpkins.com/content/images/size/w1000/2024/07/cgw_graph.jpg 1000w, https://api.loudpumpkins.com/content/images/size/w1600/2024/07/cgw_graph.jpg 1600w, https://api.loudpumpkins.com/content/images/2024/07/cgw_graph.jpg 1801w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">(nodes in red represent visited nodes and thus not explored)</span></figcaption></figure><p>Implementation:</p><pre><code class="language-python">class CGW(Problem):
   &quot;&quot;&quot;
   This class implements a Cabbage-Goat-Wolf problem as a sub-class of the
   search `Problem` class.

   The values of the state member variables of instances of this class are
   represented in a tuple as follows:

   (human, left, right)

   Where:
   human is an integer;
     1 = human (and boat) on left side,
     2 = human (and boat) on right side,

   left is a string containing zero to three of the characters &quot;CGW&quot; indicating
   which entities are on the left side of the river;

   right is a string containing zero to three of the characters &quot;CGW&quot;
   indicating which entities are on the right side of the river.

   The initial state of the CGW problem is (1,&quot;CGW&quot;,&quot;&quot;).
   &quot;&quot;&quot;

   def __init__(self, initial_state):
      &quot;&quot;&quot;
      Initialize a CGW state member variables based on the passed arguments.
      &quot;&quot;&quot;
      super().__init__(self.validate(initial_state))

   def actions(self, state):
      &quot;&quot;&quot;
      Return a list of valid actions to take. Which are simply the animal that
      needs to be moved to the other side with the human.

      Recall that a state is: (human, left_side, right_side) where human is
      1 if on the left side or 2 if on the right.

      So we can access the side we are working on using:

         state[state[0]] -&gt; state[human location] -&gt; the side human is on

      &quot;&quot;&quot;
      actions = []
      for animal in state[state[0]]:
         actions.append(animal)  # animal crossing with human
      actions.append(None)  # the human crossing alone

      return actions

   def is_goal(self, state):
      &quot;&quot;&quot;
       Return True if the problem is solved.  I.e.: human and all items are on
       the right side.
       &quot;&quot;&quot;
      return state[0] == 2 and state[2] == &quot;CGW&quot;

   def transition(self, state, action):
      &quot;&quot;&quot;
      The transition is to always move the human from one side to the other.
      The action contains a letter &quot;C&quot;, &quot;G&quot;, &quot;W&quot; to indicate which animal
      should the human take with him, or `None` if the human is to move alone.
      &quot;&quot;&quot;
      animal = action
      human = state[0]
      left_side = state[1]
      right_side = state[2]

      if human == 1:
         # human is going from left to right, move animal from left to right
         human = 2
         left_side = self._remove_animal(left_side, animal)
         right_side = self._add_animal(right_side, animal)

      elif human == 2:
         human = 1
         # human is going from right to left, move animal from right to left
         right_side = self._remove_animal(right_side, animal)
         left_side = self._add_animal(left_side, animal)

      else:  # Where is the human ?
         raise IndexError(&quot;State[0] &apos;human&apos; must be &apos;1&apos; or &apos;2&apos;.&quot;)

      state = (human, left_side, right_side)
      return self.validate(state)

   def validate(self, state):
      &quot;&quot;&quot;
      Update a state variable based on the goat eating the cabbage or the wolf
      eating the goat.
      &quot;&quot;&quot;
      if state[0] == 1:  # human is on left side
         right = state[2]  # retrieve the contents of the right side
         if &quot;G&quot; in right:  # goat is on right side
            right = self._remove_animal(right, &quot;C&quot;)  # goat eats cabbage
         if &quot;W&quot; in right:  # wolf is on right side
            right = self._remove_animal(right, &quot;G&quot;)  # wolf eats cabbage
         # reconstruct state which with new right-side contents
         state = (state[0], state[1], right)

      elif state[0] == 2:  # human is on right side
         left = state[1]  # retrieve the contents of the left side
         if &quot;G&quot; in left:  # goat is on the left side
            left = self._remove_animal(left, &quot;C&quot;)  # goat eats cabbage
         if &quot;W&quot; in left:  # wolf is on the left side
            left = self._remove_animal(left, &quot;G&quot;)  # wolf eats goat
         # reconstruct state with new left-side contents
         state = (state[0], left, state[2])

      else:  # Where is the human ?
         raise IndexError(&quot;State[0] &apos;human&apos; must be &apos;1&apos; or &apos;2&apos;.&quot;)
      return state

   def _remove_animal(self, input_string, animal):
      &quot;&quot;&quot;
      This is a utility function that returns a string that matches the
      input_string except with the given animal (letter) removed.

      exp: input: &quot;CGW&quot;, animal: &quot;G&quot;, output: &quot;CW&quot;
      &quot;&quot;&quot;
      if animal is None:
         return input_string
      return input_string.replace(animal, &quot;&quot;)

   def _add_animal(self, input_string, animal):
      &quot;&quot;&quot;
      This is a utility function that returns a string that matches the
      input_string with the given animal added in alphabetical position.
      &quot;&quot;&quot;
      if animal is None:
         return input_string
      return &quot;&quot;.join(sorted(input_string + animal))</code></pre><pre><code class="language-python">initial_state = (1, &quot;CGW&quot;, &quot;&quot;)
node = iterative_deepening_search(CGW(initial_state))
if node is not None:
   solution = node.solution()
   for index, action in enumerate(solution):
      if action is None:
         solution[index] = &apos;-&apos;
   print(&quot;&quot;.join(solution))
else:
   print(&quot;No solution found&quot;)</code></pre><p>Run:</p><pre><code class="language-bash">&gt; G-WGC-G</code></pre>]]></content:encoded></item></channel></rss>