Search Algorithms
Heuristics
a “good” way to evaluate a problem state
- can be an approximation, an educated guess, not perfect
- why?: many problems where performing a brute force search is not computationally feasible
Connect Four Heuristics Example
- heuristic should rank states closer to goal higher
- Goal: win the game
- e.g.
- four same coloured pieces in a row: +
- 3 same coloured pieces in a row: +500
- 2 same coloured pieces in a row: +100
- piece in the middle column: +10
- 3 same coloured pieces with an open spot at each end?
Sliding Puzzle Heuristic
- e.g.
- manhattan distance (number of tiles vertically+horizontally from intended tile)
- hamming distance (number of tiles that are out of place)
- ignoring the blank tile → adds unnecessary complexity
A* Search
- uses information gathered from the problem
- heuristic
- guaranteed to find the shortest path if your heuristic is admissible (never over estimates cost to reach the goal)
- evaluates path with function: where:
- : path cost from the root to node n
- : heuristic cost from n to the goal state
Iterative Deepening A* Search (IDA*)
- implementation is quite different than A*
- only remembers current path
- A* implementation typically uses dynamic programming while IDA* uses recursion
IDA* implementation
- define a “cutoff” value
- if > cutoff, stop expansion
- increase the cutoff value based on minimum of the visited child nodes
- commonly implemented with a stack tracking the current path, no priority queue
Pseudocode:
IDAStar(Node root)
cutoff = h(root)
path = Stack<Node>
path.push(root)
repeat
cheapestCost = recurse(path, 0, cutoff)
cutoff = cheapestCost
recurse(path, currentPathCost, cutoff)
currentNode = path[-1]
newCost = currentPathCost + h(currentNode)
if newCost > cutoff
return newCost
if currentNode.state == goal_state
// Goal found, print path, exit
min = MAX_VALUE
for each child in currentNode.children
if child not in path
path.push(child)
childPathCost = recurse(path, currentPathCost + 1, cutoff)
// +1 = cost from currentNode to child
min = Math.min(childPathCost, min)
path.pop()
return min
Python:
def ida_star(root):
cutoff = h(root)
path = [root] # use list as stack
while True:
cheapest_cost = recurse(path, 0, cutoff)
if cheapest_cost == float("inf"):
return None # no solution
if isinstance(cheapest_cost, Node): # found goal, return path or node
return cheapest_cost
cutoff = cheapest_cost
def recurse(path, current_path_cost, cutoff):
current_node = path[-1]
new_cost = current_path_cost + h(current_node)
if new_cost > cutoff:
return new_cost
if current_node.state == goal_state:
print("Goal found:", path)
return current_node
min_cost = float("inf")
for child in current_node.children:
if child not in path:
path.append(child)
child_path_cost = recurse(path, current_path_cost + 1, cutoff)
# +1 = cost from currentNode to child
if isinstance(child_path_cost, Node): # propagate solution
return child_path_cost
min_cost = min(child_path_cost, min_cost)
path.pop()
return min_cost
IDA* complexity
- space complexity: O(bd)
- only remembers current path (at most, b*d)
- A*: all nodes
- time complexity: depends on heuristic
- worst case: O()
- same as A*
Iterative Deepening Search
- repeatedly apply DFS with an iteratively increasing depth limit
- only check for goal state when reaching max depth current iteration
- applicable for graphs/trees
IDS implementation
boolean IDS(node, target, maxDepth)
for depth from 0 to maxDepth
if DFS(node, target, depth)
return true
return false
boolean DFS(node, target, depth)
if depth == 0 // max depth reached
if node == target
return true
return false
for each child of node
if DFS(child, target, depth - 1)
return true
return false
def ids(node, target, max_depth):
for depth in range(max_depth + 1):
if dfs(node, target, depth):
return True
return False
def dfs(node, target, depth):
if depth == 0: # max depth reached
return node == target
for child in node.children: # assumes node has a .children list
if dfs(child, target, depth - 1):
return True
return False
IDS complexity
- time complexity: O()
- b: branching factor (max number of children a node can have)
- d: max depth
- space complexity: O(bd)
IDS vs BFS
- new nodes are visited in the same order
- IDS requires less memory
- useful for very large graphs/trees
- O(bd) vs O()
- IDS provides intermediate results
- useful for game tree searches (checkers, chess, go, etc.)