\chapter{Search}

\section{Search Problems}

Searching is one of the most \itblue{fundemental techniques} in AI, underlying sub-module in many AI systems. Ai can \itblue{solve} many problems that homansare not good at, and achieve \itblue{super-human performance} in many domains (e.g. chess, go, etc.).

\begin{itemize}
    \item \textbf{Benefits}

    \begin{itemize}
        \item Useful as ageneral algorithmic technique for solving problems, both in AI and in other areas.

        \item Outperform humans in some areas (e.g. games).

        \item Practical:

        \begin{itemize}
            \item Many problems don't have specific algorithms for solving them.
            \item Useful in approximation (e.g., local search in optimization problems).
        \end{itemize}

        \item Some critical aspects of intelligent behaviour, e.g., planning, can be cast as search.
    \end{itemize}

    \item \textbf{Limitations}

    \begin{itemize}
        \item Only shows how to solve the problem once we have it correctly formulated.
    \end{itemize}
\end{itemize}

% TODO: Planning and Scheduling Problems:

\subsection{Formalizing a Problem as a Search Problem}

\begin{itemize}
    \item Necessary components

    \begin{enumerate}
        \item \bred{State Space}: A \term{state} is a representation of a \itblue{configuration} of the problem domain. The state space is the \itblue{set of all states} include in our model of the problem.
        \item \bred{Initial State}: The starting configuration.
        \item \bred{Goal State}: The configuration one wants to achieve.
        \item \bred{Actions} (or State Space Transitions): Allowed changed to move from one state to another.
    \end{enumerate}

    \item Optional Ingredients

    \begin{itemize}
        \item \itblue{Costs}: Representing the cost of moving from state to state.
        \item \itblue{Heuristics}: Help guide the search process.
    \end{itemize}
\end{itemize}

Once a search problem is formalized, there are a number of algorithms one can use to solve it. A \term{solution} is a \itblue{sequence of actions} or moves that can transform the \bred{initial state} into a \bred{goal state}.

\begin{example}[Romania Travel]
    We want to travel in Romania from Arad to Bucharest as fast as possible.

    \begin{center}
        \includegraphics[width=0.75\linewidth]{figures/Romania Travel.png}
    \end{center}

    Each state would be a city.

    \begin{itemize}
        \item \bred{State Space}: The set of all cities on the map.
        \item \bred{Initial State}: Arad.
        \item \bred{Goal State}: Bucharest.
        \item \bred{Actions}: Driving between neighbouring cities.
    \end{itemize}
\end{example}

\begin{example}[Water Jugs]
    We have a 3-liter jug and a 4-liter jug. We can fill either jug to the top from a tap, or we can empty either jug onto the ground. We can also pour the contents of one jug into the other until the receiving jug is full or the pouring jug is empty.

    Suppose initially the 4-liter jug is full, we want to have exactly 2 liters in the 3-liter jug.

    {~~~}

    We can use a pair of numbers to represent the state of the system: the amount of water in the 3-liter jug and the amount of water in the 4-liter jug.

    \begin{itemize}
        \item \bred{State Space}: The set of all pairs of numbers $(a, b)$ where $a$ is the amount of water in the 3-liter jug and $b$ is the amount of water in the 4-liter jug.
        \item \bred{Initial State}: $(0, 4)$.
        \item \bred{Goal State}: $(2, 0)$, $(2, 1)$, $(2, 2)$, $(2, 3)$, $(2, 4)$.
        \item \bred{Actions}:
        \begin{itemize}
            \item Fill the 3-liter jug from the tap.
            \item Fill the 4-liter jug from the tap.
            \item Empty the 3-liter jug onto the ground.
            \item Empty the 4-liter jug onto the ground.
            \item Pour the contents of the 3-liter jug into the 4-liter jug.
            \item Pour the contents of the 4-liter jug into the 3-liter jug.
        \end{itemize}
    \end{itemize}

    \begin{remark}
        When formalizing a search problem, always consider these questions:

        \begin{enumerate}
            \item Can we reach all states fron any given start state?

            \item Will all actions result in a change of state?

            \bred{No!} Imagine you have $(3, 4)$ and you try to fill the 3-liter jug from the tap. You will still have $(3, 4)$.
        \end{enumerate}
    \end{remark}
\end{example}

% \subsubsection{More Complex Situations}

In more complex situations,

\begin{itemize}
    \item Actions may lead to \bred{multiple states}.

    For example, filpping a coin may lead to heads or tails.

    \item We may not be \bred{sure of a given state}

    For example, when prize is behind door 1, 2, or 3.

    \item Such situations require techniques for reasoning under uncertainty: assign probabilities to given outcomes.
\end{itemize}

\subsection{Graphical Representation}

\begin{center}
    \includegraphics[width=0.5\linewidth]{figures/Search Graph Rep.png}
\end{center}

Assuming a finite search space, the

\begin{itemize}
    \item \bred{vertices} represent states in the search space; and the
    \item \bred{edges} represent transitions resulting from actions (or successor functions).
\end{itemize}

\subsubsection{Search Tree}

\begin{definition}[Search Tree]\index{Search Tree}\label{def:search-tree}
    A \term{search tree} is a \itblue{directed graph} where

    \begin{itemize}
        \item Each node represents a state.
        \item Each edge represents an action.
        \item The root node represents the initial state.
        \item The leaf nodes represent goal states.
    \end{itemize}
\end{definition}

A search tree reflects the behaviour of an algorithm as it walks through a search problem. It has two important properties:

\begin{itemize}
    \item \term{Solution depth}, denoted $d$, the depth of the shallowest goal node in the tree.
    \item \term{Maximum branching factor}, denoted $b$, the maximum number of children of any node in the tree.
\end{itemize}

\begin{remark}
    Note that the \bred{same} state may appear \bred{multiple times} in a search tree.
\end{remark}

\begin{remark}
    It is important to distinguish between \bred{states} from \bred{nodes}.

    \begin{itemize}
        \item A \bred{state} represents a possible configuration of the world.
        \item A \itblue{node} is a data structure constituting part of a search tree. It includes
        \begin{itemize}
            \item a \bred{state} and
            \item the \bred{parent node},
            \item the \bred{action} that led to this node, and
            \item the \bred{cost} of the path from the initial node to this node.
        \end{itemize}
        \item  Intuitively speaking, each node corresponds with a path from the initial state to the node's state.
        \item Two \bred{different nodes} are allowed to contain the \bred{same world state}.
    \end{itemize}
\end{remark}

\subsection{Algorithms for Search}

\begin{itemize}
    \item \textbf{Input}

    \begin{itemize}
        \item \textbf{Initial node}

        \item \textbf{Successor Function} $S(x)$

        returns the set of nodes that can be reached from node $c$ via a single action.

        \item \textbf{Goal Test Function} $G(x)$

        returns true if node $c$ satisfies the goal condition.

        \item \textbf{Action Cost Function} $C(x, a, y)$

        returns the cost of moving from node $x$ to node $y$ using action $a$.

        Note that $C(x, a, y) = \infty$ if $y$ is not reachable from $a$ via $a$.
    \end{itemize}

    \item \textbf{Output}

    \begin{itemize}
        \item A \itblue{sequence of actions} that transforms the initial node satisfying the goal test.
        \item The sequence might be, \term{optimal in cost} for some algorithms, \term{optimal in length} for some algorithms, come with \bred{no optimality} guarantees from other algorithms.
    \end{itemize}

    \item \textbf{Procedure}

    \begin{itemize}
        \item Put nodes have not yet expanded in a list called the \term{\Frontier} (or \term{Open}).
        \item Initially, only the \itblue{initial node} is in the \Frontier.
        \item At each iteration, pull a node from the \Frontier, apply $S(x)$, and insert the children back into the \Frontier.
        \item Repeat until pulling a goal node.
    \end{itemize}
\end{itemize}

% \newpage
\begin{algorithm}
    \caption{Tree Search Algorithm}

    \begin{algorithmic}[1]
        \Function{Tree-Search}{Frontier, Successors, Goal?}
            % If frontier is empty, return failure
            \If{Frontier is empty}
                \State \Return failure
            \EndIf

            \State Curr $\gets$ select state from frontier

            \If{Goal?(Curr)}
                \State \Return Curr
            \EndIf

            \State \Frontier' $\gets$ (Frontier $-$ \{ Curr \}) $\cup$ Successors(Curr)

            \State \Return \Call{TreeSearch}{Frontier', Successors, Goal?}
        \EndFunction
    \end{algorithmic}
\end{algorithm}

% TODO: example

Note that the search terminates only when a goal node is expanded into the \Frontier.

\subsubsection{Critical Properties of Search}

\begin{itemize}
    \item \textbf{Completeness}

    \begin{itemize}
        \item A search algorithm is \term{complete} if it always finds a solution if one exists.
    \end{itemize}

    \item \textbf{Optimality}

    \begin{itemize}
        \item A search algorithm is \term{optimal} if it always finds a solution with the lowest cost.
    \end{itemize}

    \item \textbf{Time Complexity}

    \begin{itemize}
        \item The \term{time complexity} of a search algorithm is the number of nodes it expands or generates.
    \end{itemize}

    \item \textbf{Space Complexity}

    \begin{itemize}
        \item The \term{space complexity} of a search algorithm is the maximum number of nodes it stores in memory.
    \end{itemize}
\end{itemize}

\section{Uninformed Search Algorithms}

\subsection{Uninformed Search Strategies}

The \bred{order} of nodes in the \Frontier determines the behaviour of the search algorithm. It determines whether or not the algorithm is complete, optimal, and how much time and space it requires. Search algorithms differ in their selection rule, but they all keep the \Frontier as an ordered set. The question of which node to select next from the \Frontier is now equivalent to the question of how to order the \Frontier.

There are strategies that adpot a \itblue{fixed-rule} for selecting the next node to be expanded. These strategies are called \term{uninformed search strategies} because they do \bred{not} use any \itblue{domain specific informatioin} about the problem other than the definition of the problem itself. These strategies include Breadth-First Search, Depth-First Search, Uniform-Cost Search, Depth-Limited Search, and Iterative Deepening Search.

In those course, it is assumed that the graph we are searching is \itblue{explicitly} represented as an adjacency list (or adjacency matrix). However, this will \bred{not} work when there an exponential number of nodes and edges. In AI applications, there are typically an exponential number of nodes, making it impossible to explicitely represent them all. AI search algorithms work with \itblue{implicitly} defined state spaces, where actions are compacted as \itblue{successor state functions}, and nodes must contaion enough information to allow the successor state function to be applied.

\subsection{Breadth-First Search (BFS)}

\begin{definition}[Breadth-First Search]\index{Breadth-First Search}\label{def:bfs}
    \term{Breadth-First Search} is a search strategy that expands the shallowest unexpanded node first.
\end{definition}

BFS explores the search tree \itblue{level by level}. It place the children of the current node at the \bred{end} of the \Frontier. This means the \Frontier is a \itblue{queue}, and we always extract the \itblue{first} element from the \Frontier.

\begin{example}
    We run BFS on the following graph,

    \begin{center}
        \includegraphics[width=0.45\linewidth]{figures/Search Graph Rep.png}
    \end{center}

    which yields the search tree

    \begin{center}
        \tikzexternalenable
        \begin{tikzpicture}[ tree-node/.style={ circle, draw=black, line width=0.5pt, minimum size=0.67cm, } ]
            \node[tree-node] (S) at (0, 0) {$S$};
            \node[tree-node] (P) at (-2, -1) {$p$};
            \node[tree-node] (E) at (0, -1) {$e$};
            \node[tree-node] (D) at (2, -1) {$d$};
            \node[tree-node] (Q) at (-3, -2) {$q$};
            \node[tree-node] (H) at (-1, -2) {$h$};
            \node[tree-node] (R) at (1, -2) {$r$};
            \node[tree-node] (F) at (2, -3) {$f$};
            \node[tree-node] (G) at (3, -4) {$G$};

            % Edges
            \draw (S) -- (P);
            \draw (S) -- (E);
            \draw (S) -- (D);
            \draw (P) -- (Q);
            \draw (E) -- (H);
            \draw (E) -- (R);
            \draw (R) -- (F);
            \draw (F) -- (G);
        \end{tikzpicture}
        \tikzexternaldisable
    \end{center}
\end{example}

\begin{itemize}
    \item \textbf{Completeness}

    \begin{itemize}
        \item BFS is \term{complete} if the \bred{branching factor} is \itblue{finite}.
        \item The length of the path (from the initial node to the node removed from the \Frontier) is \itblue{non-decreasing}, as we replace eac expanded node with path langth $k$ with a node with path length of $k + 1$.
    \end{itemize}

    \item \textbf{Optimality}

    \begin{itemize}
        \item BFS gives the \bred{shortest length solution}.

        All nodes with shorter paths are expanded prior to any node with longer path. We examine all paths of length $< k$ before all paths of length $k$. Thus, if there is a solution with length $k$, we will find it before longer solutions.

        \item BFS is \bred{not optimal} in terms of \itblue{cost}.

        BFS does not take into account the cost of the path. It is possible that a longer path has a lower cost than a shorter path.
    \end{itemize}
\end{itemize}

% TODO: Romnia Travel Example, see slides page 35

\subsubsection{Properties of BFS}

Let $b$ be the maximum branching factor and $d$ be the depth of the shallowest goal node.

\begin{itemize}
    \item \textbf{Time Complexity} \[
        1 + b + b^2 + \cdots + b^d + b(b^d - 1) \in O(b^{d+1})
    \]

    The last term id $b(b^d - 1)$ because we need to remove the goal node, who does not need any successors.

    \item \textbf{Space Complexity} \[
        \bigo{b^{d+1}}
    \]

    In the worst case, only the last node of depth $d$ satisfies the goal. So all nodes at depth $d$ except the last one will be expanded by the search and each such expansion will add up to $b$ new nodes to the \Frontier.
\end{itemize}

\begin{table}[ht!]
    \renewcommand{\arraystretch}{1.25}
    \centering
    \begin{tabular}{| l | l | l | l |}
        \hline
        Depth & Paths    & Time          & Memory    \\ \hline
        1     & $1$      & 0.01 millisec & 100 bytes \\ \hline
        6     & $10^6$   & 10 sec.       & 100 MB    \\ \hline
        8     & $10^8$   & 17 min.       & 10 GB     \\ \hline
        9     & $10^{9}$ & 3 hrs.        & 100 GB    \\ \hline
    \end{tabular}
\end{table}

Space complexity is a bigger problem than time complexity. Typically, BFS rus out of memory before it runs out of time.

\subsection{Depth-First Search (DFS)}

\begin{definition}[Depth-First Search]\index{Depth-First Search}\label{def:dfs}
    \term{Depth-First Search} is a search strategy that expands the deepest unexpanded node first.
\end{definition}

DFS explores the search tree \itblue{branch by branch}. It place the children of the current node at the \bred{front} of the \Frontier. This means the \Frontier is a \bred{stack}, and we always extract the \itblue{first} element from the \Frontier.

\begin{example}
    We run DFS on the following graph,

    \begin{center}
        \includegraphics[width=0.45\linewidth]{figures/Search Graph Rep.png}
    \end{center}

    The \Frontier changes as follows:

    \begin{center}
        \begin{tabular}{| c | c | c |}
            \hline
            \textbf{Frontier} & \textbf{Node Removed} & \textbf{Successors Added} \\ \hline \hline
            $S$               & $S$                   & $d, e, p$                 \\ \hline
            $d, e, p$         & $d$                   & $b, c, e$                 \\ \hline
            $b, c, e, e, p$   & $b$                   & $a$                       \\ \hline
            $a, c, e, e, p$   & $a$                   & $\null$                   \\ \hline
            $c, e, e, p$      & $c$                   & $a$                       \\ \hline
            $a, e, e, p$      & $a$                   & $\null$                   \\ \hline
            $e, e, p$         & $e$                   & $h, r$                    \\ \hline
        \end{tabular}

        {~~~}

        $\cdots$
    \end{center}
\end{example}

\begin{itemize}
    \item \textbf{Completeness}

    \begin{itemize}
        \item DFS is \term{not complete}. \bred{Infinite} paths cause incompleteness.
        \item This is because DFS does not keep track of visited nodes, so it can get stuck in a loop. An infinite search space can also cause DFS to run forever.
        \item We can prune paths with \bred{cycles} to get completeness, if state space is finite.
    \end{itemize}

    \item \textbf{Optimality}

    \begin{itemize}
        \item DFS is \term{not optimal} in terms of \itblue{length} or \itblue{cost}.
        \item DFS returns the \itblue{first} solution it finds, which may not be the \itblue{shortest} or \itblue{cheapest} solution.
    \end{itemize}
\end{itemize}

\subsubsection{Properties of DFS}

Let $b$ be the maximum branching factor and $m$ be the maximum depth of the search tree.

\begin{itemize}
    \item \textbf{Time Complexity} \[
        \bigo{b^m}
    \] where $m$ is the length of the \itblue{longest path} in the state space.

    \begin{itemize}
        \item This is \bred{very bad} if $m$ is much larger than $d$, but if there are \itblue{mant solution paths}, DFS may be much faster than BFS.
        \item Using \term{Heuristics}, we can determine which successor to explore first.
    \end{itemize}

    \item \textbf{Space Complexity} \[
        \bigo{bm}
    \]

    DFS has a \bred{linear} space complexity, which is a significant advantage.

    This is because DFA only explores a single branch of the search tree at a time. \Frontier only contains the current node $v$ along with the $m$ descendants of $v$.  Each node can have at most $b$ unexplored siblings and there are at most $m$ nodes on the current branch, which gives $\bigo{bm}$.
\end{itemize}

\subsection{Depth-Limited Search (DLS)}

Despite DFS being \itblue{incomplete} and \itblue{non-optimal}, it has a great advantage over BFS: \itblue{linear space complexity}. This makes it possible to use DFS in situations where BFS would run out of memory.

In order to address the incompleteness of DFS, we can impose a \itblue{depth limit} $D$ on the search. This means that we will only explore paths of length $D$ or less. If we reach a node at depth $D$ that is not a goal node, we will \itblue{backtrack} and explore other paths. This is called \term{Depth-Limited Search} (DLS).

\begin{definition}[Depth-Limited Search]\index{Depth-Limited Search}\label{def:dls}
    \term{Depth-Limited Search} is a search strategy that expands the deepest unexpanded node first, but only up to a given depth limit $D$.
\end{definition}

\begin{itemize}
    \item {\color{darkGreen}\textbf{Benefit}:} DLS overcomes the infinite length paths issue,
    \item \bred{Limitation:} DLS only finds a solution if the \itblue{depth limit} $D$ is greater than or equal to the depth of the shallowest goal node $d$.
\end{itemize}

\begin{remark}
    The root of a search tree is at depth 0. A path consisting onlty of the root node has length 0.

    Recall that the length of a path is the number of actions (edges) in the path.
\end{remark}

\begin{center}
    \includegraphics[width=0.67\linewidth]{figures/ids-limit-3.png}
\end{center}

\begin{algorithm}[ht!]
    \caption{Depth-Limited Search Algorithm}
    \begin{algorithmic}[1]
        \Function{DLS}{start, frontier, successors, goal?, maxd}
            \State frontier.insert(<start>)
            \Comment{frontier must be a stack for DFS}
            \State cutoff $\gets$ false
            \While{frontier is not empty}
                \State curr $\gets$ frontier.extract()
                \Comment{Remove node from frontier}
                \If{goal?(curr.final())}
                    \State \Return curr, cutoff 
                    \Comment{curr is solution}
                \ElsIf{\Call{Length}{curr} $<$ maxd}
                \Comment{Only successors if lendth(curr) < maxd}
                    \For{succ $\in$ successors(curr.final())}
                        \State frontier.insert(<p, succ>)
                    \EndFor
                \Else
                    \State cutoff $\gets$ true
                    \Comment{Reached depth limit, some nodes not expanded}
                \EndIf
            \EndWhile

            \State \Return failure, cutoff
        \EndFunction
    \end{algorithmic}
\end{algorithm}

\subsection{Iterative Deepening Search (IDS)}

DLS is \itblue{complete} if the \itblue{depth limit} $D$ is greater than or equal to the depth of the shallowest goal node $d$. However, we do not know $d$ in advance. We can start at a depth limit $D = 0$, and iteratively \bred{increase} $D$ until we find a solution. We stop if {\color{darkGreen}\textbf{a solution is found}}, or the search \bred{failed without cutting off} any nodes because of the depth limit. This is called \term{Iterative Deepening Search} (IDS).

Note that if no nodes were cut off, the search examined all nodes in the state space and found nosolution, hence there is no solution.

\begin{definition}[Iterative Deepening Search]\index{Iterative Deepening Search}\label{def:ids}
    \term{Iterative Deepening Search} is a search strategy that repeatedly applies DLS with increasing depth limits until a solution is found.
\end{definition}

\begin{center}
    \includegraphics[width=0.67\linewidth]{figures/ids-limit-1.png}
    \includegraphics[width=0.67\linewidth]{figures/ids-limit-2.png}
    \includegraphics[width=0.67\linewidth]{figures/ids-limit-3.png}
\end{center}

\begin{algorithm}[ht!]
    \caption{Iterative Deepening Search Algorithm}

    \begin{algorithmic}
        \Function{IDS}{start, frontier, successors, goal?}
            \State depth $\gets$ 0
            \While{true}
                \State solution, cutoff $\gets$ \Call{DLS}{start, successors, goal?, depth}
                \If{solution is not failure}
                    \State \Return solution
                \ElsIf{cutoff is false}
                \Comment{No nodes at deeper levels exist, no solution}
                    \State \Return failure
                \EndIf
                \State depth $\gets$ depth + 1
            \EndWhile
        \EndFunction
    \end{algorithmic}
\end{algorithm}

\begin{itemize}
    \item \textbf{Completeness}

    \begin{itemize}
        \item IDS is \term{complete}.
    \end{itemize}

    \item \textbf{Optimality}

    \begin{itemize}
        \item IDS gives the \bred{shortest length solution}.

        \item IDS is \term{not optimal} in terms of \itblue{cost}, for the same reason as DFS.

        We can make IDS optimal by doing the following:

        \begin{itemize}
            \item Can use a \bred{cost bound} instead of depth bound.
            \item Only expand nodes of cost \itblue{less} than the cost bound.
            \item Keep track of the \term{minimum cost unexpanded node} in each iteration, \itblue{increase the cost bound} to that on the next iteration.
            \item Can be \bred{computationally expensive} since need as many iterations of the search as there are distinct node costs.
        \end{itemize}
    \end{itemize}
\end{itemize}

\subsubsection{Properties of IDS}

\begin{itemize}
    \item \textbf{Time Complexity} \[
        (d+1)b^0 + db^1 + (d-1)b^2 + \cdots + b^d \in \bigo{b^d}
    \]

    \begin{itemize}
        \item This is \bred{very bad} if $d$ is much larger than $m$, but if there are \itblue{mant solution paths}, IDS may be much faster than BFS.
        \item Using \term{Heuristics}, we can determine which successor to explore first.
    \end{itemize}

    \item \textbf{Space Complexity} \[
        \bigo{bd}
    \]

    The space complexity of IDS is still linear.
\end{itemize}

\begin{remark}
    Comparing to BFS,

    \begin{itemize}
        \item The {\color{darkGreen}time complexity} of IDS can be better since it does not expand nodes at the solution depth, while BFS (in the worst case) must expand all the \itblue{bottom later nodes} until it expands a goal node. However, with a simple optimization BFS can achieve the \itblue{same time complexity} as IDS.

        \item The {\color{darkGreen}space complexity} of IDS is much better.

        \begin{itemize}
            \item In practice, BFS can be buch better depending on the problem: effective \bred{cycle checking} can be employed with BFS
            \item IDS cycle checking will make the space complexity as bad as BFS.
        \end{itemize}
    \end{itemize}
\end{remark}

\subsection{Path Checking and Cycle Checking}

\subsubsection{Path Checking}

\begin{definition}[Path Checking]\index{Path Checking}\label{def:path-checking}
    \term{Path Checking} is a search strategy that checks whether a node is an ancestor of itself along the path from the initial node to the node.
\end{definition}

\begin{remark}[Notation]
    \begin{itemize}
        \item A path $p_k$ is represented as a typle of states $\langle s_0, s_1, \cdots, s_k \rangle$ where $s_0, s_1, \cdots, s_k$ are states in $p_k$ in the order they appear in the path.
        \item Suppose $s_k$ is expanded to obtain a child succcessor statce $c$. The obtained path $\langle s_0, s_1, \cdots, s_k, c \rangle$ can be written as $\langle p_k, c \rangle$.
    \end{itemize}
\end{remark}

\begin{itemize}
    \item In every path $\langle p_k, c \rangle$ where $p_k$ is the path $\langle s_0, s_1, \cdots, s_k \rangle$, ensure that the \bred{final state} $c$ is not equal to any ancestors of $c$ along this path. That is, \[
        c \notin \{ s_0, s_1, \cdots, s_k \}
    \]

    \item Paths are checked in \bred{isolation}

    \item {\color{darkGreen}\textbf{Benefit}:} Does not increase time and space commplexity.

    \item \bred{Limitation}: Does not prune all the redundant states.
\end{itemize}

\begin{example}
    Consider the Romania Travel problem. Below is an example of a search tree.

    \begin{center} \includegraphics[width=0.67\linewidth]{figures/path-checking.png} \end{center}

    The nodes coloured in red are pruned by path checking, as they are ancestors of the current node.
\end{example}

\subsubsection{Cycle Checking}

Cycle checking is also called \term{Multiple path checking}. It is a \itblue{more general} version of path checking. It checks whether a node has been expanded before, not just whether it is an ancestor of itself along the path from the initial node to the node.

\begin{definition}[Cycle Checking]\index{Cycle Checking}\label{def:cycle-checking}
    \term{Cycle Checking} is a search strategy that checks whether a node has been expanded before.
\end{definition}

\begin{itemize}
    \item Keep track of \bred{all} nodes previous expanded during the search using a list called the \term{closed list}.

    \item When we expand $n_k$ to obtain successor $c$,

    \begin{itemize}
        \item Ensure that $c$ is not equal to any \itblue{previously expanded} node.
        \item If it is, we \bred{do not add} $c$ to the \Frontier.
    \end{itemize}

    \item {\color{darkGreen}\textbf{Benefit}:} Very \itblue{effective} in pruning redundant states.

    \item \bred{Limitation}: Expensive in term of \bred{space}.

    The space complexity is $\bigo{b^{d}}$ with optimization, and $\bigo{b^{d+1}}$ without optimization (which is the same as BFS).
\end{itemize}

\begin{remark}
    We cannot utilize this technique with DFS. This is because DFS only puts a node in the closed list after all its descendants have been expanded. Thus, the ``root'' node will be put on the list later than its descendants, and cycle checking will fail.

    Moreover, the space complexity of DFS with cycle checking would be exponential, which will loose the advantage of DFS.
\end{remark}

\begin{example}
    Consider the Romania Travel problem. Below is an example of a search tree.

    \begin{center}
        \includegraphics[width=0.67\linewidth]{figures/cycle-checking.png}
    \end{center}

    The nodes coloured in red are the explored ancestors, and the nodes coloured yellow are the nodes already exploare in another branch.
\end{example}

\subsubsection{Optimal Cost}

If we reject a path $\langle p, c \rangle$ because we have previously seen state $c$ via a different path $p'$, it could be that $\langle p, c \rangle$ is a cheaper path to $c$ than $p$

The solution would be keeping track of each state as well as the \bred{known minimum cost} of a path to that state.

\begin{itemize}
    \item If a more \bred{expensive} path to a previously seen state is found, do not add the corresponding node to the \Frontier.
    \item If a cheaper path to a previously seen state is found, add the corresponding node to the \Frontier and
    \begin{itemize}
        \item \itblue{Remove} other more \itblue{expensive} nodes to the same state from the \Frontier, OR
        \item Lazily, \itblue{ignore} these more expensive nodes when/if they are removed for expansion.
    \end{itemize}
\end{itemize}

\subsection{Uniform-Cost Search (UCS)}

The \term{Uniform-Cost Search} (UCS) algorithm is a variant of BFS that uses \itblue{cost} as the criterion for selecting the next node to expand. UCS expands the node with the \itblue{lowest path cost} $g(n)$, where $g(n)$ is the cost of the path from the initial node to node $n$.

\begin{definition}[Uniform-Cost Search]\index{Uniform-Cost Search}\label{def:ucs}
    \term{Uniform-Cost Search} is a search strategy that expands the node with the lowest path cost $g(n)$.
\end{definition}

\begin{remark}
    UCS is a \itblue{generalization} of BFS. BFS is UCS with a \itblue{constant cost} of 1 for each action.
\end{remark}

\begin{center}
    \includegraphics[width=0.45\linewidth]{figures/ucs-1.png}
    \hfill
    \includegraphics[width=0.45\linewidth]{figures/ucs-2.png}

    {~~~}

    \includegraphics[width=0.45\linewidth]{figures/ucs-3.png}
    \hfill
    $\dots$
\end{center}

\begin{itemize}
    \item \textbf{Completeness}

    \begin{itemize}
        \item UCS is complete under the condition that there is a \bred{non-zero constant lower-bound $\epsilon$} on the cost of every action.

        That is, each transition from one state to another must have a cost of at least $\epsilon > 0$.

        \item Under  this condition the cost of the nodes chosen to be expanded will be non-decreasing and eventually we will expand all nodes with cost equal to the cost of a solution path
    \end{itemize}

    \item \textbf{Optimality}

    \begin{itemize}
        \item UCS finds the \bred{minimum cost} solution if each transition has cost $\ge \epsilon > 0$.

        \item UCS explores nodes in the search space in non-decreasing order of cost. So it must find minimum cost path to a goal before finding any higher costs paths leading to a goal.
    \end{itemize}

    \begin{proof}
        UCS is optimal if the cost of each action is $\ge \epsilon > 0$.

        \begin{remark}[Intuition]
            \begin{itemize}
                \item nodes are expanded in order of \bred{non-decreasing cost}.
                \item \bred{All nodes} with cost strictly less than $C$ are expanded before \bred{all nodes} with cost equal to $C$.
                \item The first path found to a state is the \bred{cheapest} path to that state (be it a goal state or not).
            \end{itemize}
        \end{remark}

        In all of the following lemmas the assumption is that each transition has cost $\ge \epsilon > 0$.

        \begin{lemma}
            Let $c(n)$ denote the cost of a node n on the \Frontier. If a node $n_2$ is expanded after $n_1$ by UCS, then $\color{darkRed}c(n_1) \le c(n_2)$.
        \end{lemma}

        \begin{lemma}
            Let $n$ be an arbitrart node expanded by UCS in a search tree. All the nodes in the search space with cost strictly less than $c(n)$ are expanded \bred{before $n$}.
        \end{lemma}

        \begin{lemma}
            Let $p$ be the first path UCS find whose final state is a state $s$. Then $p$ is a \bred{minimal cost} path to $s$.
        \end{lemma}
    \end{proof}
\end{itemize}

\subsubsection{Properties of UCS}

The time and space complexity of UCS are both \[
    \bigo{ b^{\floor{C^* / \epsilon} + 1} }
\]

\begin{itemize}
    \item UCS has to expand \itblue{all nodes} with cost \bred{less} than $C^*$ and potentially \itblue{all nodes} with cost \bred{equal} to $C^*$.
    \item In the worst case, there are $\bigo{ b^{\floor{C^* / \epsilon} + 1} }$ nodes with cost \itblue{less than or equal to} $C^*$.
\end{itemize}

\begin{remark}
    In the worst case, there are $\color{darkRed} \floor{C^* / \epsilon}$ actions on the optimal path, and each state has $b$ successors, where $C^*$ denotes the cost of the optimal solution path. We also assume each action has cost $\ge \epsilon > 0$.
\end{remark}

\section{Heuristic Search}

\subsection{Heuristic Search Strategies}

Uniform search methods never \itblue{look ahead} to the goal. They enever try to evaluate which nodes on the \Frontier are the most promising. Heuristic search methods, on the other hand, use \itblue{domain-specific knowledge} to evaluate which nodes are the most promising.

\begin{itemize}
    \item If we are concerned about the \bred{cost} of the solution, we might want to consider how costly it's to extend a node so that it reaches a goal node.

    That is, the cost of getting from the node to a goal.

    \item If we are concerned about \bred{minimizing computation} in search we might want to consider how easy it is to extend that node so that it reaches a goal.

    That is, how hard is it to get from the node to a goal.
\end{itemize}

The idea of a heuristic search is to develop a \itblue{domain-specific} \term{heuristic function} $h(n)$ that estimates the cost of the cheapest path from node $n$ to a goal node. $h(n_1) < h(n_2)$ means that we \bred{guess} $n_1$ is \itblue{closer} to a goal than $n_2$.

\begin{definition}[Heuristic Function]\index{Heuristic Function}\label{def:heuristic-function}
    A \term{heuristic function} $h(n)$ is a function that estimates the cost of the cheapest path from node $n$ to a goal node.
\end{definition}

\begin{remark}
    $h(n)$ is a function \bred{only} of the state of $n$. That is, if the states of $n_1$ and $n_2$ are the same, then $h(n_1) = h(n_2)$.
\end{remark}

Sometimes, we might even use a \itblue{state}, rather than a node, as the argument of the heuristic function.

\begin{remark}
    $h$ \bred{must} be defined so that $h(n) = 0$ for any goal node $n$.
\end{remark}

There are different ways of guessing heuristic cost in different domains. For example, Alpha Go exploited machine learning techniques to compute the heuristic estimate of a state (go board configuration).

\begin{example}
    Consider the Romania Travel problem. The heuristic function $h(n)$ is defined to be the Euclidean distance between the current city and Bucharest.

    \begin{center}
        \includegraphics[width=0.67\linewidth]{figures/heuristic-function.png}
    \end{center}
\end{example}

\subsection{Greedy Best-First Search (GBFS)}

Greedy Best-First Search (GBFS) uses $h(n)$ to rank the nodes on the \Frontier. It always expands the node with the \itblue{lowest heuristic cost} $h(n)$.

\begin{definition}[Greedy Best-First Search]\index{Greedy Best-First Search}\label{def:gbfs}
    \term{Greedy Best-First Search} is a search strategy that expands the node with the lowest heuristic cost $h(n)$.
\end{definition}

GBFS greedily tries to find the \itblue{cheapest} path to a goal. It \itblue{ignores} the cost of $n$, so it can be \bred{lead astray} by paths that look promising based on $h(n)$ but actually have a high cost.

\begin{center}
    \tikzexternalenable
    \begin{tikzpicture}[graph-node/.style={circle,draw=black,line width=0.5pt,minimum size=0.67cm}]
        \node[graph-node] (s) at (0, 0) {$s$};
        \node[graph-node] (n1) at (-2, -1) {$n_1$};
        \node[graph-node] (n2) at (-1, -2) {$n_2$};
        \node[graph-node] (n3) at (2, -1) {$n_3$};
        \node[graph-node] (g) at (0, -3) {$g$};

        \draw[thick,darkGreen,-latex] (s) -- (n1) node[midway] {$10$};
        \draw[thick,darkGreen,-latex] (n1) -- (n2) node[midway] {$10$};
        \draw[thick,darkGreen,-latex] (n2) -- (g) node[midway] {$10$};
        \draw[thick,darkRed,-latex] (s) -- (n3) node[midway] {$100$};
        \draw[thick,darkRed,-latex] (n3) -- (g) node[midway] {$10$};
    \end{tikzpicture}
\end{center}

Greedy search can be very efficient in practice at finding solutions, but it requires developing a good heuristic function. Greedy search is \itblue{not complete} and \itblue{not optimal}. The solution returned by a greedy search can be \bred{very far from optimal}.

\begin{example}
    GBFS will be stuck in the following graph, where the optimal heuristic leads to an infinite loop, while only the suboptimal heuristic leads to the goal.

    % TODO: Add graph
\end{example}

\subsection{A* Search}

A* Search is a variant of UCS that uses $h(n)$ to rank the nodes on the \Frontier. It always expands the node with the \itblue{lowest evaluation cost} \[ f(n) = g(n) + h(n), \] where $g(n)$ is the cost of the path from the initial node to node $n$. Here, $f(n)$ is an estimate of the cost of getting to the goal through $n$.

\begin{definition}[A* Search]\index{A* Search}\label{def:astar}
    \term{A* Search} is a search strategy that expands the node with the lowest evaluation cost \[ f(n) = g(n) + h(n). \]
\end{definition}

\begin{example}
    Consider a tracing example of Romania Travel problem. The heuristic function $h(n)$ is defined to be the Euclidean distance between the current city and Bucharest.

    \begin{minipage}[t]{0.45\linewidth} \begin{center} \includegraphics[width=\linewidth]{figures/a-star-search/1.png} Arad \end{center} \end{minipage}
    \hfill%
    \begin{minipage}[t]{0.45\linewidth} \begin{center} \includegraphics[width=\linewidth]{figures/a-star-search/2.png} Sibiu \end{center} \end{minipage}

    {~~~}

    {~~~}

    \begin{minipage}[t]{0.45\linewidth} \begin{center} \includegraphics[width=\linewidth]{figures/a-star-search/3.png} Rimriau V\^alcea \end{center} \end{minipage}
    \hfill%
    \begin{minipage}[t]{0.45\linewidth} \begin{center} \includegraphics[width=\linewidth]{figures/a-star-search/4.png} F{\v a}g{\v a}ra{\c s} \end{center} \end{minipage}

    {~~~}

    {~~~}

    \begin{minipage}[t]{0.45\linewidth} \begin{center} \includegraphics[width=\linewidth]{figures/a-star-search/5.png} Pite{\c s}ti \end{center} \end{minipage}
    \hfill%
    \begin{minipage}[t]{0.45\linewidth} \begin{center} \includegraphics[width=\linewidth]{figures/a-star-search/6.png} Bucharest \end{center} \end{minipage}
\end{example}

\begin{remark}[No Solution Case]
    A*, as well as all of the uninformed search algorithms, have the same behaviour when there is \bred{no solution}:

    \begin{itemize}
        \item If there are an \itblue{infinite} number of different \itblue{states} reachable from the initial state, then these algorithms \bred{never terminate}.
        \item If there are a \itblue{finite} number of different reachable states (and nodes) and we do either \itblue{path checking} or \itblue{cycle checking}, they will eventually terminate and correctly declare that there is no solution (assuming costs are always $\ge \epsilon > 0$).
    \end{itemize}
\end{remark}

\begin{itemize}
    \item \textbf{Completeness}

    \begin{theorem}[Completeness of A* Search]\label{thm:completeness-of-astar}
        A* will always find a solution if one exists as long as

        \begin{itemize}
            \item the branching factor is \itblue{finite};
            \item every action has a \itblue{finite cost} greater than $\epsilon > 0$; and
            \item $h(n)$ is \itblue{finite} for every node $n$ that can be extended to \bred{reach a goal node}.
        \end{itemize}
    \end{theorem}

    \begin{proof}
        If a solution $n$ exists, then at all times either
        \begin{itemize}
            \item $n$ has been expanded by A*, or
            \item an ancestor of $n$ is on the \Frontier.
        \end{itemize}

        Suppose the latter holds, and let the ancestor on the \Frontier~ be $n_i$.

        Then, $n_i$ must have a finite $f$-value.

        As A* continues to run, the $f$-value of the nodes on the \Frontier~eventually \bred{increase}. So, eventually, either A* terminates because it found a solution, or $n_i$ becomes the node \bred{on the \Frontier~with the lowest $f$-value}.

        If $n_i$ is expanded, then either $n_i - n$ and A* returns $n$ as a solution, or $n_i$ is replaced by its successors, one of which $n_{i+1}$ is a \itblue{closer ancestor} of $n$.

        Applying the same argument to $n_{i+1}$, we see that if A* continues to run without finding a solution, it will \bred{eventually expand every ancestor of $n$}, including $n$ itself.

        Therefore, A* will eventually return a solution if one exists.
    \end{proof}

    \item \textbf{Optimality}

    \begin{definition}[Asmissibility]\index{Asmissibility}\label{def:asmissibility}
        Let $h^*(n)$ be the cost of an optimal path from $n$ to a goal node ($\infty$ if there is no path). An \term{admissible} heuristic is a heuristic that satisfies the following condition for all nodes $n$ in the search space: \[
            h(n) \le h^*(n)
        \]
    \end{definition}

    In other words, an admissible heuristic is a heuristic that \bred{never overestimates} the cost of reaching a goal node from $n$.

    To achieve optimality, we must pou come conditions on $h(n)$ and the search space.

    \begin{itemize}
        \item Each action in the search space must have a \itblue{cost $\ge \epsilon > 0$}.
        \item $h$ must be \itblue{admissible}.
    \end{itemize}

    This way, the search will not miss any promising paths. If it really is cheap to get to a goal via $n$ (i.e., both $g(n)$ and $h^*(n)$ are low), then $f(n)$ is also low, and eventually $n$ will be expanded.

    \begin{example}
        Consider when there are two goal nodes, $g_1$ and $g_2$, with $\textsc{Cost}(g_1) < \textsc{Cost}(g_2)$. Let $n$ be a node on the path to $g_1$.

        Then, by having an admissible heuristic, we will always expand $n$ before expanding any node on the path to $g_2$ (even if $g_2$ itself is on the \Frontier).
    \end{example}

    \begin{proposition}\label{prop:optimal-cost}
        A* with an admissible heuristic never expands a node with $f$-value greater than the cost of an optimal solution.
    \end{proposition}

    \begin{proof}
        Let $C^*$ be the cost of an optimal solution.

        Let $p: \langle s_0, s_1, \cdots, s_k \rangle$ be an optimal solution path. Then, $\textsc{cost}(p) = \textsc{cost}( \langle s_0, s_1, \dots, s_k \rangle) = C^*$.

        It can be shown bt induction for each node in the search space that is reachable from the initial node, at every iteration an ancestor of the node is on the \Frontier.

        Let $n$ be a node reachable from the initial state and $n_0, n_1, \dots, n_i, \dots, n$ be ancestors of $n$. SO at least one of $n_0, n_1, \dots, n_i, \dots, n$ is \bred{always} on the \Frontier.

        We show that with an admissible heuristic, for every ancestor $n_i$ of $n$ on the \Frontier, $f(n_i) \le C^*$

        \begin{align*}
            C^8 & = \textsc{Cost} \left( \langle s_0, s_1, \dots, s_k \rangle \right)                                                                             \\
                & = \textsc{Cost} \left( \langle s_0, s_1, \dots, s_i \rangle \right) + \textsc{Cost} \left( \langle s_{i+1}, s_{i+2}, \dots, s_k \rangle \right) \\
                & = g(n_i) + h^*(n_1)                                                                                                                             \\
                & \ge  g(n_i) + h(n_i) = f(n_i)
        \end{align*}

        We know that A* always expands a node on teh \Frontier~ that has \itblue{lowest $f$-value}. So every node A* expands has $f$-value $\le f(n_i) \le C^*$.
    \end{proof}

    \begin{theorem}[Optimality of A* Search]\label{thm:optimality-of-astar}
        A* with an admissible heuristic always finds an optimal cost solution if one exists, as long as

        \begin{itemize}
            \item the branching factor is \itblue{finite}; and
            \item every action has a \itblue{finite cost} greater than $\epsilon > 0$
        \end{itemize}
    \end{theorem}

    \begin{proof}
        Let $C^*$ be the cost of an optimal solution.

        If a solution exists, then by completeness, A* will eventually expand some solution node $n$.

        By the proposition, $f(n) \le C^*$. Since $n$ is a goal node, we have $h(n) = 0$. So $f(n) = g(n) = \textsc{Cost}(n)$. Thus, $\textsc{Cost}(n) \le C^*$.

        We also have that $C^* \le \textsc{Cost}(n) = f(n)$ since no solution can have lower cost than the optimal solution.

        Thus, $\textsc{Cost}(n) = C^*$, and $n$ is an optimal solution.

        That is, A* returns a cost-optimal solution.
    \end{proof}

    \begin{definition}[Monotonicity / Consistency]\index{Monotonicity}\index{Consistency}\label{def:monotonicity}
        A \term{monotone} (or \term{consistent}) heuristic $h$ is a heuristic that satisfies the triangle inequality: for all nodes $n_1, n_2$ and all actions $a$ we have that \[
            h(n_1) \le C(n_1, a, n_2) + h(n_2)
        \] where $C(n_1, a, n_2)$ is the cost of getting from the state $n_1$ to the state $n_2$ by applying action $a$.
    \end{definition}

    In other words, a monotone heuristic will not suddenly decrement the estimated cost by taking a single action. A consistent heuristic will not be misleading, thus improves the efficiency of A*.

    \begin{theorem}
        Monotonicity implies admissibility. That is, \[
            (\forall n_1, n_2 a) h(n_1) \le C(n_1, a, n_2) + h(n_2) \implies (\forall n) h(n) \le h^*(n)
        \]
    \end{theorem}

    If a node $n_2$ is expanded after $n_1$ by $A*$ with a monotonic heuristic, then $f(n_1) \le f(n_2)$. With a monotonic heuristic, the first time A* expands a node $n$, $n$ must be a \itblue{minimum cost} solution to $n.state$.
\end{itemize}

\subsubsection{Properties of A* Search}

\begin{itemize}
    \item \textbf{Completeness and Optimality}

    Yes, as shown in the previous theorems.

    \item \textbf{Time Complexity and Space Complexity}

    When $h(n) = 0$ for all nodes $n$, A* is equivalent to UCS.

    It can be shown that when $h(n) > 0$ (for some $n$) and an admissible heuristic is used, the number of nodes expanded can be no longer than UCS, hence the same bounds on time and space complexity apply. We still have an \bred{exponential} time and space complexity, unless we have a \itblue{good heuristic}. In real-world problems, we sometimes run out of time and memory before we find a solution.
\end{itemize}

\subsection{Iterative Deepening A* (IDA*)}

The objective of \term{Iterative Deepening A*} (IDA*) is to \itblue{avoid} the \itblue{exponential} space complexity of A* by using \itblue{iterative deepening}. Similar to iterative deepening search, IDA* repeatedly applies A* with increasing $f$-value limits until a solution is found. At each iteration, the cutoff value is the \itblue{smallest $f$-value} of any node that exceeded the cutoff in the \itblue{previous iteration}. This will avoid overhead associated with keeping a sorted queue of nodes, and the \Frontier~will occupy only \bred{linear space}.

\begin{definition}[Iterative Deepening A*]\index{Iterative Deepening A*}\label{def:ida-star}
    \term{Iterative Deepening A*} (IDA*) is a search strategy that repeatedly applies A* with increasing $f$-value limits until a solution is found.
\end{definition}

\subsection{Building Heuristic Functions}

\begin{remark}[How to Build A Heuristic]
    \bred{Simplify} a problem when building heuristics and let $h(n)$ be the cost of reaching the goal in the easier problem.
\end{remark}

\begin{example}
    In the 8-Puzzle we can only move a tile from square $A$ to $B$ if $A$ is adjacent (left, right, above, below) to $B$ and $B$ is blank. We can relax some of these conditions and:

    \begin{enumerate}
        \item allow a move from $A$ to $B$ if $A$ is adjacent to $B$ (ignore whether or not position is blank), which leads to the Manhattan distance heuristic

        \item allow a move from $A$ to $B$ if $B$ is blank (ignore adjacency);

        \item allow all moves from $A$ to $B$ (ignore both conditions), which leads to the misplaced tiles heuristic
    \end{enumerate}

    \begin{figure}[ht!]
        \centering

        \begin{subfigure}[t]{0.45\linewidth}
            \centering
            \includegraphics*[width=0.45\linewidth]{figures/8-puzzle-start.png}
            \caption*{Start State}
        \end{subfigure}
        \hfil%
        \begin{subfigure}[t]{0.45\linewidth}
            \centering
            \includegraphics*[width=0.45\linewidth]{figures/8-puzzle-goal.png}
            \caption*{Goal State}
        \end{subfigure}
    \end{figure}

    Admissible Heuristics for the 8-puzzle Problem include:

    \begin{itemize}
        \item $h(n)$: number of misplaced tiles in $n.state$ (misplaced tiles)
        \item $h(n)$: total Manhattan distance of all tiles from their goal positions in $n.state$ (Manhattan distance)
    \end{itemize}
\end{example}