Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

State Space Search

Intelligence is fundamentally about making effective choices. To make a good choice, an agent must consider various alternatives and their potential effects. The systematic process of considering these alternatives is known as search.

A core hypothesis in AI is that all problems requiring intelligence can be formulated as a state space, where intelligence is demonstrated by searching within that space.


A problem is defined by its state space, which consists of the following four elements:

  1. State: A specific condition or mode of the problem.

  2. Initial State: The start state from which the program attempts to solve the problem.

  3. Set of Operators: Actions available within the problem framework. An operator transitions the system from the current state to another valid state.

  4. Goal State: The desired state the problem should end in.

Note: While usually interested in the specific sequence of operators (the path) required to reach the goal state, this is not always the case.

Examples of State Space Problems

Annapolis waypoints. Find the shortest path using waypoints.

Annapolis waypoints. Find the shortest path using waypoints.

These problems can all be mathematically modeled as a graph.


The Generate and Test Algorithm

The basic process of search involves “expanding” a state by generating its successors. Most search algorithms follow the generate-and-test model which is really just a general paradigm for searching a state space. You can think of it like this:

GenerateTest(StartState s)
  while (isNotGoal(s))
    generate successors to s
    point successors back to s
    add successors to the open set
    add s to the closed set
    s <- SOME state from open

State Space vs. Search Tree: It is important to distinguish between the state space and the search tree. Because algorithms may loop or undo previous operators, the resulting search tree is often significantly larger than the underlying state space. These graphs are implicit; rather than storing the entire graph in memory, the algorithm keeps only a portion in memory, implying the rest via the start state and our operators.


Search Algorithm Evaluation Criteria

When evaluating search algorithms, we consider four key metrics:

  1. Completeness: Is the algorithm guaranteed to find a solution if one exists?

  2. Optimality: Does the algorithm guarantee finding the optimal (best) solution?

  3. Time Complexity: How long does it take to find a solution? This is typically measured in the worst-case scenario, though average-case is sometimes used.

  1. Space Complexity: How much memory does the algorithm require? In AI, memory constraints are often more critical than time constraints.


Brute-Force Search Strategies

Brute-force (or uninformed) search strategies do not use domain-specific knowledge to guide the search. The four we cover in this class are:

1. Breadth-First Search (BFS)

BFS expands the current node and then all of its children before moving deeper. It covers all nodes at depth 1, then depth 2, and so on. Pseudocode:

BreadthFirstSearch(StartState s)
  add s to closed
  while ( s != null and isNotGoal(s) )
    generate successors to s
    for each successor not in closed
      point successor back to s
      add successor to closed
      add successor to open queue
    s <- next state from open queue

  return s
Basic graph. Example of a graph with nodes that you might search.

Basic graph. Example of a graph with nodes that you might search.

Example trace of the search:

Open QueueClosed ListComment
(A)Start at A
(B A) (C A)(A)Add neighbors of A
(C A) (E B A) (F B A) (G B A)(A B)New states go to the back
(E B A) (F B A) (G B A) (D C A)(A B C)G already in open set
(F B A) (G B A) (D C A) (H E B A)(A B C E)Search continues...

Analysis:

Scale of the Problem:

DepthNodesTimeMemory
01.00001 sec100 bytes
2111.001 sec10 KB
411,111.11 sec1 MB
610 sec111 MB
817 min11 GB
1028 hours1 TB
12116 days111 TB
1432 years11 PB

What does this tell us? If the problem is big enough, then time can be a serious contstraint, BUT using BFS, the real problem is memory. I commonly have problems that take an hour or 2 to run, but 11G of RAM is not easy to come by even on today’s bigger machines. Serious problems are often worth waiting a week or 2 to run, but where are you going to get a terabyte? And that is only at depth 10.

Uniform Cost is just like breadth-first search, except instead of expanding a node from the shallowest depth, you expand a node with the minimal cost accrued so far g(n).

Pseudocode:

UniformCost(StartState s)
  while ( s != null and isNotGoal(s) )
    add s to closed
    generate successors to s
    for each successor c not in closed
        calculate g(c)
        point c back to s
        add c to open priority queue
    s <- next state from open

  return s
Graph with edge costs. Example of a graph with edges that have costs.

Graph with edge costs. Example of a graph with edges that have costs.

Key Notes on the Closed List:

Observations: This uniform cost search has similar characteristics of breadth-first search, except it works better for searches with costs. This is the famous Dijkstra’s algorithm.

Analysis:

3. Depth-First Search (DFS)

DFS always expands the node deepest in the search tree. It utilizes a stack, and importantly for us, we do not use a CLOSED LIST! You may see DFS implementations elsewhere that use a closed list, but we will focus on a memory-efficient version that does not need it.

Pseudocode:

DepthFirstSearch(StartState s)
  while (s != null and isNotGoal(s))
    generate successors to s (not on this path)
    point successors back to s
    push each successor on open stack
    s <- pop next state from open
  return s

Basic graph. Example of a graph with nodes that you might search.

Basic graph. Example of a graph with nodes that you might search.

Example trace of depth-first search:

Open QueueComment
(A)
(B A) (C A)not exciting yet
(E B A) (F B A) (G B A) (C A)C is getting buried on the stack
(H E B A) (D E B A) (F B A) (G B A) (C A)Can’t count on particular expansion order
(K H E B A) (J H E B A) (G H E B A) (D E B A) (F B A) (G B A) (C A)hey, this isn’t the shortest path...
......

Analysis:

4. Depth-First Iterative Deepening (DFID)

Depth-First Iterative Deepening (DFID) is an algorithm designed to combine the best properties of Depth-First Search and Breadth-First Search. It attempts to add optimality and completeness to DFS while maintaining its efficient space complexity.

The Strategy: Depth-Limited Search

To understand DFID, imagine a Depth-Limited DFS. This is a standard DFS that treats any node at a specific depth d as a dead end (a “cutoff”).

If the true minimal solution lies at depth s, running a DFS with a limit of d yields three possible outcomes:

  1. s > d : The DFS will not find a solution (the cutoff is too shallow).

  2. s < d : The DFS will find a solution, but it is not guaranteed to be optimal (it may find a deeper, non-optimal solution down a different branch first).

  3. s = d : The DFS will find a solution, and it is guaranteed to be optimal.

Ideally, we would run a depth-limited DFS where the limit equals the exact depth of the solution (s=d). However, since we do not know the depth of the solution beforehand, we must find it iteratively.

The Algorithm

DFID proposes a simple iterative process:

  1. Perform a depth-limited DFS with a limit of 0.

  2. If a solution is found, terminate.

  3. If not, increment the depth limit by 1 and start over.

  4. Repeat this process until a solution is found.

Why this works:


Time Complexity Analysis

But wait! Since we restart the search from scratch for every new depth limit, aren’t we doing a significant amount of redundant work?

Let’s analyze the number of nodes generated. Assume a branching factor b and a solution depth s.

Summing this up for the total search gives us: b^d + 2b^(d-1) + 3b^(d-2) + ... + db^0

While this looks like a lot of extra work, the total work is dominated by the largest term. This series is bounded by a polynomial where the dominant term is b^d

Conclusion: Despite the re-generation of upper levels, the asymptotic time complexity remains O(b^d).

DFID is often a “big win” for large search spaces requiring brute-force algorithms, as it achieves the low memory footprint of DFS with the optimality of BFS.

The idea of bi-directional search is to reduce time while caring less about space. This strategy runs two simultaneous searches: one forward from the start state and one backward from the goal state. A solution is found when the two meet in the middle.


Partial Information Scenarios

In some cases, the agent lacks full knowledge of the state space: