A* heuristic search¶
A* is Dijkstra ordered by f(state) = g(state) + h(state): known path cost plus
a lower bound on remaining cost. It helps when the target is specific and the
heuristic focuses exploration.
Correctness contract¶
h(state) ≥ 0.h(goal) = 0.- Admissible:
hnever overestimates the true remaining cost. - Prefer consistent:
h(u) ≤ cost(u,v) + h(v)for every edge. Then a state need not be reopened after its best priority is finalized under the usual implementation.
flowchart TD
accTitle: A-star state expansion flow
accDescr: Pop the state with smallest known cost plus heuristic. Ignore a stale entry, finish at the goal, otherwise relax neighbors and push each improved cost with its admissible estimate.
A["pop minimum f = g + h"] --> B{"Is heap entry current?"}
B -->|no| A
B -->|yes| C{"Is this the goal?"}
C -->|yes| D["return g"]
C -->|no| E["relax passable neighbors"]
E --> F["push improved g + h"]
F --> A
For four-direction unit-cost movement, Manhattan distance is admissible because each move changes row or column by only one.
Tested grid template¶
def a_star_grid(
grid: Sequence[Sequence[int]], start: tuple[int, int], goal: tuple[int, int]
) -> int:
"""Return a shortest four-direction path length through zero cells."""
if not grid or not grid[0] or any(len(row) != len(grid[0]) for row in grid):
raise ValueError("grid must be nonempty and rectangular")
rows, columns = len(grid), len(grid[0])
for row, column in (start, goal):
if not 0 <= row < rows or not 0 <= column < columns:
raise ValueError("endpoint outside grid")
if grid[row][column] != 0:
return -1
best = {start: 0}
frontier = [(abs(start[0] - goal[0]) + abs(start[1] - goal[1]), 0, start)]
while frontier:
_estimate, distance, cell = heappop(frontier)
if distance != best[cell]:
continue
if cell == goal:
return distance
row, column = cell
for row_step, column_step in ((1, 0), (-1, 0), (0, 1), (0, -1)):
neighbor = (row + row_step, column + column_step)
if not (0 <= neighbor[0] < rows and 0 <= neighbor[1] < columns):
continue
if grid[neighbor[0]][neighbor[1]] != 0:
continue
candidate = distance + 1
if candidate < best.get(neighbor, inf):
best[neighbor] = candidate
# Manhattan distance is admissible for four-direction unit moves.
heuristic = abs(neighbor[0] - goal[0]) + abs(neighbor[1] - goal[1])
heappush(frontier, (candidate + heuristic, candidate, neighbor))
return -1
inline int a_star_grid(const std::vector<std::vector<int>>& grid,
std::pair<int, int> start,
std::pair<int, int> goal) {
if (grid.empty() || grid.front().empty()) {
throw std::invalid_argument("grid must be nonempty and rectangular");
}
const int rows = static_cast<int>(grid.size());
const int columns = static_cast<int>(grid.front().size());
for (const auto& row : grid) {
if (static_cast<int>(row.size()) != columns) {
throw std::invalid_argument("grid must be nonempty and rectangular");
}
}
const auto valid_cell = [&](const std::pair<int, int>& cell) {
return cell.first >= 0 && cell.first < rows && cell.second >= 0 &&
cell.second < columns;
};
if (!valid_cell(start) || !valid_cell(goal)) {
throw std::invalid_argument("endpoint outside grid");
}
if (grid[start.first][start.second] != 0 ||
grid[goal.first][goal.second] != 0) {
return -1;
}
typedef std::tuple<int, int, int, int> State;
std::priority_queue<State, std::vector<State>, std::greater<State>> frontier;
std::vector<std::vector<int>> best(
rows, std::vector<int>(columns, std::numeric_limits<int>::max()));
best[start.first][start.second] = 0;
frontier.push(State(std::abs(start.first - goal.first) +
std::abs(start.second - goal.second),
0, start.first, start.second));
const int moves[4][2] = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
while (!frontier.empty()) {
const State state = frontier.top();
frontier.pop();
const int distance = std::get<1>(state);
const int row = std::get<2>(state);
const int column = std::get<3>(state);
if (distance != best[row][column]) {
continue;
}
if (row == goal.first && column == goal.second) {
return distance;
}
for (int move = 0; move < 4; ++move) {
const int next_row = row + moves[move][0];
const int next_column = column + moves[move][1];
if (next_row < 0 || next_row >= rows || next_column < 0 ||
next_column >= columns || grid[next_row][next_column] != 0) {
continue;
}
const int candidate = distance + 1;
if (candidate < best[next_row][next_column]) {
best[next_row][next_column] = candidate;
// Manhattan distance is admissible for four-direction unit moves.
const int heuristic = std::abs(next_row - goal.first) +
std::abs(next_column - goal.second);
frontier.push(
State(candidate + heuristic, candidate, next_row, next_column));
}
}
}
return -1;
}
inline int a_star_grid(const std::vector<std::vector<int>>& grid,
std::pair<int, int> start,
std::pair<int, int> goal) {
if (grid.empty() || grid.front().empty()) {
throw std::invalid_argument("grid must be nonempty and rectangular");
}
const int rows = static_cast<int>(grid.size());
const int columns = static_cast<int>(grid.front().size());
for (const auto& row : grid) {
if (static_cast<int>(row.size()) != columns) {
throw std::invalid_argument("grid must be nonempty and rectangular");
}
}
const auto valid_cell = [&](const std::pair<int, int>& cell) {
return cell.first >= 0 && cell.first < rows && cell.second >= 0 &&
cell.second < columns;
};
if (!valid_cell(start) || !valid_cell(goal)) {
throw std::invalid_argument("endpoint outside grid");
}
if (grid[start.first][start.second] != 0 ||
grid[goal.first][goal.second] != 0) {
return -1;
}
typedef std::tuple<int, int, int, int> State;
std::priority_queue<State, std::vector<State>, std::greater<State>> frontier;
std::vector<std::vector<int>> best(
rows, std::vector<int>(columns, std::numeric_limits<int>::max()));
best[start.first][start.second] = 0;
frontier.push(State(std::abs(start.first - goal.first) +
std::abs(start.second - goal.second),
0, start.first, start.second));
const int moves[4][2] = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
while (!frontier.empty()) {
const State state = frontier.top();
frontier.pop();
const int distance = std::get<1>(state);
const int row = std::get<2>(state);
const int column = std::get<3>(state);
if (distance != best[row][column]) {
continue;
}
if (row == goal.first && column == goal.second) {
return distance;
}
for (int move = 0; move < 4; ++move) {
const int next_row = row + moves[move][0];
const int next_column = column + moves[move][1];
if (next_row < 0 || next_row >= rows || next_column < 0 ||
next_column >= columns || grid[next_row][next_column] != 0) {
continue;
}
const int candidate = distance + 1;
if (candidate < best[next_row][next_column]) {
best[next_row][next_column] = candidate;
// Manhattan distance is admissible for four-direction unit moves.
const int heuristic = std::abs(next_row - goal.first) +
std::abs(next_column - goal.second);
frontier.push(
State(candidate + heuristic, candidate, next_row, next_column));
}
}
}
return -1;
}
Choose A*, Dijkstra, or BFS¶
| Situation | Choice |
|---|---|
| Unit edges, no useful heuristic | BFS |
| Nonnegative edges, many targets/all distances | Dijkstra |
| One target, nonnegative edges, strong lower bound | A* |
| Negative edge | Bellman–Ford or another suitable model |
Worst-case work remains comparable to Dijkstra when the heuristic gives no guidance. An overestimating heuristic may be faster but can lose optimality.