Coding Interview Patterns Module 23 – Weighted graphs: shortest paths and spanning trees
A* search with admissible heuristics
A warehouse robot's fastest route over slow and blocked floor tiles: the A* search problem, its Dijkstra baseline and the signal for an estimate.
What you will learn
- Find the cheapest route on a grid with A*, ordering cells by time so far plus the Manhattan distance to the goal
- Explain admissibility and consistency, and prove what each one guarantees
- Measure how many cells A* settles against Dijkstra, and how the tie rule changes that count
- Show with a run that an estimate which overestimates can return a slower route
- Choose estimates for diagonal moves and for several goals
Before you start
What you’ll learn · Before you start
What you will learn
- Find the cheapest route on a grid with A*, ordering cells by time so far plus the Manhattan distance to the goal
- Explain admissibility and consistency, and prove what each one guarantees
- Measure how many cells A* settles against Dijkstra, and how the tie rule changes that count
- Show with a run that an estimate which overestimates can return a slower route
- Choose estimates for diagonal moves and for several goals
Before you start
On this page
A* search finds a cheapest route from a start to one known goal, as Dijkstra’s algorithm does, but it orders its queue by the cost so far plus an estimate of the cost still to come. With an admissible heuristic, an estimate that never overestimates the remaining cost, the moment A* takes the goal from its queue it has a cheapest route. On grids the usual estimate is the Manhattan distance to the goal. A good estimate steers the search towards the goal, so it settles fewer cells than Dijkstra for the same answer.
The problem: a robot on a warehouse floor
A warehouse floor is a grid of up to 300 × 300 cells. # is a shelf the robot cannot enter, . a tile that takes 1
second to enter and ~ a wet tile that takes 3. The robot at S moves one cell up, down, left or right per step,
and entering S or the packing desk T takes 1 second. Return the fewest seconds from S to T, or −1 if T
cannot be reached. The control computer plans 5,000 trips a minute, so a trip should settle as few cells as it can.
- The 6 × 9 floor in the diagram takes 15 seconds; its route crosses one wet tile because going round costs more.
S.#Tabove..#.cannot be done: −1.S~Ttakes 4 seconds: 3 for the wet tile and 1 for the desk.
The first example, with the Manhattan distance in every cell
Text description of the diagram
A grid of 6 rows and 9 columns. The robot S is in the top-left corner and the packing desk T in the bottom-right corner. Dark cells are shelves: in row 0 at column 3; in row 1 at columns 1 and 3; in row 2 at columns 1, 6 and 7; in row 3 at columns 1, 2 and 3; in row 4 at columns 3, 5 and 7; in row 5 at columns 5 and 7. Light blue cells marked with a tilde are wet tiles, which take 3 seconds to enter: row 1 at columns 5 and 6, row 2 at column 5, row 3 at column 5, and row 5 at columns 0 and 1. Every other cell takes 1 second.
Each open cell shows a small number, its Manhattan distance to the desk: 12 next to the robot, falling to 1 next to the desk. A thick line marks the fastest route, 15 seconds: right along row 0 to column 2, down column 2 to row 2, right to column 4, down to row 3, right along row 3 through the wet tile at column 5 to column 8, and down to the desk. The route spends 3 seconds on one wet tile, because going around it costs more.
The brute force, and why it fails at 5,000 trips a minute
Listing routes is hopeless, and Dijkstra’s algorithm, though right, searches nearly the whole floor:
"""Two baselines for the robot's route: listing routes, which explodes, and Dijkstra's algorithm, which is right but
settles almost every cell that the robot could reach sooner than the desk."""
import heapq
import time
from math import comb
COST = {".": 1, "~": 3, "S": 1, "T": 1}
def dijkstra(floor):
"""(seconds to the desk, cells settled), or (-1, cells settled) when the desk cannot be reached."""
rows, cols = len(floor), len(floor[0])
start = next((r, row.index("S")) for r, row in enumerate(floor) if "S" in row)
desk = next((r, row.index("T")) for r, row in enumerate(floor) if "T" in row)
best, heap, settled = {start: 0}, [(0, start)], 0
while heap:
g, (r, c) = heapq.heappop(heap)
if g > best[(r, c)]:
continue
settled += 1
if (r, c) == desk:
return g, settled
for nr, nc in ((r - 1, c), (r + 1, c), (r, c - 1), (r, c + 1)):
if 0 <= nr < rows and 0 <= nc < cols and floor[nr][nc] != "#":
ng = g + COST[floor[nr][nc]]
if ng < best.get((nr, nc), float("inf")):
best[(nr, nc)] = ng
heapq.heappush(heap, (ng, (nr, nc)))
return -1, settled
def floor_of(rows, cols, seed, shelves=20, wet=15):
state, out = seed, []
for _ in range(rows):
row = []
for _ in range(cols):
state = state * 48271 % 2147483647
v = state % 100
row.append("#" if v < shelves else "~" if v < shelves + wet else ".")
out.append(row)
out[0][0], out[-1][-1] = "S", "T"
return ["".join(row) for row in out]
def open_cells(floor):
return sum(row.count(".") + row.count("~") + row.count("S") + row.count("T") for row in floor)
SMALL = ["S..#.....",
".#.#.~~..",
".#...~##.",
".###.~...",
"...#.#.#.",
"~~...#.#T"]
for name, floor in [("small floor", SMALL), ("walled-off desk", ["S.#T", "..#."]), ("one wet tile", ["S~T"])]:
seconds, settled = dijkstra(floor)
print(f"{name}: {seconds} s; Dijkstra settled {settled} of {open_cells(floor)} open cells")
print(f"routes of 38 steps across an open 20 x 20 floor, corner to corner: {comb(38, 19):,}")
big = floor_of(300, 300, 1)
start = time.process_time()
seconds, settled = dijkstra(big)
used = time.process_time() - start
print(f"300 x 300 floor: {seconds} s; Dijkstra settled {settled:,} of {open_cells(big):,} open cells in {used:.2f} s of CPU")
print(f"at that speed, 5,000 trips a minute need {5000 * used / 60:.0f} CPU cores") Output
small floor: 15 s; Dijkstra settled 40 of 40 open cells walled-off desk: -1 s; Dijkstra settled 4 of 6 open cells one wet tile: 4 s; Dijkstra settled 3 of 3 open cells routes of 38 steps across an open 20 x 20 floor, corner to corner: 35,345,263,800 300 x 300 floor: 620 s; Dijkstra settled 71,927 of 72,081 open cells in 0.13 s of CPU at that speed, 5,000 trips a minute need 11 CPU cores
This output changes from run to run: the CPU time depends on the computer and on what else it is doing; the seconds and the counts do not
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 brute_force.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
An open 20 × 20 floor alone has 35,345,263,800 shortest routes from corner to corner. Dijkstra settles all 40 open cells of the small floor, and 71,927 of the 72,081 open cells of the 300 × 300 floor, because the desk is in the far corner. In the recorded run a single trip took over a tenth of a second of CPU in CPython, so 5,000 trips a minute would need more than ten processor cores.
What to notice
- The desk is known. Dijkstra spreads out evenly in every direction and never uses where the desk is.
- A cheap lower bound exists. Every step takes at least 1 second, so the time still needed is at least the Manhattan distance to the desk.
- Bounds add up. Time so far plus that bound is a lower bound for every route through a cell, so cells can be ordered by it.
Five ideas follow:
- A* orders cells by g + h, the seconds so far plus the Manhattan distance to the desk.
- An admissible estimate never exceeds the true remaining time.
- A consistent estimate drops by at most the cost of each step, so a step never lowers g + h.
- A tie rule decides which of two cells with equal g + h comes out of the queue first.
- A weighted estimate multiplies h by a number above 1 and gives up the guarantee of a fastest route.
A* step by step Pro: opens with a Pro, Premium or Ultimate pass
In this part:
- Why an admissible estimate returns the fastest route, proved
- Why a consistent estimate settles each cell once, proved
- A dry run on the small floor
- Dijkstra against A* on 300 × 300 floors, measured
- The code in five languages
- How much work it does
- Edge cases and common bugs
- Variants: diagonal moves and several desks
- Follow-up questions
- Exercise: the robot’s fastest route, within a budget of cells
- The robot's fastest route, within a budget of cells
- A practice set with sample answers
- Check yourself
- Interview questions
- Why is the Manhattan distance an admissible estimate on a grid with four moves where every step costs at least 1, and when does it stop being one?
- A game moves units on a 512 × 512 tile map in eight directions, and a diagonal step costs 1.4. A teammate's A* with the Manhattan estimate sometimes returns longer paths than a slow Dijkstra. Explain why, fix it, and say what the fix costs in speed.
- Your A* with the Manhattan estimate gives right answers, but on large open maps it settles almost every cell, as Dijkstra does. Why, and what small change fixes it? Show that the change keeps every answer right.
- A delivery app answers 1,000 route queries a second on a fixed road network of 2 million junctions with travel times as costs. The straight-line estimate is weak because roads wind. How would you make A* fast, and what would you precompute?
- Key takeaways
1 diagram · 6 runnable examples (C++, Go, Java, JavaScript, Python) · 1 exercise with sample tests · 3 interview answers · about 25 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; nothing renews unless you choose to. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
References
- A Formal Basis for the Heuristic Determination of Minimum Cost Paths (P. E. Hart, N. J. Nilsson and B. Raphael, IEEE Transactions on Systems Science and Cybernetics 4(2), pages 100–107) (IEEE)
- A note on two problems in connexion with graphs (E. W. Dijkstra, Numerische Mathematik 1, pages 269–271) (Springer)
- Heuristic search viewed as path finding in a graph (I. Pohl, Artificial Intelligence 1(3–4), pages 193–204) (Elsevier)
- heapq: Heap queue algorithm (The Python Standard Library) (Python Software Foundation)
- math.comb() (The Python Standard Library) (Python Software Foundation)
- time.process_time() (The Python Standard Library) (Python Software Foundation)
- PriorityQueue (Java SE 25 and JDK 25 API) (Oracle)
- Package container/heap (The Go Authors)
- Number.MAX_SAFE_INTEGER (MDN Web Docs)
- AddressSanitizer (Clang documentation) (LLVM Project)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress