Implementing A* Pathfinding Algorithm
1. Applying A* Algorithm
| Component | Description | Implementation |
|---|---|---|
| Heuristic Function | Estimates cost from node to goal | h(n) = Manhattan distance |
| Cost Function | Actual cost from start to node | g(n) = sum of edge weights |
| Total Cost | Combined cost: f(n) = g(n) + h(n) | f(n) = g(n) + h(n) |
Example: Basic A* Implementation
const aStar = (grid, start, end) => {
const openSet = [start];
const cameFrom = new Map();
const gScore = new Map().set(start, 0);
const fScore = new Map().set(start, heuristic(start, end));
while (openSet.length > 0) {
const current = openSet.reduce((a, b) => fScore.get(a) < fScore.get(b) ? a : b);
if (current === end) return reconstructPath(cameFrom, current);
openSet.splice(openSet.indexOf(current), 1);
const neighbors = getNeighbors(current, grid);
for (const neighbor of neighbors) {
const tentativeGScore = gScore.get(current) + 1;
if (tentativeGScore < (gScore.get(neighbor) || Infinity)) {
cameFrom.set(neighbor, current);
gScore.set(neighbor, tentativeGScore);
fScore.set(neighbor, gScore.get(neighbor) + heuristic(neighbor, end));
if (!openSet.includes(neighbor)) openSet.push(neighbor);
}
}
}
return null;
};
Note: A* is optimal if the heuristic is admissible (never overestimates).
Warning: Performance degrades with high branching factors or large search spaces.
2. Implementing Manhattan Distance Heuristic
| Property | Description | Usage |
|---|---|---|
| manhattanDistance | Calculates the Manhattan distance between two points (x1, y1) and (x2, y2) | manhattanDistance(x1, y1, x2, y2) |
Example: Manhattan Distance Calculation
function manhattanDistance(x1, y1, x2, y2) {
return Math.abs(x1 - x2) + Math.abs(y1 - y2);
}
// Example usage
const distance = manhattanDistance(1, 2, 4, 6);
console.log(distance); // Output: 7
3. Implementing Euclidean Distance Heuristic
| Property | Description | Formula |
|---|---|---|
| heuristic | Estimates the cost from current node to goal node | h(n) = √[(x_goal - x_current)² + (y_goal - y_current)²] |
Example: Calculating Euclidean Distance
const calculateHeuristic = (node, goal) => {
const dx = Math.abs(node.x - goal.x);
const dy = Math.abs(node.y - goal.y);
return Math.sqrt(dx * dx + dy * dy);
};
// Example usage
const startNode = { x: 0, y: 0 };
const goalNode = { x: 3, y: 4 };
console.log(calculateHeuristic(startNode, goalNode)); // Output: 5
Note: The Euclidean distance heuristic is admissible and consistent, making it suitable for A* pathfinding.
4. Implementing Admissible Heuristics
Admissible heuristics are essential for the A* algorithm to guarantee optimal pathfinding. A heuristic is admissible if it never overestimates the cost to reach the goal.
| Heuristic | Description | Formula | Admissibility |
|---|---|---|---|
| Manhattan Distance | Sum of absolute differences in x and y coordinates | h = |x1 - x2| + |y1 - y2| |
ADMISSIBLE |
| Euclidean Distance | Direct straight-line distance between two points | h = √[(x1 - x2)² + (y1 - y2)²] |
ADMISSIBLE |
| Diagonal Distance | Combination of Manhattan and diagonal moves | h = (D1 * |x1 - x2|) + (D2 * |y1 - y2|) |
ADMISSIBLE |
Example: Manhattan Distance Implementation
function manhattanDistance(nodeA, nodeB) {
return Math.abs(nodeA.x - nodeB.x) + Math.abs(nodeA.y - nodeB.y);
}
Example: Euclidean Distance Implementation
function euclideanDistance(nodeA, nodeB) {
return Math.sqrt(
Math.pow(nodeA.x - nodeB.x, 2) + Math.pow(nodeA.y - nodeB.y, 2)
);
}
Note: The choice of heuristic affects A* performance. Euclidean is more accurate for free movement, while Manhattan is better for grid-based navigation.
5. Implementing Consistent Heuristics
A heuristic is consistent (or monotonic) if for every node n and every successor n' of n, the estimated cost from n to n' is less than or equal to the cost of moving from n to n' plus the estimated cost from n' to the goal.
This property ensures that A* will not need to re-expand nodes, making it more efficient.
| Property | Description | Formula |
|---|---|---|
| Consistency | For all nodes n and successors n' |
h(n) ≤ c(n, n') + h(n') |
Example: Manhattan Distance Heuristic
The Manhattan distance heuristic is consistent for grid-based pathfinding:
function manhattanDistance(nodeA, nodeB) {
return Math.abs(nodeA.x - nodeB.x) + Math.abs(nodeA.y - nodeB.y);
}
// Consistent check:
// h(n) ≤ c(n, n') + h(n')
// Manhattan distance satisfies this property
Note: Consistent heuristics are always admissible, but not all admissible heuristics are consistent.
6. Managing Open and Closed Sets
| Operation | Description | Implementation | Notes |
|---|---|---|---|
| Open Set Initialization | Initialize the open set with the start node | openSet = new PriorityQueue();<br>openSet.enqueue(startNode, startNode.f); |
Priority queue ensures lowest f(n) is processed first |
| Closed Set Management | Track visited nodes to avoid cycles | closedSet = new Set();<br>closedSet.add(currentNode); |
Used to skip already-processed nodes |
| Node Addition to Open Set | Add neighbors to open set if not in closed set | if (!closedSet.has(neighbor)) {<br> openSet.enqueue(neighbor, neighbor.f);<br>} |
Ensures no duplicate processing of nodes |
| Open Set Updates | Update node cost if better path found | if (neighbor.g < neighbor.gOld) {<br> neighbor.g = neighbor.gOld;<br> neighbor.f = neighbor.g + neighbor.h;<br> openSet.update(neighbor, neighbor.f);<br>} |
Re-evaluate and update node in priority queue |
Example: Open and Closed Set Operations
const openSet = new PriorityQueue();
const closedSet = new Set();
// Initialize
openSet.enqueue(startNode, startNode.f);
while (!openSet.isEmpty()) {
const currentNode = openSet.dequeue();
closedSet.add(currentNode);
if (currentNode === goalNode) {
// Path found
return reconstructPath(currentNode);
}
for (const neighbor of getNeighbors(currentNode)) {
if (!closedSet.has(neighbor)) {
const tentativeG = currentNode.g + distance(currentNode, neighbor);
if (tentativeG < neighbor.g) {
neighbor.parent = currentNode;
neighbor.g = tentativeG;
neighbor.f = neighbor.g + neighbor.h;
if (!openSet.contains(neighbor)) {
openSet.enqueue(neighbor, neighbor.f);
} else {
openSet.update(neighbor, neighbor.f);
}
}
}
}
}
7. Implementing Priority Queue
| Operation | Description | Time Complexity | Implementation |
|---|---|---|---|
| insert(item, priority) | Adds an item with a given priority to the queue | O(log n) | |
| extractMin() | Removes and returns the item with the lowest priority | O(log n) | |
| isEmpty() | Checks if the queue is empty | O(1) | |
Example: Using Priority Queue in A* Pathfinding
const pq = new PriorityQueue();
pq.insert({ node: 'A', fScore: 5 }, 5);
const next = pq.extractMin(); // Returns { node: 'A', fScore: 5 }
8. Implementing Path Reconstruction
| Step | Description | Implementation |
|---|---|---|
| 1. Initialize | Start with the goal node and an empty path list. | path = []<br>current = goal_node |
| 2. Backtrack | Traverse the cameFrom map to reconstruct the path. | while current in cameFrom:<br> path.append(current)<br> current = cameFrom[current] |
| 3. Reverse Path | Reverse the path to get it from start to goal. | path.reverse() |
Example: Reconstructing Path in A* Algorithm
const reconstructPath = (cameFrom, current) => {
const path = [current];
while (cameFrom.has(current)) {
current = cameFrom.get(current);
path.push(current);
}
return path.reverse();
};
Note: The path reconstruction assumes that
cameFrom is a Map or object storing the parent node for each visited node.
9. Implementing Bidirectional A*
| Property | Description | Notes |
|---|---|---|
| startNode | Starting node for the forward search | Must be a valid node in the graph |
| goalNode | Target node for the backward search | Must be a valid node in the graph |
| forwardOpenSet | Priority queue for forward search | Sorted by f(n) = g(n) + h(n) |
| backwardOpenSet | Priority queue for backward search | Sorted by f(n) = g(n) + h(n) |
| forwardClosedSet | Set of visited nodes in forward search | Used to avoid revisiting nodes |
| backwardClosedSet | Set of visited nodes in backward search | Used to avoid revisiting nodes |
| intersectionNode | Node where forward and backward paths meet | Used to reconstruct the full path |
Example: Bidirectional A* Implementation
function bidirectionalAStar(graph, startNode, goalNode) {
const forwardOpenSet = new PriorityQueue();
const backwardOpenSet = new PriorityQueue();
const forwardClosedSet = new Set();
const backwardClosedSet = new Set();
// Initialize forward search
forwardOpenSet.enqueue(startNode, heuristic(startNode, goalNode));
const forwardCameFrom = new Map();
const forwardGScore = new Map();
forwardGScore.set(startNode, 0);
// Initialize backward search
backwardOpenSet.enqueue(goalNode, heuristic(goalNode, startNode));
const backwardCameFrom = new Map();
const backwardGScore = new Map();
backwardGScore.set(goalNode, 0);
while (!forwardOpenSet.isEmpty() && !backwardOpenSet.isEmpty()) {
const currentForward = forwardOpenSet.dequeue();
const currentBackward = backwardOpenSet.dequeue();
if (forwardClosedSet.has(currentForward) || backwardClosedSet.has(currentBackward)) {
continue;
}
forwardClosedSet.add(currentForward);
backwardClosedSet.add(currentBackward);
// Check for intersection
if (forwardClosedSet.has(currentBackward) || backwardClosedSet.has(currentForward)) {
const intersectionNode = findIntersection(forwardClosedSet, backwardClosedSet);
return reconstructPath(intersectionNode, forwardCameFrom, backwardCameFrom);
}
// Expand forward search
for (const neighbor of graph.getNeighbors(currentForward)) {
if (!forwardClosedSet.has(neighbor)) {
const tentativeGScore = forwardGScore.get(currentForward) + graph.getDistance(currentForward, neighbor);
if (tentativeGScore < (forwardGScore.get(neighbor) || Infinity)) {
forwardCameFrom.set(neighbor, currentForward);
forwardGScore.set(neighbor, tentativeGScore);
const fScore = tentativeGScore + heuristic(neighbor, goalNode);
forwardOpenSet.enqueue(neighbor, fScore);
}
}
}
// Expand backward search
for (const neighbor of graph.getNeighbors(currentBackward)) {
if (!backwardClosedSet.has(neighbor)) {
const tentativeGScore = backwardGScore.get(currentBackward) + graph.getDistance(currentBackward, neighbor);
if (tentativeGScore < (backwardGScore.get(neighbor) || Infinity)) {
backwardCameFrom.set(neighbor, currentBackward);
backwardGScore.set(neighbor, tentativeGScore);
const fScore = tentativeGScore + heuristic(neighbor, startNode);
backwardOpenSet.enqueue(neighbor, fScore);
}
}
}
}
return null; // No path found
}
Note: Bidirectional A* improves performance by searching from both start and goal simultaneously, potentially reducing the search space significantly.
10. Comparing A* vs Dijkstra
| Feature | A* | Dijkstra |
|---|---|---|
| Pathfinding Type | Heuristic-based shortest path |
Uninformed shortest path |
| Heuristic Function | Yes (e.g., Manhattan, Euclidean) |
No |
| Time Complexity | O(b^d) with good heuristic |
O((V + E) log V) |
| Space Complexity | O(b^d) |
O(V) |
| Optimality | Optimal if heuristic is admissible |
Always optimal |
| Use Case | Large search spaces with known goal |
When full exploration is needed |
When to Use Which
- A*: Ideal for pathfinding in grids or graphs where a heuristic can guide the search efficiently.
- Dijkstra: Best when you need to find shortest paths from one node to all others, or when no heuristic is available.
11. Applying to Grid Pathfinding
| Component | Description | Implementation |
|---|---|---|
| Grid Representation | 2D array where each cell is either walkable (0) or blocked (1) | let grid = [[0, 0, 1], [0, 1, 0], [0, 0, 0]]; |
| Heuristic Function | Manhattan distance or Euclidean distance between nodes | h = |x1 - x2| + |y1 - y2| |
| Node Structure | Contains f, g, h, and parent node |
|
Example: Basic Grid Pathfinding
function aStar(grid, start, end) {
const openSet = [new Node(start.x, start.y, 0, heuristic(start, end))];
const closedSet = [];
while (openSet.length > 0) {
openSet.sort((a, b) => a.f - b.f);
const current = openSet.shift();
if (current.x === end.x && current.y === end.y) {
return reconstructPath(current);
}
closedSet.push(current);
const neighbors = getNeighbors(current, grid);
for (const neighbor of neighbors) {
if (closedSet.includes(neighbor)) continue;
const tentativeG = current.g + 1;
if (tentativeG < neighbor.g || !openSet.includes(neighbor)) {
neighbor.parent = current;
neighbor.g = tentativeG;
neighbor.h = heuristic(neighbor, end);
neighbor.f = neighbor.g + neighbor.h;
if (!openSet.includes(neighbor)) {
openSet.push(neighbor);
}
}
}
}
return null;
}
12. Optimizing with Jump Point Search
| Feature | Description | Implementation |
|---|---|---|
| Jump Point Search (JPS) | Optimization for A* that skips unnecessary nodes in uniform-cost grids | Uses directional heuristics to prune search space |
| Pruning Strategy | Only expands nodes that are likely to lead to the goal | Eliminates nodes in straight lines without obstacles |
| Performance Gain | Reduces search space by up to 90% in grid-based environments | Improves A* runtime from O(b^d) to O(b^(d/2)) |
Example: Basic JPS Node Expansion
function jump(node, direction) {
const next = node.move(direction);
if (!isValid(next)) return null;
if (isGoal(next)) return next;
// Check for jump points in the direction
if (hasObstacle(next, direction)) {
return next;
}
// Continue in same direction
return jump(next, direction);
}
Note: JPS is most effective in uniform-cost grids with clear directional patterns.