Chapter 3: Solving Problems by Searching
Well-defined problem
1. initial state 2. actions (successor functions) initial state + actions --> state space + path 3. goal test 4. path cost/step cost
Well-defined problem
A problem has 4 formal components: 1. The initial state that the agent starts in. 2. A description of the possible actions available to the agent. The most common formulation uses a successor function. Together, the initial state and successor function implicitly define the state space of the problem-the set of all states reachable from the initial state. The state space forms a graph in which the nodes are states and the arcs between nodes are actions. A path in the state space is a sequence of states connected by a sequence of actions. 3. The goal test, which determines whether a given state is a goal state. Sometimes there is an explicit set of possible goal states, and the test simply checks whether the given state is one of them. 4. A path cost function that assigns a numeric cost to each path. The problem-solving agent chooses a cost function that reflects its own performance measure.
What does a search algorithm do?
A search algorithm takes a problem as input and returns a solution in the form of an action sequence.
Solution
A solution to a problem is a path from the initial state to a goal state. Solution quality is measured by the path cost function, and an optimal solution has the lowest path cost among all solutions.
Backtracking Search
A variant of depth-first search called backtracking search uses still less memory. In backtracking, only one successor is generated at a time rather than all successors; each partially expanded node remembers which successor to generate next. In this way, only O(m) memory is needed rather than O(bm). Backtracking search facilitates yet another memory-saving (and time-saving) trick: the idea of generating a successor by modifying the current state description directly rather than copying it first. This reduces the memory requirements to just one state description and O(m) actions. For this to work, we must be able to undo each modification when we go back to generate the next successor.
Breadth-first search
Breadth-first search is a simple strategy in which the root node is expanded first, then all the successors of the root node are expanded next, then their successors, and so on. In general, all the nodes are expanded at a given depth in the search tree before any nodes at the next level are expanded. Breadth-first search can be implemented by calling TREE-SEARCH with an empty fringe that is a first-in-first-out (FIFO) queue, assuring that the nodes that are visited first will be expanded first. In other words, calling TREE-SEARCH(problem,FIFO-QUEUE()) results in a breadth-first search. The FIFO queue puts all newly generated successors at the end of the queue, which means that shallow nodes are expanded before deeper nodes.
Uniform-cost search
Breadth-first search is optimal when all step costs are equal, because it always expands the shallowest unexpanded node. By a simple extension, we can find an algorithm that is optimal with any step cost function. Instead of expanding the shallowest node, uniform-cost search expands the node n with the lowest path cost. Note that if all step costs are equal, this is identical to breadth-first search. Uniform-cost search does not care about the number of steps a path has, but only about their total cost. Therefore, it will get stuck in an infinite loop if it ever expands a node that has a zero-cost action leading back to the same state.
Evaluation of bidirectional search
Checking a node for membership in the other search tree can be done in constant time with a hash table, so the time complexity of bidirectional search is O(b^d/2). At least one of the search trees must be kept in memory so that the membership check can be done, hence the space complexity is also O(b^d/2). This space requirement is the most significant weakness of bidirectional search. The algorithm is complete and optimal (for uniform step costs) if both searches are breadth-first; other combinations may sacrifice completeness, optimality, or both.
Evaluation of Depth-First Search
Completeness: If the left subtree were of unbounded depth but contained no solutions, depth-first search would never terminate. Optimality: The drawback of depth-first search is that it can make a wrong choice and get stuck going down a very long (or even infinite) path when a different choice would lead to a solution near the root of the search tree. Time Complexity: In the worst case, depth-first search will generate all of the O(b^m) nodes in the search tree, where m is the maximum depth of any node. Note that m can be much larger than d (the depth of the shallowest solution), and is infinite if the tree is unbounded. Space Complexity: Depth-first search has very modest memory requirements. It needs to store only a single path from the root to a leaf node, along with the remaining unexpanded sibling nodes for each node on the path. Once a node has been expanded, it can be removed from memory as soon as all its descendants have been fully explored.
Evaluation of Iterative Deepening Depth-First Search
Completeness: Like breadth-first search, it is complete when the branching factor is finite and optimal when the path cost is a nondecreasing function of the depth of the node. Time Complexity: Iterative deepening search may seem wasteful, because states are generated multiple times. It turns out this is not very costly. The reason is that in a search tree with the same (or nearly the same) branching factor at each level, most of the nodes are in the bottom level, so it does not matter much that the upper levels are generated multiple times. In an iterative deepening search, the nodes on the bottom level (depth d) are generated once, those on the next to bottom level are generated twice, and so on, up to the children of the root, which are generated d times. Space Complexity: Like depth-first search, its memory requirements are very modest: O(bd) to be precise. In general, iterative deepening is the preferred uninformed search method when there is a large search space and the depth of the solution is not known.
Evaluation of Uniform-Cost Search
Completeness: We can guarantee completeness provided the cost of every step is greater than or equal to some small positive constant c. Optimality: This condition is also sufficient to ensure optimality. It means that the cost of a path always increases as we go along the path. From this property, it is easy to see that the algorithm expands nodes in order of increasing path cost. Therefore, the: first goal node selected for expansion is the optimal solution. Time and Space Complexity: Uniform-cost search is guided by path costs rather than depths, so its complexity cannot easily be characterized in terms of b and d. Instead, let C* be the cost of the optimal solution, and assume that every action costs at least E. Then the algorithm's worst-case time and space complexity can be much greater than b^d. This is because uniform-cost search can, and often does, explore large trees of small steps before exploring paths involving large and perhaps useful steps
Depth-first search
Depth-first search always expands the deepest node in the current fringe of the search tree. The search proceeds immediately to the deepest level of the search tree, where the nodes have no successors. As those nodes are expanded, they are dropped from the fringe, so then the search "backs up" to the next shallowest node that still has unexplored successors. This strategy can be implemented by TREE-SEARCH with a last-in-first-out (LIFO) queue, also known as a stack. As an alternative to the TREE-SEARCH implementation, it is common to implement depth-first search with a recursive function that calls itself on each of its children in turn.
Searching for solutions
Having formulated some problems, we now need to solve them. This is done by a search through the state space. This chapter deals with search techniques that use an explicit search tree that is generated by the initial state and the successor function that together define the state space. The root of the search tree is a search node corresponding to the initial state. The first step is to test whether this is a goal state. Because this is not a goal state, we need to consider some other states. This is done by expanding the current state; that is, applying the successor function to the current state, thereby generating a new set of states. We continue choosing, testing, and expanding until either a solution is found or there are no more states to be expanded. The choice of which state to expand is determined by the search strategy.
Expressing complexity
In AI, where the graph is represented implicitly by the initial state and successor function and is frequently infinite, complexity is expressed in terms of three quantities: b, the branching factor or maximum number of successors of any node; d, the depth of the shallowest goal node; and m, the maximum length of any path in the state space.
search
In general, an agent with several immediate options of unknown value can decide what to do by just examining different possible sequences of actions that lead to states of known value, and then choosing the best sequence. This process of looking for such a sequence is called search.
state space vs. search tree
It is important to distinguish between the state space and the search tree. For the route finding problem, there are only 20 states in the state space, one for each city. But there are an infinite number of paths in this state space, so the search tree has an infinite number of nodes.
nodes vs. states
It is important to remember the distinction between nodes and states. A node is a bookkeeping data structure used to represent the search tree. A state corresponds to a configuration of the world. Thus, nodes are on particular paths, as defined by PARENT-NODE pointers, whereas states are not. Furthermore, two different nodes can contain the same world state, if that state is generated via two different search paths.
Iterative deepening depth-first search
Iterative deepening search (or iterative deepening depth-first search) is a general strategy, often used in combination with depth-first search, that finds the best depth limit. It does this by gradually increasing the limit-first 0, then 1, then 2, and so on-until a goal is found. This will occur when the depth limit reaches d, the depth of the shallowest goal node. Iterative deepening combines the benefits of depth-first and breadth-first search
How does a search algorithm work?
Once a solution is found, the actions it recommends can be carried out. This is called the execution phase. Thus, we have a simple "formulate, search, execute" design for the agent. After formulating a goal and a problem to solve, the agent calls a search procedure to solve it. It then uses the solution to guide its actions, doing whatever the solution recommends as the next thing to do-typically, the first action of the sequence-and then removing that step from the sequence. Once the solution has been executed, the agent will formulate a new goal.
Evaluation of Breadth-First Search
Pros: Completeness: complete-if the shallowest goal node is at some finite depth d, breadth-first search will eventually find it after expanding all shallower nodes (provided the branching factor b is finite). Optimality: The shallowest goal node is not necessarily the optimal one; technically, breadth-first search is optimal if the path cost is a nondecreasing function of the depth of the node. (For example, when all actions have the same cost.) Cons: Every node that is generated must remain in memory, because it is either part of the fringe or is an ancestor of a fringe node. The space complexity is, therefore, the same as the time complexity (plus one node for the root).
Example Problem: Vacuum World
The first example we will examine is the vacuum world. This can be formulated as a problem as follows: States: The agent is in one of two locations, each of which might or might not contain dirt. Thus there are 2 x 2^2 = 8 possible world states. Initial state: Any state can be designated as the initial state. Successor function: This generates the legal states that result from trying the three actions (Left, Right, and Suck). Goal test: This checks whether all the squares are clean. Path cost: Each step costs 1, so the path cost is the number of steps in the path.
Bidirectional search
The idea behind bidirectional search is to run two simultaneous searches-one forward from the initial state and the other backward from the goal, stopping when the two searches meet in the middle. Bidirectional search is implemented by having one or both of the searches check each node before it is expanded to see if it is in the fringe of the other search tree; if so, a solution has been found. For example, if a problem has solution depth d = 6, and each direction runs breadth-first search one node at a time, then in the worst case the two searches meet when each has expanded all but one of the nodes at depth 3. For b = 10, this means a total of 22,200 node generations, compared with 1 1,111,100 for a standard breadth-first search.
Measuring problem-solving performance
The output of a problem-solving algorithm is either failure or a solution. (Some algorithms might get stuck in an infinite loop and never return an output.) We will evaluate an algorithm's performance in four ways: ~ Completeness: Is the algorithm guaranteed to find a solution when there is one? ~ Optimality: Does the strategy find the optimal solution (the lowest path cost among all solutions)? ~ Time complexity: How long does it take to find a solution? ~ Space complexity: How much memory is needed to perform the search?
Depth-limited search
The problem of unbounded trees can be alleviated by supplying depth-first search with a predetermined depth limit l. That is, nodes at depth l are treated as if they have no successors. This approach is called depth-limited search. The depth limit solves the infinite-path problem. Unfortunately, it also introduces an additional source of incompleteness if we choose t < d, that is, the shallowest goal is beyond the depth limit. (This is not unlikely when d is unknown.) Depth-limited search will also be nonoptimal if we choose l > d. Its time complexity is O(be) and its space complexity is O(bl). Depth-first search can be viewed as a special case of depth-limited search with l = infinity. Sometimes, depth limits can be based on knowledge of the problem. For example, on the map of Romania there are 20 cities. Therefore, we know that if there is a solution, it must be of length 19 at the longest, so l = 19 is a possible choice. But in fact if we studied the map carefully, we would discover that any city can be reached from any other city in at most 9 steps. This number, known as the diameter of the state space, gives us a better depth limit, which leads to a more efficient depth-limited search.
uninformed search (also called blind search)
The term means that they have no additional information about states beyond that provided in the problem definition. All they can do is generate successors and distinguish a goal state from a non-goal state. Strategies that know whether one non-goal state is "more promising" than another are called informed search or heuristic search strategies; they will be covered in Chapter 4. All search strategies are distinguished by the order in which nodes are expanded.
Node representation
There are many ways to represent nodes, but we will assume that a node is a data structure with five components: STATE: the state in the state space to which the node corresponds; PARENT-NODE: the node in the search tree that generated this node; ACTION: the action that was applied to the parent to generate the node; PATH-COST: the cost, traditionally denoted by g(n), of the path from the initial state to the node, as indicated by the parent pointers; and DEPTH: the number of steps along the path from the initial state
Measuring time
Time is often measured in terms of the number of nodes generated during the search, and space in terms of the maximum number of nodes stored in memory.
Assessing the effectiveness of a search algorithm
To assess the effectiveness of a search algorithm, we can consider just the search cost which typically depends on the time complexity but can also include a term for memory usage-or we can use the total cost, which combines the search cost and the path cost of the solution found.
the fringe
the collection of nodes that have been generated but not yet expanded. Each element of the fringe is a leaf node, that is, a node with no successors in the tree.