Skip to main content

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)
function insert(item, priority) {
  this.heap.push({ item, priority });
  this.bubbleUp(this.heap.length - 1);
}
extractMin() Removes and returns the item with the lowest priority O(log n)
function extractMin() {
  if (this.heap.length === 0) return null;
  const min = this.heap[0];
  const last = this.heap.pop();
  if (this.heap.length > 0) {
    this.heap[0] = last;
    this.bubbleDown(0);
  }
  return min.item;
}
isEmpty() Checks if the queue is empty O(1)
function isEmpty() {
  return this.heap.length === 0;
}

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
class Node {
  constructor(x, y, g, h) {
    this.x = x;
    this.y = y;
    this.g = g;
    this.h = h;
    this.f = g + h;
    this.parent = null;
  }
}

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;
}
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.

Key Benefits of JPS

  • Significantly reduces number of nodes evaluated
  • Preserves A* optimality guarantees
  • Works best in open grids with few obstacles

Was this article helpful?