2025-10-26
Goal: Find a path from a start state to a goal state in a state space.
Components:
Idea: Explore the most promising node first based on a given evaluation function.
Very Generic: Can be adapted to various search strategies each with its own evaluation function.
Data Structures:
Algorithm Steps:
Algorithm Steps:
Algorithm Steps:
A* Search Algorithm: A specific instance of Best-First Search that uses the evaluation function:
\[f(u) = g(u) + h(u)\]
where:
Nearly all search algorithms you know can be expressed as Best-First Search
| Search Algorithm | Evaluation Function \(f(u)\) |
|---|---|
| Breadth-First Search | \(f(u) = depth(u)\) |
| Uniform-Cost Search (Dijkstra) | \(f(u) = g(u)\) |
| Greedy Best-First Search | \(f(u) = h(u)\) |
| A* Search | \(f(u) = g(u) + h(u)\) |
where \(b\) is the branching factor and \(d\) is the depth of the goal node.
Once G is expanded, the algorithm determines that it is a goal state and returns the solution path.
A heuristic function \(h(u)\) is an educated guess of the cost to reach the goal from node \(u\).
Efficiency
A good heuristic function can significantly decrease the effective branching factor \(b^*\), leading to faster search times and reduced memory usage.
Optimality
Depending on the properties of the heuristic function, A* can guarantee finding the optimal solution.
No Repeat States
A good heuristic can help avoid re-expanding nodes.
A heuristic function \(h(u)\) is admissible if it never overestimates the true cost to reach the goal from node \(u\).
\[h(u) \leq h^*(u)\]
where \(h^*(u)\) is the actual cost to reach the goal from node \(u\).
Example: Straight-line distance in pathfinding problems, no. of misplaced tiles in 8-puzzle.
Properties
A heuristic function \(h(u)\) is consistent if, for every node \(u\) and every successor \(v\) of \(u\), the following holds:
\[h(u) \leq c(u, v) + h(v)\]
where \(c(u, v)\) is the cost of the edge from \(u\) to \(v\).
Example: Straight-line distance in pathfinding problems.
Properties
class SearchProblem(ABC):
@abstractmethod
def is_goal(self, state: SearchProblemState) -> bool:
"""
Check if the given state is a goal state.
"""
pass
@abstractmethod
def transition(
self, state: SearchProblemState, action: SearchProblemAction
) -> SearchProblemState:
"""
Given a state and an action, return the
resulting state after applying the action.
"""
pass
@abstractmethod
def get_initial_state(self) -> SearchProblemState:
"""
Return the initial state of the search problem.
"""
passdef best_first_search(
problem: SearchProblem,
search_function: BestFirstSearchFunction,
) -> tuple[SearchNode | None, int]:
root_node = SearchNode(state=problem.get_initial_state(),path_cost=0)
frontier = PriorityQueue()
frontier.put((search_function.evaluate(root_node), 0, root_node))
explored: dict[SearchProblemState, SearchNode] = {}
no_explored = 0
no_frontier_updates = 0
while frontier.qsize() > 0:
# ... [upcoming code] ...
return None, no_explored# ... [previous code] ...
_, _, current_node = frontier.get()
no_explored += 1
if problem.is_goal(current_node.state):
return current_node, no_explored
explored[current_node.state] = current_node
for action in current_node.state.get_actions():
child_node = SearchNode(
state=problem.transition(current_node.state, action),
parent=current_node,
action=action,
path_cost=current_node.path_cost + action.get_cost(),
)
# ... [upcoming code] ...
# ... [previous code] ...# ... [previous code] ...
if (
child_node.state not in explored
or child_node.path_cost < explored[child_node.state].path_cost
):
no_frontier_updates += 1
frontier.put((search_function.evaluate(child_node),
no_frontier_updates,
child_node,
)
)
explored[child_node.state] = child_node
# ... [previous code] ...class HeuristicFunction(ABC):
@abstractmethod
def estimate(self, state: SearchProblemState) -> float:
pass
class AStarSearchFunction(BestFirstSearchFunction):
def __init__(self, heuristic: HeuristicFunction):
self.heuristic = heuristic
def evaluate(self, node: SearchNode) -> float:
return node.path_cost + self.heuristic.estimate(node.state)Place N queens on an N x N chessboard such that no two queens attack each other.
Number of pairs of queens that are attacking each other.
Reasoning: - We care about reaching a goal more than the path cost (placing queens is cheap). - So, we use a penalty-based heuristic. - The more pairs of attacking queens, the worse the state.
class NQueensState(SearchProblemState):
def __init__(self, board: list[int], n: int):
assert len(board) == n
assert all(0 <= row < n for row in board)
# board is a list where index is column and value is row
self.board = board
self.n = n
def get_actions(self) -> Iterable[SearchProblemAction]:
for col in range(self.n):
for row in range(self.n):
if self.board[col] != row:
yield NQueensAction(col, row)class NQueensProblem(SearchProblem):
def __init__(self, n initial_state: NQueensState | None = None):
self.n = n
self.initial_state = initial_state
def transition(
self, state: SearchProblemState, action: SearchProblemAction
) -> SearchProblemState:
new_board = state.board[:]
new_board[action.col] = action.new_row
return NQueensState(new_board, self.n)
def get_initial_state(self) -> SearchProblemState:
return self.initial_stateclass NQueensProblem(SearchProblem):
# ... [previous code] ...
def is_goal(self, state: SearchProblemState) -> bool:
board = state.board
n = self.n
if len(set(board)) < n:
return False # Two queens in the same row
for col1 in range(n):
for col2 in range(col1 + 1, n):
if abs(board[col1] - board[col2]) == abs(col1 - col2):
return False # Two queens on the same diagonal
return Trueclass NoQueenAttackHeuristic(HeuristicFunction):
def estimate(self, state: NQueensState) -> float:
board = state.board
n = state.n
attacks = 0
for col1 in range(n):
for col2 in range(col1 + 1, n):
if board[col1] == board[col2] or (
abs(board[col1] - board[col2]) == abs(col1 - col2)
):
attacks += 1
return float(attacks)Test Setup
| Algorithm | Avg. Explored |
|---|---|
| Uniform-Cost Search | 171,973.2 |
| A* Search (Row Conflicts Heuristic) | 5,165.0 |
| A* Search (Total No. of Attacks Heuristic) | 52.6 |


