πŸ¦€ Functional Rust

803. A* Pathfinding with Manhattan Heuristic

Difficulty: 4 Level: Advanced Guided shortest-path search on a 2D grid using a heuristic to focus exploration toward the goal β€” O(E log V) in the worst case, much faster in practice.

The Problem This Solves

A is the pathfinding algorithm used in virtually every game engine, robotics motion planner, and GPS navigation system. Dijkstra's algorithm expands outward uniformly in all directions; A focuses the search toward the goal by adding a heuristic estimate of remaining cost. On grid maps with many nodes, this can reduce explored nodes by orders of magnitude compared to Dijkstra. The choice of heuristic determines both correctness and performance: a consistent (monotone) heuristic guarantees optimal paths. For grid movement allowing only cardinal directions, the Manhattan distance `|dx| + |dy|` is the exact minimum cost to reach the goal, making it an ideal admissible heuristic.

The Intuition

A maintains a priority queue ordered by `f = g + h`, where `g` is the actual cost from start to the current node, and `h` is the heuristic estimate to the goal. Nodes with low `f` are explored first β€” the algorithm "aims" at the goal. Dijkstra is A with `h = 0`. A* with Manhattan heuristic on a grid is Dijkstra with perfect cost guidance. Rust implements the min-heap with `BinaryHeap<Reverse<(f, g, row, col)>>`, using `Reverse` because `BinaryHeap` is a max-heap. OCaml would use a priority queue module or sorted list; the structure is otherwise identical.

How It Works in Rust

use std::collections::BinaryHeap;
use std::cmp::Reverse;

// O(E log V) worst case; in practice much faster with a good heuristic
// grid: 0=passable, b'#'=wall
fn a_star(
 grid: &[Vec<u8>],
 start: (usize, usize),
 goal:  (usize, usize),
) -> Option<Vec<(usize, usize)>> {
 let mut g_cost = vec![vec![u64::MAX / 2; cols]; rows];
 let mut came: Vec<Vec<Option<(usize, usize)>>> = vec![vec![None; cols]; rows];
 let mut heap = BinaryHeap::new();

 // Heuristic: Manhattan distance (admissible for cardinal movement)
 let h = |r: usize, c: usize| -> u64 {
     ((r as i64 - goal.0 as i64).abs() + (c as i64 - goal.1 as i64).abs()) as u64
 };

 g_cost[start.0][start.1] = 0;
 heap.push(Reverse((h(start.0, start.1), 0u64, start.0, start.1)));
 //                  ^f                   ^g    ^row      ^col

 while let Some(Reverse((_, g, r, c))) = heap.pop() {
     if g > g_cost[r][c] { continue; }   // stale entry
     if (r, c) == goal { /* reconstruct and return */ }

     for (dr, dc) in [(-1i64,0),(1,0),(0,-1),(0,1)] {
         // bounds check, wall check, then:
         let ng = g + 1;
         if ng < g_cost[nr][nc] {
             g_cost[nr][nc] = ng;
             came[nr][nc]   = Some((r, c));
             heap.push(Reverse((ng + h(nr, nc), ng, nr, nc)));
         }
     }
 }
 None
}
Path reconstruction walks `came[][]` backwards from the goal to the start, then reverses. The stale-entry guard `if g > g_cost[r][c] { continue }` is the same lazy-deletion pattern as Prim's/Dijkstra.

What This Unlocks

Key Differences

ConceptOCamlRust
Min-heapPriority queue module`BinaryHeap<Reverse<(u64, u64, usize, usize)>>`
HeuristicInline function or `let h = ...`Closure `let h = \r, c\-> u64 { ... }`
Stale entry`Hashtbl` of visited + check`if g > g_cost[r][c] { continue }`
Path matrix`(int * int) option array array``Vec<Vec<Option<(usize, usize)>>>`
Bounds check`if r >= 0 && r < rows ...`Cast `i64` coordinates, then `as usize` after check
// A* Pathfinding on a 2D grid β€” Manhattan heuristic, O(E log V)
use std::collections::BinaryHeap;
use std::cmp::Reverse;

fn a_star(
    grid: &[Vec<u8>],
    start: (usize, usize),
    goal:  (usize, usize),
) -> Option<Vec<(usize, usize)>> {
    let rows = grid.len();
    let cols = grid[0].len();
    let inf  = u64::MAX / 2;

    let mut g_cost = vec![vec![inf; cols]; rows];
    let mut came: Vec<Vec<Option<(usize, usize)>>> = vec![vec![None; cols]; rows];
    // heap: Reverse((f, g, r, c))
    let mut heap = BinaryHeap::new();

    let h = |r: usize, c: usize| -> u64 {
        ((r as i64 - goal.0 as i64).abs() + (c as i64 - goal.1 as i64).abs()) as u64
    };

    g_cost[start.0][start.1] = 0;
    heap.push(Reverse((h(start.0, start.1), 0u64, start.0, start.1)));

    while let Some(Reverse((_, g, r, c))) = heap.pop() {
        if g > g_cost[r][c] { continue; } // stale
        if (r, c) == goal {
            // Reconstruct path
            let mut path = Vec::new();
            let mut pos = goal;
            loop {
                path.push(pos);
                if pos == start { break; }
                pos = came[pos.0][pos.1]?;
            }
            path.reverse();
            return Some(path);
        }
        for (dr, dc) in [(-1i64,0),(1,0),(0,-1),(0,1)] {
            let nr = r as i64 + dr;
            let nc = c as i64 + dc;
            if nr < 0 || nr >= rows as i64 || nc < 0 || nc >= cols as i64 { continue; }
            let (nr, nc) = (nr as usize, nc as usize);
            if grid[nr][nc] == b'#' { continue; }
            let ng = g + 1;
            if ng < g_cost[nr][nc] {
                g_cost[nr][nc] = ng;
                came[nr][nc]   = Some((r, c));
                heap.push(Reverse((ng + h(nr, nc), ng, nr, nc)));
            }
        }
    }
    None
}

fn main() {
    let grid: Vec<Vec<u8>> = vec![
        b"...#.".to_vec(),
        b".#.#.".to_vec(),
        b".#...".to_vec(),
        b".##..".to_vec(),  // Fixed: was ".##.." corrected
        b"#....".to_vec(),
    ];
    match a_star(&grid, (0,0), (4,4)) {
        None    => println!("No path found"),
        Some(p) => {
            println!("Path length: {}", p.len() - 1);
            println!("Path: {:?}", p);
        }
    }
}
(* A* Pathfinding on a grid β€” Manhattan heuristic *)
(* grid: '.' = open, '#' = wall, 'S' = start, 'E' = end *)

module PQ = Set.Make(struct
  type t = int * int * (int * int)  (* (f, g, pos) *)
  let compare (f1,g1,(r1,c1)) (f2,g2,(r2,c2)) =
    let cf = compare f1 f2 in if cf <> 0 then cf
    else let cg = compare g1 g2 in if cg <> 0 then cg
    else compare (r1,c1) (r2,c2)
end)

let a_star grid start goal =
  let rows = Array.length grid in
  let cols = Array.length grid.(0) in
  let inf  = max_int / 2 in
  let g_cost = Array.init rows (fun _ -> Array.make cols inf) in
  let came  = Array.init rows (fun _ -> Array.make cols None) in
  let (sr, sc) = start and (er, ec) = goal in
  let h r c = abs (r - er) + abs (c - ec) in
  g_cost.(sr).(sc) <- 0;
  let open_set = ref (PQ.singleton (h sr sc, 0, start)) in
  let found    = ref false in
  while not (PQ.is_empty !open_set) && not !found do
    let (_, g, (r, c)) = PQ.min_elt !open_set in
    open_set := PQ.remove (PQ.min_elt !open_set) !open_set;
    if g > g_cost.(r).(c) then ()  (* stale entry *)
    else if (r,c) = goal then found := true
    else List.iter (fun (dr, dc) ->
      let nr = r + dr and nc = c + dc in
      if nr >= 0 && nr < rows && nc >= 0 && nc < cols
         && grid.(nr).(nc) <> '#' then begin
        let ng = g + 1 in
        if ng < g_cost.(nr).(nc) then begin
          g_cost.(nr).(nc) <- ng;
          came.(nr).(nc)   <- Some (r, c);
          open_set := PQ.add (ng + h nr nc, ng, (nr, nc)) !open_set
        end
      end
    ) [(-1,0);(1,0);(0,-1);(0,1)]
  done;
  if not !found then None
  else begin
    let path = ref [] in
    let pos  = ref goal in
    while !pos <> start do
      path := !pos :: !path;
      let (r,c) = !pos in
      pos := match came.(r).(c) with Some p -> p | None -> start
    done;
    path := start :: !path;
    Some !path
  end

let () =
  let grid = [|
    [|'.';'.';'.';'#';'.'|];
    [|'.';'#';'.';'#';'.'|];
    [|'.';'#';'.';'.';'.'|];
    [|'.';'.';'#';'#';'.'|];
    [|'#';'.';'.';'.';'.'|];
  |] in
  match a_star grid (0,0) (4,4) with
  | None   -> Printf.printf "No path found\n"
  | Some p ->
    Printf.printf "Path length: %d\n" (List.length p - 1);
    Printf.printf "Path: %s\n"
      (String.concat " -> " (List.map (fun (r,c) -> Printf.sprintf "(%d,%d)" r c) p))