A* Search — When Heuristic Multipliers Break Optimality
A 1.3x heuristic safety factor caused 23% longer drone routes with zero errors.
20+ years shipping performance-critical code where algorithms decide the bill. Written from production experience, not tutorials.
- A* finds shortest paths by combining actual cost g(n) with heuristic estimate h(n) via f(n) = g(n) + h(n)
- Admissible heuristics (never overestimate) guarantee optimal paths — violate admissibility and you get wrong answers with no error signal
- A* degenerates to Dijkstra when h=0, and to greedy best-first search when g is ignored
- Priority queue keyed by f, lazy deletion for stale entries, closed set for visited nodes
- Checking goal at insertion time instead of pop time silently returns suboptimal paths on weighted graphs — this is the most common production bug
- Worst-case O((V+E) log V) but heuristic quality determines practical speedup over Dijkstra — a perfect heuristic expands only nodes on the optimal path
- Biggest mistake: multiplying a lower-bound heuristic by any factor > 1.0 destroys admissibility and guarantees suboptimal results with no warning
Imagine you are navigating a city to reach a friend's house. You could wander every street blindly — that is Breadth-First Search. Or you could always take the road that feels closest to the destination without caring how far you have already walked — that is Greedy Best-First Search, and it will send you down dead ends. A* does something smarter: it keeps a running score that combines 'how far I have already walked from the start' with 'my best estimate of how far I still need to go', and always takes the step with the lowest combined score. It never forgets where it came from, and it never chases shortcuts that ignore the path already traveled. That is why navigation apps find your route in milliseconds — they are not checking every road, just the ones that genuinely look promising given both signals at once.
Every time a game character finds a path around an obstacle, a delivery drone picks the fastest route between warehouses, or a robot arm plans its motion through 3D space, there is a good chance A is doing the heavy lifting. It is one of those algorithms that genuinely earns its place in production systems — not because it is the simplest, but because it hits the sweet spot between speed and correctness that Dijkstra's and greedy search both miss in opposite directions. Understanding A at a deep level separates engineers who can implement pathfinding from engineers who can tune, scale, and debug it under real production constraints.
The core problem A solves is informed shortest-path search. Dijkstra's algorithm is optimal but blind — it expands nodes in every direction equally, wasting effort on paths that lead away from the goal. Pure greedy search races toward the goal but ignores accumulated cost, so it often finds a fast-looking but suboptimal route through terrain that happened to have low heuristic values. A threads the needle by combining both signals into a single evaluation function f(n) = g(n) + h(n), where g(n) is the exact cost from start to node n, and h(n) is a heuristic estimate of the remaining cost to the goal. When h(n) never overestimates the true remaining cost — the admissibility condition — A is provably optimal. When h(n) is also consistent — satisfying the triangle inequality across neighboring nodes — A never needs to reopen a closed node, making it strictly more efficient.
By the end of this article you will understand exactly why admissibility guarantees optimality, how to choose and design heuristics for different problem domains, why tie-breaking is the silent performance killer in grid-based A, how weighted A trades bounded suboptimality for speed, and what production engineers do differently from textbook implementations. You will walk away with complete, production-quality Java code and the mental model to answer every A* question an interviewer can reasonably ask.
What is A* Search — Plain English Before the Formulas
A* (pronounced A-star) finds the shortest path from a start node to a goal node in a weighted graph by using a heuristic — an estimate of the remaining distance to the goal — to prioritize which nodes to explore next. It does not wander randomly. It does not blindly explore every direction. It specifically focuses exploration on the nodes that look most promising given both how far you have already traveled and how far you probably still need to go.
Dijkstra's algorithm explores nodes by their actual cost from the start — it knows exactly how far each node is but has no idea which direction the goal is. It expands outward like a ripple in every direction, which is optimal but wasteful when you have a specific destination. Greedy best-first search goes to the opposite extreme — it only looks at the estimated cost to the goal and ignores how far it has already walked. This can lead it straight into a dead end because it chases the heuristic without accounting for the actual terrain cost.
A threads the needle between both approaches. Its evaluation function is f(n) = g(n) + h(n), where g(n) is the exact accumulated cost from start to node n, and h(n) is the heuristic estimate from n to the goal. By always expanding the node with the lowest f(n), A balances what it knows for certain (g) with what it estimates (h). When the heuristic never overestimates the true remaining cost — the admissibility condition — A* is mathematically guaranteed to find the optimal path while exploring dramatically fewer nodes than Dijkstra on graphs with a clear spatial structure.
- Dijkstra opens the door closest to where you started, measured by actual walking distance (g only). Optimal — never misses the shortest path. Slow — explores in every direction equally with no goal awareness.
- Greedy opens the door that feels closest to the exit, measured by straight-line guess (h only). Fast — heads toward the goal immediately. Unreliable — ignores terrain cost and gets fooled by dead ends.
- A* opens the door with the lowest total estimated trip cost — actual distance walked plus estimated distance remaining (g + h). Optimal AND goal-focused — explores far fewer nodes than Dijkstra without sacrificing correctness.
- When h=0 for all nodes, A* collapses to Dijkstra — the heuristic contributes nothing and pure cost drives expansion.
- When g is ignored, A* collapses to greedy — cost is abandoned and the heuristic races toward the goal unchecked.
- Admissibility is what keeps A* honest. Without it, you have speed but not correctness — and wrong paths with no error message.
How A* Works — The Algorithm Built from First Principles
Rather than treating A* as a black-box procedure to memorize, derive each step from the problem it solves.
You want to find the minimum-cost path from start to goal. At any point during the search, you have a set of candidate nodes worth exploring — nodes you know how to reach but have not yet fully expanded. For each candidate n, you know g(n) exactly (the best path found so far from start to n), and you estimate h(n) (some lower bound on the remaining cost to goal). The combined f(n) = g(n) + h(n) is your best possible total path cost through n. You should always explore the candidate with the lowest f(n) first — that is the most promising path.
Algorithm: 1. Initialize: open_set = priority queue containing (f(start), start). Set g[start] = 0. All other nodes have g = infinity. 2. While open_set is not empty: a. Pop node n with minimum f(n). b. If n is the goal: return the reconstructed path. The g-value at this moment is the proven optimal cost. c. Mark n as closed — its g-value is now final and optimal. d. For each neighbor nb of n with edge cost cost(n, nb): i. If nb is already in closed_set: skip — its g-value is already optimal. ii. Compute tentative_g = g[n] + cost(n, nb). iii. If tentative_g < g[nb]: this is a better path to nb. Update g[nb] = tentative_g. Record parent[nb] = n. Push (g[nb] + h(nb), nb) into open_set. 3. If open_set empties without reaching the goal: no path exists.
Why does this work? When n is popped with minimum f(n), any alternative path through a node still in the open set has f >= f(n). Since h is admissible (never overestimates), that alternative path's true cost is at least f(n). Therefore g(n) as computed is the true optimal cost to n. This is the formal argument for why pop-time goal checking is correct and insertion-time goal checking is not.
- Manhattan distance |goal_r
- n_r| + |goal_c
- n_c|: admissible and consistent for 4-directional grids.
- Chebyshev distance max(|goal_r
- n_r|, |goal_c
- n_c|): admissible and consistent for 8-directional grids.
- Euclidean distance sqrt((goal_x
- n_x)^2 + (goal_y
- n_y)^2): admissible for any spatial graph, consistent for continuous spaces.
- Domain-specific precomputed tables: offline Dijkstra runs that provide tight lower bounds without overestimating.
Diagonal Distance Heuristic for 8-Directional Movement
When movement allows diagonal steps (8-directional), Manhattan distance underestimates the true path cost because it assumes you must take separate horizontal and vertical steps. For grids where diagonal moves are allowed, a tighter admissible heuristic is the diagonal distance (also called octile distance).
The formula for diagonal distance depends on the cost of diagonal moves relative to cardinal moves. Let D be the cost of a cardinal step and D2 be the cost of a diagonal step (often D=1, D2=√2 for Euclidean metric). Then:
h(n) = D (|Δx| + |Δy|) + (D2 - 2D) * min(|Δx|, |Δy|)
When D = D2 (diagonal costs same as cardinal), this simplifies to Chebyshev distance: h(n) = max(|Δx|, |Δy|).
When D2 > D (the common case), the formula accounts for the fact that diagonal steps are cheaper than two cardinal steps. The expression effectively counts the number of diagonal steps as min(|Δx|,|Δy|) and the remaining cardinal steps as |Δx|+|Δy| - 2*min(|Δx|,|Δy|).
Use diagonal distance whenever your grid allows 8-directional movement and you want an admissible heuristic that avoids the underestimation of Manhattan. It is consistent as long as edge costs satisfy the triangle inequality (e.g., D2 ≤ 2D). For games and robotics planning on 8-connected grids, this heuristic provides significant performance improvements over Manhattan.
Why Greedy Best-First Search Fails — The Perils of Heuristic-Only Navigation
Greedy best-first search (GBFS) prioritizes nodes using only the heuristic h(n) — it ignores the accumulated cost g(n). In every iteration, it expands the node that appears closest to the goal, regardless of how expensive the path to that node was. This can produce fast initial progress but often leads to suboptimal or even catastrophic results.
Consider a 2D grid with obstacles and varied terrain costs. GBFS charges toward the goal using the heuristic (e.g., Euclidean distance). If a direct route passes through a high-cost swamp (cost 10 per step), GBFS will greedily enter the swamp because the heuristic value is small — the goal looks close. Dijkstra would avoid the swamp and take a longer but cheaper route around it. A* would balance the cheap detour cost against the heuristic, eventually finding the optimal path. GBFS fails because it lacks the exact cost component: it has no memory of how much it has already spent.
Another failure mode: dead ends. If a heuristic falsely suggests a short path through a cul-de-sac, GBFS rushes in and then must backtrack extensively. Dijkstra and A* would never enter such regions in the first place because their cost-aware expansion would have already ruled them out.
In production, GBFS is rarely used alone. It may appear as a component in anytime algorithms or for very rough approximations where path optimality is irrelevant (e.g., a quick visual guide). But for any system that requires predictable, correct routing, GBFS is unreliable — use A* instead.
Progressive Algorithm Evolution: From Blind Search to Informed Search
The progression from uninformed to informed search helps understand why A* is the right choice for most pathfinding problems. Each step adds a new piece of information:
| Algorithm | Uses actual cost g(n)? | Uses heuristic h(n)? | Optimal? | Typical use case |
|---|---|---|---|---|
| Breadth-First Search (BFS) | No (only step count) | No | Yes (unweighted) | Unweighted graphs, shortest path in number of edges |
| Dijkstra | Yes | No | Yes | Weighted graphs, all-pairs shortest paths |
| Greedy Best-First Search | No | Yes | No | Very fast approximations, no optimality required |
| A* Search | Yes | Yes | Yes (with admissible h) | Single-target weighted graphs, real-world routing |
BFS treats all edges as unit cost and expands level by level. Dijkstra adds cost tracking, making it optimal for weighted graphs but blind to the goal location. Greedy adds goal-awareness through the heuristic but discards cost, losing optimality. A* combines both: it tracks exact cost like Dijkstra and uses goal-awareness like greedy, achieving optimality and efficiency simultaneously.
This progression shows that A is not a fundamentally different algorithm — it is the natural integration of cost tracking and heuristic guidance. Each predecessor is a special case of A (h=0 gives Dijkstra; g ignored gives greedy; h=0 and g step count gives BFS).
Worked Example — A* on a 4x4 Grid with Full Node Expansion Trace
Grid (S = start at (0,0), G = goal at (3,3), X = obstacle, . = open cell):
S . . . . X X . . X . . . . . G
Heuristic: Manhattan distance h(r,c) = |3-r| + |3-c|. Edge cost: 1 per step. g(start)=0, h(start)=6, f(start)=6.
All node states shown as (r,c) g=X h=Y f=Z.
Initial open set: {(0,0) g=0 h=6 f=6}
Step 1 — Pop (0,0) f=6. Mark closed. Neighbors: (0,1) and (1,0). Both open. (0,1): tentative_g=1, h=5, f=6. Push. (1,0): tentative_g=1, h=5, f=6. Push. Open set: {(0,1) f=6, (1,0) f=6}
Step 2 — Pop (0,1) f=6 (tie broken arbitrarily). Mark closed. Neighbors: (0,0) closed skip. (0,2) open. (1,1) obstacle skip. (0,2): tentative_g=2, h=4, f=6. Push. Open set: {(1,0) f=6, (0,2) f=6}
Step 3 — Pop (1,0) f=6. Mark closed. Neighbors: (0,0) closed skip. (2,0) open. (1,1) obstacle skip. (2,0): tentative_g=2, h=4, f=6. Push. Open set: {(0,2) f=6, (2,0) f=6}
Step 4 — Pop (0,2) f=6. Mark closed. Neighbors: (0,1) closed skip. (0,3) open. (1,2) obstacle skip. (0,3): tentative_g=3, h=3, f=6. Push. Open set: {(2,0) f=6, (0,3) f=6}
Step 5 — Pop (2,0) f=6. Mark closed. Neighbors: (1,0) closed skip. (3,0) open. (2,1) obstacle skip. (3,0): tentative_g=3, h=3, f=6. Push. Open set: {(0,3) f=6, (3,0) f=6}
Step 6 — Pop (0,3) f=6. Mark closed. Neighbors: (0,2) closed skip. (1,3) open. (1,3): tentative_g=4, h=2, f=6. Push. Open set: {(3,0) f=6, (1,3) f=6}
Step 7 — Pop (3,0) f=6. Mark closed. Neighbors: (2,0) closed skip. (3,1) open. (3,1): tentative_g=4, h=2, f=6. Push. Open set: {(1,3) f=6, (3,1) f=6}
Step 8 — Pop (1,3) f=6. Mark closed. Neighbors: (0,3) closed skip. (2,3) open. (2,3): tentative_g=5, h=1, f=6. Push. Open set: {(3,1) f=6, (2,3) f=6}
Step 9 — Pop (3,1) f=6. Mark closed. Neighbors: (3,0) closed skip. (3,2) open. (2,1) obstacle skip. (3,2): tentative_g=5, h=1, f=6. Push. Open set: {(2,3) f=6, (3,2) f=6}
Step 10 — Pop (2,3) f=6. Mark closed. Neighbors: (1,3) closed skip. (3,3) open. (2,2) open. (3,3): tentative_g=6, h=0, f=6. Push. (2,2): tentative_g=6, h=2, f=8. Push. Open set: {(3,2) f=6, (3,3) f=6, (2,2) f=8}
Step 11 — Pop (3,2) f=6. Mark closed. Neighbors: (3,1) closed skip. (3,3) already in open with g=6. Tentative g from (3,2) = 6. Not better — skip. (2,2): tentative from (3,2) = 6, current g=6 from step 10. Equal — skip. Open set: {(3,3) f=6, (2,2) f=8}
Step 12 — Pop (3,3) f=6. THIS IS THE GOAL. Return path.
Path reconstruction via parent map: (3,3) ← (2,3) ← (1,3) ← (0,3) ← (0,2) ← (0,1) ← (0,0)
Final path: (0,0) → (0,1) → (0,2) → (0,3) → (1,3) → (2,3) → (3,3) Total cost: 6. Length: 7 nodes. Optimal — this is the shortest path around the obstacles.
Nodes expanded: 12. Dijkstra on this grid would expand all 12 open cells before reaching the goal, offering no savings. On larger grids with distant goals, the savings become dramatic because A* does not expand the cells in the bottom-left quadrant that are provably not on any optimal path to the bottom-right goal.
Complete Production Implementation — A* with Lazy Deletion, Tie-Breaking, and Path Reconstruction
The implementation below is production-quality: lazy deletion instead of decrease-key, long arithmetic throughout, tie-breaking by g-value, full path reconstruction, and input validation. It handles weighted grids by accepting a cost function, making it adaptable to terrain-cost maps, road networks, and game world navigation.
Admissibility and Consistency — Why the Heuristic Is Everything
The heuristic h(n) is the component that makes A faster than Dijkstra. It is also the component that can silently break correctness if designed carelessly. Understanding admissibility and consistency at a formal level is what separates an engineer who implements A from one who can certify it is correct in production.
Admissibility means h(n) <= true_cost(n, goal) for every node n in the graph. The heuristic never overestimates the remaining cost. This is the condition that guarantees A finds the optimal path. Intuition: if A is about to skip a path through node n because f(n) looks too high, admissibility guarantees that the true cost through n really is at least f(n). If we skipped it, we could not have done better through n anyway.
Consistency (also called monotonicity) is a stronger condition: h(n) <= cost(n, nb) + h(nb) for every node n and every neighbor nb. This is the triangle inequality applied to the heuristic. Consistency implies admissibility. The practical consequence of consistency is that A* with a consistent heuristic never needs to reopen a closed node — once a node is popped and marked closed, its g-value is proven optimal and no future path can improve it. This means every node is expanded at most once, giving the tightest possible worst-case bound.
The relationship: every consistent heuristic is admissible. Not every admissible heuristic is consistent. If your heuristic is admissible but not consistent, A* still finds the optimal path, but it may occasionally need to reopen a closed node when a better path to it is discovered later.
- Manhattan distance: consistent for 4-directional grids with uniform edge costs. Proof: moving from n to nb costs at least 1, and Manhattan distance changes by at most 1 per step, so h(n) <= 1 + h(nb) always holds.
- Euclidean distance: consistent for any spatial graph where edge costs equal Euclidean length. Follows directly from the geometric triangle inequality.
- Diagonal distance: consistent for 8-directional grids where diagonal moves cost sqrt(2).
How to validate your heuristic in production: 1. Run Dijkstra on your graph to compute true_cost(n, goal) for all nodes n. 2. Assert h(n) <= true_cost(n, goal) for every n. 3. Run this as a CI test on a representative graph sample, not just hand-picked cases. 4. If any assertion fails, your heuristic is inadmissible — fix it before shipping.
- h(n) <= true_cost(n, goal) means f(n) = g(n) + h(n) is a lower bound on the true cost of any path through n.
- When A* closes the goal node, it has found a path with cost g(goal). Every other node still in the open set has f >= g(goal) >= true_optimal_cost. No alternative can be cheaper.
- A multiplier > 1.0 on a lower-bound heuristic destroys admissibility immediately. 1.3 × euclidean is not a conservative estimate — it is an overestimate by definition.
- Consistent heuristics give you an additional guarantee: the first time a node is expanded, its g-value is final. No reopening needed.
- Zero heuristic (h=0) is trivially admissible and consistent — it makes A* exactly Dijkstra. It is the safe fallback when you cannot design a meaningful heuristic.
C++ Implementation Stub for A* Pathfinding
The core A* logic translates directly to C++ with standard library containers. Below is a minimal but complete grid-based implementation using std::priority_queue with a custom comparator for tie-breaking. This stub focuses on the algorithm structure and can be extended with terrain costs and 8-directional movement.
std::vector for gScore and closed sets for cache locality. For very large grids, consider a bucket priority queue or a relaxed heap to avoid O(log n) per pop.std::priority_queue is the standard container. For extra performance, consider a custom bucket priority queue or a relaxed heap. The key point is that the algorithm remains unchanged across languages — only the data structure syntax differs.LONG_MAX for unvisited and always check closed after popping.Applications — Where A* Excels in Production Systems
A is the dominant pathfinding algorithm in video games for moving NPCs and units across tactical maps, where it must compute paths for hundreds of agents per frame with <1ms per query. GPS navigation systems use A on road network graphs with heuristic based on straight-line distance or Haversine distance for long-haul routes. Robotics applications use A for arm motion planning in 3D configuration spaces, often with custom heuristics derived from precomputed distance fields. A is also used in network routing, puzzle solving, and natural language processing for word ladder problems. Its flexibility and optimality guarantee make it the go-to algorithm whenever a shortest path is needed and a meaningful heuristic exists.
The Open and Closed Set — Where Good Paths Go to Die
Every A* implementation runs on two collections: the open set and the closed set. The open set holds nodes you've seen but haven't fully evaluated. The closed set holds nodes you've already processed. Get these wrong and your algorithm either explodes in memory or finds paths that don't exist.
The open set must be a priority queue sorted by f-cost. Production code uses a binary heap, not a sorted list. A sorted list destroys performance as the grid scales — inserting a node becomes O(n) instead of O(log n). The closed set is a simple hash set keyed by node coordinates or ID. That's it. Two data structures. One clear contract.
Here's the trap: developers over-engineer the closed set. They store g-costs, h-costs, parent pointers inside it. Don't. The closed set answers one question: "Have I already expanded this node?" Store minimal data. Keep your state in the node objects on the open set or in a separate path reconstruction map. Every extra byte in the closed set is cache pollution you don't need.
The Euclidean Heuristic — Why Your Robot Crashes Into Walls
The Euclidean distance heuristic — sqrt((dx² + dy²)) — looks mathematically pure but actively sabotages grid-based pathfinding. Why? Because it underestimates the true cost on a grid where diagonal moves cost the same as cardinal moves. Your heuristic says "I'm close" while your path is blocked by a wall. The robot commits to a dead end.
On an 8-directional grid where diagonal moves cost sqrt(2) ≈ 1.414, Euclidean is admissible and consistent. On a 4-directional grid, it's only admissible if you allow fractional movement costs. Real grids use integer costs. The heuristic says 5, but the actual path is 9. A* expands more nodes than necessary because the heuristic is too optimistic.
Use Manhattan distance for 4-directional grids. Use Chebyshev distance for 8-directional grids where diagonals cost 1. Use octile distance when diagonals cost sqrt(2). Match your heuristic to your movement model. Anything else wastes CPU cycles and frustrates players when their NPC walks into a corner and gives up.
Key Components of A* — What Drives the Search
A* combines three essential pieces: the graph representation, the cost functions, and the priority queue. The graph defines nodes and edges; edges carry a movement cost. Two cost functions drive decisions: g(n) is the actual cost from start to node n, and h(n) is the heuristic estimate from n to the goal. The total f(n) = g(n) + h(n) determines node expansion order. A min-heap priority queue (often a binary heap) ensures the node with lowest f(n) is examined next. Without these components working together, A degrades into Dijkstra's algorithm (if h is zero) or greedy best-first search (if g is ignored). Understanding each piece explains why A guarantees optimal paths only when h is admissible (never overestimates) and consistent (obeys triangle inequality). Tune any component, and the path quality, speed, or memory usage shifts dramatically.
Managing Node Lists — Open and Closed Set Efficiency
A* maintains two collections: the open set (nodes to evaluate) and the closed set (nodes already fully expanded). A naive list-based open set causes O(n) lookups per expansion, crippling performance on grids with thousands of nodes. Production code uses a binary heap or Fibonacci heap for the open set, paired with a hash map tracking each node's current g-value for O(1) membership tests. The closed set is typically a boolean array or hash set. A critical optimization is lazy deletion: when a better path to a node already in the open set is found, update its g-value and push the new entry instead of removing the old one. The old stale entry is ignored when popped if its g-value mismatches the map. This avoids expensive heap removal operations but increases memory. For embedded systems with tight memory, use a fixed-size array and index-based heaps.
Complexity Analysis — When A* Slows Down
A time complexity depends on the heuristic quality and graph branching factor. In the worst case, with a poor heuristic (close to zero), A degrades into Dijkstra's algorithm with O(b^d) node expansions, where b is the branching factor and d is solution depth. Space complexity is O(b^d) because the open set stores all frontier nodes. With an ideal admissible heuristic that perfectly estimates remaining cost, A expands only nodes on the optimal path, giving O(d) complexity. The effective branching factor b = N^(1/d), where N is total expanded nodes, measures heuristic quality—a value near 1.0 is ideal. In practice, grid pathfinding with Manhattan heuristic on an open grid expands O(n) nodes for an n x n grid. Memory dominates: storing 10 million nodes (common in large maps) costs ~1.2 GB with Java objects. Use bit-packed grids or hierarchical pathfinding for terrains exceeding 10k x 10k cells.
Delivery Drone Routes 23% Longer Than Optimal — Inadmissible Heuristic in Production
- An inadmissible heuristic produces wrong answers silently — no crash, no exception, no log line. Just suboptimal paths that look plausible because they are valid paths, just not optimal ones.
- Never multiply heuristic values by a safety factor without formally proving that admissibility is preserved. A factor greater than 1.0 on a lower-bound heuristic always violates admissibility.
- Always validate heuristic correctness with automated tests that compare A* output against Dijkstra on a representative sample of graphs. If Dijkstra ever finds a shorter path, your heuristic is inadmissible.
- Separate the real-time routing path from the optimal routing path in your architecture. Use weighted A with a logged optimality bound when latency matters, and standard A when correctness is non-negotiable.
grep -n 'heuristic\|def h\|return.*Math.abs\|return.*sqrt\|\* 1\.' io/thecodeforge/pathfinding/AStarSolver.javajava -cp . io.thecodeforge.pathfinding.AStarValidator --compare-dijkstra --graph test.graph --sample-rate 0.1Key takeaways
if (tentative_g < gScore[neighbor]) to prevent unbounded heap growth.Common mistakes to avoid
5 patternsUsing an inadmissible heuristic (overestimating) without realising it
Checking for the goal at insertion time instead of pop time
Not implementing lazy deletion or closed set check
if (closed[node]) continue;. Then mark node as closed. This eliminates all stale heap entries without needing decrease-key.Pushing every neighbor without checking g-value improvement
tentative_g < gScore[neighbor]. Without this guard, every path to a node — including strictly worse ones — adds a heap entry. This is the most common performance bug in production A* implementations.Using string concatenation for node keys in large graphs
row + "," + col string allocation dominates runtime and GC pressure.key = row * width + col. Use long[][] or int[] arrays for gScore and parent maps. This eliminates all string allocation and reduces memory footprint significantly.Practice These on LeetCode
Interview Questions on This Topic
What is the difference between admissibility and consistency in A*?
Frequently Asked Questions
20+ years shipping performance-critical code where algorithms decide the bill. Written from production experience, not tutorials.
That's Graphs. Mark it forged?
17 min read · try the examples if you haven't