What is A* Search?
A* (A-Star) is an intelligent graph traversal and pathfinding algorithm that finds the shortest path between a starting node and a goal node. By combining the actual distance from the start ($g(n)$) with an estimated heuristic distance to the goal ($h(n)$), A* selectively explores paths, achieving optimal efficiency compared to unguided algorithms like Dijkstra.
Explanation
- A Search Algorithm* is a directed heuristic search algorithm. It is widely used in maps, game development, and routing applications where we need to find the shortest path across a weighted graph or a grid populated with obstacles.
Real-World Analogy
- Finding your way to a skyscraper in a city. Instead of exploring every street in all directions (like BFS or Dijkstra), you look up at the skyscraper and always prioritize walking down streets that lead in the general direction of the tower. Even if you have to detour around a block (obstacle), you use visual feedback (heuristics) to stay oriented.
The Mathematical Foundation
- For any node $n$, A* calculates an evaluation function $f(n)$:
- $$\Large f(n) = g(n) + h(n)$$
| Variable | Name | Description |
|---|---|---|
| $g(n)$ | Exact Cost | The actual cost/distance traversed from the starting node to reach node $n$. |
| $h(n)$ | Heuristic Cost | The estimated cost/distance from node $n$ to the final goal. |
| $f(n)$ | Total Cost | The total estimated cost of the path passing through node $n$. |
- A* always expands the node with the lowest $f(n)$ value.
Heuristic Rules: Admissibility & Consistency
- To guarantee that A* finds the absolute shortest path, its heuristic function $h(n)$ must satisfy two critical properties:
-
- Admissibility: The heuristic must never overestimate the actual cost to reach the goal. That is, $h(n) \le h^(n)$ for all $n$, where $h^(n)$ is the true shortest path cost from $n$ to the goal. If a heuristic is admissible, A* is guaranteed to find the shortest path.
-
- Consistency (Monotonicity): For every node $n$ and each successor $n’$ generated by action $a$, the estimated cost to the goal from $n$ must not be greater than the step cost to $n’$ plus the estimated cost to the goal from $n’$: $$h(n) \le c(n, a, n’) + h(n’)$$ If a heuristic is consistent, A* will find the optimal path without needing to reopen nodes in the closed set, maximizing efficiency.
-
Common Distance Heuristics
| Distance Metric | Formula | Movement Grid Allowed |
|---|---|---|
| Manhattan Distance | $D \cdot ( | x_1 - x_2 |
| Diagonal / Octile | $D \cdot ( | dx |
| Euclidean Distance | $D \cdot \sqrt{(x_1 - x_2)^2 + (y_1 - y_2)^2}$ | Any-Angle Movement (Straight line distance) |
How It Works
The Core Idea
- Keep two sets of nodes:
- Open Set: Nodes discovered but not yet evaluated (implemented as a Min-Priority Queue / Min-Heap sorted by $f$ score).
- Closed Set: Nodes already evaluated.
- Pop the node with the lowest $f$ from the Open Set. If it’s the goal, reconstruct the path. Otherwise, move it to the Closed Set, evaluate its neighbors, and add/update them in the Open Set.
flowchart TD A["Start — Add source node to Open Set\nSet g(start)=0, f(start)=h(start)"] --> B{"Open Set empty?"} B -- Yes --> H["❌ End — Path Not Found"] B -- No --> C["Pop node U with lowest f(U) from Open Set"] C --> D{"Is U the Goal?"} D -- Yes --> F["✅ Found — Reconstruct path via parent pointers"] D -- No --> G["Move U to Closed Set"] G --> I["For each neighbor V of U"] I --> J{"V in Closed Set or Obstacle?"} J -- Yes --> I J -- No --> K{"New path to V shorter\nthan current g(V)?"} K -- No --> I K -- Yes --> L["parent(V) = U\ng(V) = g(U) + weight(U, V)\nf(V) = g(V) + h(V)\nAdd V to Open Set (if not already there)"] L --> I I -- "No more neighbors" --> B
Step-by-Step Algorithm
INPUT: Grid/Graph, Start node S, Goal node G
OUTPUT: Shortest path from S to G
1. Initialize OPEN as a Min-Heap, containing node S with f(S) = h(S)
2. Initialize CLOSED as an empty set
3. Set g(S) = 0, and g(n) = infinity for all other nodes
4. WHILE OPEN is not empty:
a. Pop U from OPEN (node with lowest f(U))
b. Add U to CLOSED
c. IF U == G:
RETURN reconstructed path from G to S using parent pointers
d. For each neighbor V of U:
i. IF V in CLOSED or V is an obstacle:
CONTINUE
ii. temp_g = g(U) + distance(U, V)
iii. IF temp_g < g(V):
g(V) = temp_g
f(V) = g(V) + h(V)
parent(V) = U
IF V is not in OPEN:
Push V to OPEN
Live Walkthrough — Pathfinding on a 2x3 Grid
- Find path from Start
(0, 0)to Goal(1, 2)on a grid with an obstacle at(0, 1).
Grid:
[ S, #, . ] S = Start (0, 0) | # = Obstacle (0, 1)
[ ., ., G ] G = Goal (1, 2) | . = Empty cell
Heuristics h(r, c) = Manhattan distance to Goal (1, 2)
h(0, 0) = |0-1| + |0-2| = 3
h(1, 0) = |1-1| + |0-2| = 2
h(1, 1) = |1-1| + |1-2| = 1
h(1, 2) = 0
┌─────────────────────────────────────────────────────────────────────────────────┐
│ Step │ Node Popped │ Open Set Contents (r, c, f, g) │ Closed Set │ Action │
├─────────────────────────────────────────────────────────────────────────────────┤
│ 1 │ — │ {(0,0, f=3, g=0)} │ {} │ Start │
│ 2 │ (0,0) │ {(1,0, f=3, g=1)} │ {(0,0)} │ Expands neighbors:│
│ │ │ │ │ (1,0) [g=1, f=3] │
│ │ │ │ │ (0,1) obstacle! │
│ 3 │ (1,0) │ {(1,1, f=3, g=2)} │ {(0,0), │ Expands neighbors:│
│ │ │ │ (1,0)} │ (1,1) [g=2, f=3] │
│ 4 │ (1,1) │ {(1,2, f=3, g=3), (0,2, f=5,g=3)}│{(0,0), │ Expands neighbors:│
│ │ │ │ (1,0),(1,1)│ (1,2) [g=3, f=3] │
│ │ │ │ │ (0,2) [g=3, f=5] │
│ 5 │ (1,2) │ — │ — │ Goal reached! │
└─────────────────────────────────────────────────────────────────────────────────┘
Optimal path: (0,0) -> (1,0) -> (1,1) -> (1,2) (Length = 3 steps)
Time & Space Complexity
-
Complexity Summary
- Time Complexity: O(E log V) — depends on the quality of the heuristic. In the worst case (uninformative heuristic), it searches the entire graph like Dijkstra, taking $O(E \log V)$ with a binary heap.
- Space Complexity: O(V) — open and closed sets store up to $O(V)$ nodes in memory.
Complexity Table
| Algorithm / Case | Time Complexity | Space Complexity | Guarantee |
|---|---|---|---|
| A Search (Worst Heuristic)* | O(E log V) | O(V) | Optimal path found (looks like Dijkstra) |
| A Search (Best Heuristic)* | O(d) where d is depth | O(d) | Linear optimal path finding |
| Dijkstras Algorithm | O(E log V) | O(V) | Optimal path on weighted graphs |
| Greedy Best-First | O(bᵐ) | O(bᵐ) | Sub-optimal path (fast but inaccurate) |
Scaling Comparison vs. Dijkstra & Greedy BFS
xychart-beta title "Visited Nodes Comparison (Lower is Better)" x-axis ["10x10 Grid", "50x50 Grid", "100x100 Grid", "500x500 Grid"] y-axis "Visited Cells" 0 --> 260000 bar [100, 2500, 10000, 250000] line [25, 300, 800, 4500]
| Grid Size | Total Cells | Nodes Visited (Dijkstra) | Nodes Visited (A* Manhattan) |
|---|---|---|---|
| 10x10 | 100 | 100 | 25 |
| 50x50 | 2,500 | 2,500 | 300 |
| 100x100 | 10,000 | 10,000 | 800 |
| 500x500 | 250,000 | 250,000 | 4,500 |
-
Guided Efficiency
A* uses heuristics to avoid exploring nodes in directions away from the target, resulting in massive performance gains over Dijkstra on large grids.
Implementation
-
Implementation on a 2D grid with cardinal (4-way) movements.
0is traversable,1is an obstacle. Start and end are coordinate pairs. Languages: Python · Cpp · Java Script · Java · CGrid layout:
import heapq
def a_star_grid(grid, start, end):
"""
A* Search on a 2D grid.
Time: O(E log V) | Space: O(V)
Returns the path as list of coordinates, or empty if not found.
"""
rows, cols = len(grid), len(grid[0])
def heuristic(r, c):
# Manhattan distance
return abs(r - end[0]) + abs(c - end[1])
# Heap storing: (f_score, g_score, (row, col))
open_set = []
heapq.heappush(open_set, (heuristic(start[0], start[1]), 0, start))
parent = {}
g_score = {start: 0}
closed_set = set()
while open_set:
f, g, curr = heapq.heappop(open_set)
if curr == end:
# Reconstruct path
path = []
while curr in parent:
path.append(curr)
curr = parent[curr]
path.append(start)
return path[::-1]
if curr in closed_set:
continue
closed_set.add(curr)
r, c = curr
for dr, dc in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
nr, nc = r + dr, c + dc
neighbor = (nr, nc)
if 0 <= nr < rows and 0 <= nc < cols and grid[nr][nc] == 0:
if neighbor in closed_set:
continue
temp_g = g + 1
if temp_g < g_score.get(neighbor, float('inf')):
g_score[neighbor] = temp_g
f_score = temp_g + heuristic(nr, nc)
parent[neighbor] = curr
heapq.heappush(open_set, (f_score, temp_g, neighbor))
return [] # Path not found
# Example Setup
grid = [
[0, 1, 0, 0],
[0, 1, 0, 0],
[0, 0, 0, 1],
[0, 1, 0, 0]
]
print("Path found:", a_star_grid(grid, (0, 0), (3, 2)))
# Output: [(0, 0), (1, 0), (2, 0), (2, 1), (2, 2), (3, 2)]#include <iostream>
#include <vector>
#include <queue>
#include <cmath>
#include <unordered_map>
#include <set>
#include <algorithm>
struct Node {
int r, c;
int f, g;
bool operator>(const Node& other) const {
return f > other.f;
}
};
// Hash function for pair coordinates
struct PairHash {
std::size_t operator()(const std::pair<int, int>& p) const {
return (p.first * 31) ^ p.second;
}
};
std::vector<std::pair<int, int>> aStarGrid(const std::vector<std::vector<int>>& grid,
std::pair<int, int> start, std::pair<int, int> end) {
int rows = grid.size();
int cols = grid[0].size();
auto heuristic = [&](int r, int c) {
return std::abs(r - end.first) + std::abs(c - end.second);
};
std::priority_queue<Node, std::vector<Node>, std::greater<Node>> openSet;
openSet.push({start.first, start.second, heuristic(start.first, start.second), 0});
std::unordered_map<std::pair<int, int>, std::pair<int, int>, PairHash> parent;
std::unordered_map<std::pair<int, int>, int, PairHash> gScore;
gScore[start] = 0;
std::set<std::pair<int, int>> closedSet;
std::vector<std::pair<int, int>> directions = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
while (!openSet.empty()) {
Node curr = openSet.top();
openSet.pop();
std::pair<int, int> currPair = {curr.r, curr.c};
if (currPair == end) {
std::vector<std::pair<int, int>> path;
while (parent.find(currPair) != parent.end()) {
path.push_back(currPair);
currPair = parent[currPair];
}
path.push_back(start);
std::reverse(path.begin(), path.end());
return path;
}
if (closedSet.find(currPair) != closedSet.end()) continue;
closedSet.insert(currPair);
for (auto& dir : directions) {
int nr = curr.r + dir.first;
int nc = curr.c + dir.second;
std::pair<int, int> neighbor = {nr, nc};
if (nr >= 0 && nr < rows && nc >= 0 && nc < cols && grid[nr][nc] == 0) {
if (closedSet.find(neighbor) != closedSet.end()) continue;
int tempG = curr.g + 1;
if (gScore.find(neighbor) == gScore.end() || tempG < gScore[neighbor]) {
gScore[neighbor] = tempG;
int fScore = tempG + heuristic(nr, nc);
parent[neighbor] = currPair;
openSet.push({nr, nc, fScore, tempG});
}
}
}
}
return {};
}
int main() {
std::vector<std::vector<int>> grid = {
{0, 1, 0, 0},
{0, 1, 0, 0},
{0, 0, 0, 1},
{0, 1, 0, 0}
};
std::vector<std::pair<int, int>> path = aStarGrid(grid, {0, 0}, {3, 2});
std::cout << "Path: ";
for (auto& cell : path) {
std::cout << "(" << cell.first << "," << cell.second << ") ";
}
std::cout << "\n";
return 0;
}// Complete A* Search with Custom Binary Min-Heap
class MinHeap {
constructor() {
this.data = [];
}
push(item) {
this.data.push(item);
this.up(this.data.length - 1);
}
pop() {
if (this.data.length === 0) return null;
const top = this.data[0];
const bottom = this.data.pop();
if (this.data.length > 0) {
this.data[0] = bottom;
this.down(0);
}
return top;
}
size() {
return this.data.length;
}
up(i) {
while (i > 0) {
const p = Math.floor((i - 1) / 2);
if (this.data[p].f <= this.data[i].f) break;
const tmp = this.data[p];
this.data[p] = this.data[i];
this.data[i] = tmp;
i = p;
}
}
down(i) {
const len = this.data.length;
while (i * 2 + 1 < len) {
let child = i * 2 + 1;
if (child + 1 < len && this.data[child + 1].f < this.data[child].f) {
child++;
}
if (this.data[i].f <= this.data[child].f) break;
const tmp = this.data[child];
this.data[child] = this.data[i];
this.data[i] = tmp;
i = child;
}
}
}
function aStarGrid(grid, start, end) {
const rows = grid.length;
const cols = grid[0].length;
function heuristic(r, c) {
return Math.abs(r - end[0]) + Math.abs(c - end[1]);
}
const heap = new MinHeap();
heap.push({ r: start[0], c: start[1], f: heuristic(start[0], start[1]), g: 0 });
const parent = {};
const gScore = {};
gScore[`${start[0]},${start[1]}`] = 0;
const closedSet = new Set();
while (heap.size() > 0) {
const curr = heap.pop();
const key = `${curr.r},${curr.c}`;
if (curr.r === end[0] && curr.c === end[1]) {
const path = [];
let currKey = key;
while (parent[currKey]) {
const [r, c] = currKey.split(',').map(Number);
path.push([r, c]);
currKey = parent[currKey];
}
path.push(start);
return path.reverse();
}
if (closedSet.has(key)) continue;
closedSet.add(key);
const directions = [[-1, 0], [1, 0], [0, -1], [0, 1]];
for (const [dr, dc] of directions) {
const nr = curr.r + dr;
const nc = curr.c + dc;
const nKey = `${nr},${nc}`;
if (nr >= 0 && nr < rows && nc >= 0 && nc < cols && grid[nr][nc] === 0) {
if (closedSet.has(nKey)) continue;
const tempG = curr.g + 1;
if (gScore[nKey] === undefined || tempG < gScore[nKey]) {
gScore[nKey] = tempG;
const fScore = tempG + heuristic(nr, nc);
parent[nKey] = key;
heap.push({ r: nr, c: nc, f: fScore, g: tempG });
}
}
}
}
return [];
}
const grid = [
[0, 1, 0, 0],
[0, 1, 0, 0],
[0, 0, 0, 1],
[0, 1, 0, 0]
];
console.log("Path:", aStarGrid(grid, [0, 0], [3, 2]));
// Output: [ [ 0, 0 ], [ 1, 0 ], [ 2, 0 ], [ 2, 1 ], [ 2, 2 ], [ 3, 2 ] ]import java.util.*;
public class AStar {
static class Node implements Comparable<Node> {
int r, c, f, g;
Node(int r, int c, int f, int g) {
this.r = r;
this.c = c;
this.f = f;
this.g = g;
}
public int compareTo(Node other) {
return Integer.compare(this.f, other.f);
}
}
public static List<int[]> aStarGrid(int[][] grid, int[] start, int[] end) {
int rows = grid.length;
int cols = grid[0].length;
PriorityQueue<Node> openSet = new PriorityQueue<>();
openSet.add(new Node(start[0], start[1], Math.abs(start[0] - end[0]) + Math.abs(start[1] - end[1]), 0));
Map<String, String> parent = new HashMap<>();
Map<String, Integer> gScore = new HashMap<>();
gScore.put(start[0] + "," + start[1], 0);
Set<String> closedSet = new HashSet<>();
int[][] directions = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
while (!openSet.isEmpty()) {
Node curr = openSet.poll();
String currKey = curr.r + "," + curr.c;
if (curr.r == end[0] && curr.c == end[1]) {
List<int[]> path = new ArrayList<>();
String tmp = currKey;
while (parent.containsKey(tmp)) {
String[] split = tmp.split(",");
path.add(new int[]{Integer.parseInt(split[0]), Integer.parseInt(split[1])});
tmp = parent.get(tmp);
}
path.add(start);
Collections.reverse(path);
return path;
}
if (closedSet.contains(currKey)) continue;
closedSet.add(currKey);
for (int[] d : directions) {
int nr = curr.r + d[0];
int nc = curr.c + d[1];
String nKey = nr + "," + nc;
if (nr >= 0 && nr < rows && nc >= 0 && nc < cols && grid[nr][nc] == 0) {
if (closedSet.contains(nKey)) continue;
int tempG = curr.g + 1;
if (!gScore.containsKey(nKey) || tempG < gScore.get(nKey)) {
gScore.put(nKey, tempG);
int fScore = tempG + Math.abs(nr - end[0]) + Math.abs(nc - end[1]);
parent.put(nKey, currKey);
openSet.add(new Node(nr, nc, fScore, tempG));
}
}
}
}
return new ArrayList<>();
}
public static void main(String[] args) {
int[][] grid = {
{0, 1, 0, 0},
{0, 1, 0, 0},
{0, 0, 0, 1},
{0, 1, 0, 0}
};
List<int[]> path = aStarGrid(grid, new int[]{0, 0}, new int[]{3, 2});
System.out.print("Path: ");
for (int[] cell : path) {
System.out.print("(" + cell[0] + "," + cell[1] + ") ");
}
System.out.println();
}
}#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <math.h>
#define ROWS 4
#define COLS 4
struct Node {
int r, c;
int f, g;
};
// Simple C Min-Heap implementation
struct MinHeap {
struct Node* data;
int capacity;
int size;
};
struct MinHeap* createMinHeap(int capacity) {
struct MinHeap* heap = (struct MinHeap*)malloc(sizeof(struct MinHeap));
heap->capacity = capacity;
heap->size = 0;
heap->data = (struct Node*)malloc(capacity * sizeof(struct Node));
return heap;
}
void swap(struct Node* a, struct Node* b) {
struct Node temp = *a;
*a = *b;
*b = temp;
}
void push(struct MinHeap* heap, struct Node node) {
heap->data[heap->size] = node;
int i = heap->size++;
while (i > 0) {
int p = (i - 1) / 2;
if (heap->data[p].f <= heap->data[i].f) break;
swap(&heap->data[p], &heap->data[i]);
i = p;
}
}
struct Node pop(struct MinHeap* heap) {
struct Node top = heap->data[0];
heap->data[0] = heap->data[--heap->size];
int i = 0;
while (i * 2 + 1 < heap->size) {
int child = i * 2 + 1;
if (child + 1 < heap->size && heap->data[child + 1].f < heap->data[child].f) {
child++;
}
if (heap->data[i].f <= heap->data[child].f) break;
swap(&heap->data[i], &heap->data[child]);
i = child;
}
return top;
}
int heuristic(int r, int c, int tr, int tc) {
return abs(r - tr) + abs(c - tc);
}
void aStar(int grid[ROWS][COLS], int sr, int sc, int tr, int tc) {
struct MinHeap* heap = createMinHeap(100);
struct Node start = {sr, sc, heuristic(sr, sc, tr, tc), 0};
push(heap, start);
int gScore[ROWS][COLS];
int parentR[ROWS][COLS];
int parentC[ROWS][COLS];
bool closedSet[ROWS][COLS];
for (int i = 0; i < ROWS; i++) {
for (int j = 0; j < COLS; j++) {
gScore[i][j] = 1e9;
closedSet[i][j] = false;
parentR[i][j] = -1;
parentC[i][j] = -1;
}
}
gScore[sr][sc] = 0;
int dRow[] = {-1, 1, 0, 0};
int dCol[] = 0, dColMap[] = {0, 0, -1, 1}; // 4 directions
bool found = false;
while (heap->size > 0) {
struct Node curr = pop(heap);
if (curr.r == tr && curr.c == tc) {
found = true;
break;
}
if (closedSet[curr.r][curr.c]) continue;
closedSet[curr.r][curr.c] = true;
for (int i = 0; i < 4; i++) {
int nr = curr.r + dRow[i];
int nc = curr.c + dColMap[i];
if (nr >= 0 && nr < ROWS && nc >= 0 && nc < COLS && grid[nr][nc] == 0) {
if (closedSet[nr][nc]) continue;
int tempG = curr.g + 1;
if (tempG < gScore[nr][nc]) {
gScore[nr][nc] = tempG;
int fScore = tempG + heuristic(nr, nc, tr, tc);
parentR[nr][nc] = curr.r;
parentC[nr][nc] = curr.c;
struct Node neighbor = {nr, nc, fScore, tempG};
push(heap, neighbor);
}
}
}
}
if (found) {
printf("Path found: ");
int cr = tr, cc = tc;
int pathR[100], pathC[100], pathLen = 0;
while (cr != -1 && cc != -1) {
pathR[pathLen] = cr;
pathC[pathLen] = cc;
pathLen++;
int tempR = parentR[cr][cc];
int tempC = parentC[cr][cc];
cr = tempR;
cc = tempC;
}
for (int i = pathLen - 1; i >= 0; i--) {
printf("(%d,%d) ", pathR[i], pathC[i]);
}
printf("\n");
} else {
printf("Path not found\n");
}
free(heap->data);
free(heap);
}
int main() {
int grid[ROWS][COLS] = {
{0, 1, 0, 0},
{0, 1, 0, 0},
{0, 0, 0, 1},
{0, 1, 0, 0}
};
aStar(grid, 0, 0, 3, 2);
return 0;
}
When to Use A* Search
flowchart TD Q{"Graph features?"} Q -- "Unweighted graph / Minimum edges" --> R1["❌ Use BFS\nBFS is faster and does not require sorting"] Q -- "Weighted graph & No coordinates/heuristics" --> R2["❌ Use Dijkstra\nWithout heuristic, A* behaves like Dijkstra"] Q -- "Spatial graph/grid with distance heuristics" --> R3["✅ Use A*\nMost efficient guided shortest path finder"]
✅ Use A* Search When
- You are designing pathfinding in games (RTS, RPG grids, navigating NPCs around dynamic obstacles).
- You are routing on physical coordinate networks (GPS navigation, maps, robotics).
- You have an admissible, consistent heuristic function that is fast to compute.
- Memory is sufficient to maintain the priority queue frontier.
❌ Avoid A* Search When
- You do not have a reliable heuristic function (defaults to Dijkstra).
- All edge costs are equal/unweighted (use Breadth First Search instead for reduced overhead).
- Graph is extremely massive and memory is constrained (A* stores all path alternatives in memory, which can crash systems; look into IDA* instead).
Variations & Common Optimization Tricks
Relationship to Other Algorithms
- A* acts as a mathematical bridge between blind searches and greedy heuristic searches.
- If we set the heuristic estimate $h(n) = 0$, A* becomes equivalent to Dijkstra’s Algorithm.
- If we set the actual cost $g(n) = 0$, A* becomes equivalent to Greedy Best-First Search (highly prioritized heuristic exploration, faster but loses shortest path guarantee).
Optimal Tie-Breaking
- If multiple nodes have the same $f(n)$ cost, the priority queue pop order can affect performance. A common optimization is to break ties by prioritizing nodes with higher $g(n)$ scores (or equivalently, lower $h(n)$ scores). This pushes the search forward toward the goal rather than exploring other routes of equal cost.
Iterative Deepening A* (IDA*)
- IDA* is a memory-saving variant that combines depth-first search backtracking with heuristic evaluation, replacing the priority queue with a capped threshold limits.
Key Takeaways
- Evaluation function — $f(n) = g(n) + h(n)$ guides exploration toward the target.
- Optimal & Complete — guarantees finding the shortest path if $h(n)$ is admissible (never overestimates) and consistent.
- Min-Heap driven — open set selection is powered by priority queues for $O(\log V)$ node retrieval.
- Dijkstra as base — Dijkstra is a special case of A* where the heuristic function $h(n) = 0$.
- Memory bound — A* stores both open and closed sets in memory, which scales to $O(V)$ size.