Skip to main content

Updated Sep 11, 2026

Shortest Paths

"Shortest" means fewest edges on an unweighted graph and lowest total weight on a weighted one, and the two need different algorithms — which one is decided by two questions: are the edges weighted, and can any weight be negative?

Choosing​

SituationAlgorithmComplexity
Unweighted graphBFSO(V+E)O(V + E)
Non-negative weights, one sourceDijkstra'sO((V+E)log⁡V)O((V + E) \log V)
Negative weights allowed, one sourceBellman–FordO(V⋅E)O(V \cdot E)
All pairs, dense graphFloyd–WarshallO(V3)O(V^3)
Non-negative weights, one target knownA* with an admissible heuristicDepends on heuristic

Mechanism​

Dijkstra's algorithm​

Repeatedly finalise the nearest unfinalised vertex, then relax its outgoing edges. A priority queue supplies "nearest" in O(log⁡V)O(\log V), which is where the log factor comes from.

Animation of Dijkstra's algorithm on a weighted graph of six vertices, with tentative distances starting at infinity and being progressively lowered as each nearest vertex is finalised
Tentative distances start at ∞ and fall as shorter routes are discovered. Each step permanently fixes the nearest remaining vertex — once fixed, it is never revisited. Wikimedia Commons, Public domain
import heapq

def dijkstra(graph, start):
"""graph: {node: [(neighbour, weight), ...]} with all weights >= 0."""
dist = {start: 0}
prev = {start: None}
pq = [(0, start)] # (distance, node)
done = set()

while pq:
d, node = heapq.heappop(pq)
if node in done: # a stale entry — skip it
continue
done.add(node) # dist[node] is now final

for nb, weight in graph[node]:
candidate = d + weight
if candidate < dist.get(nb, float("inf")):
dist[nb] = candidate
prev[nb] = node
heapq.heappush(pq, (candidate, nb)) # lazy deletion
return dist, prev

The done check implements lazy deletion: heapq cannot decrease an existing entry's key, so the code pushes a new one and ignores the outdated copy when it surfaces. This is the standard workaround, it keeps the complexity correct, and it is simpler than maintaining an indexed heap.

Worked trace: Dijkstra's from A​

Run on the shared weighted graph. Each row is one real pop (stale entries — an earlier, more expensive copy of an already-finalised vertex — are noted but change nothing):

pop done so far dist[A,B,C,D,E,F] frontier after (stale entries dropped)
A (0) {A} 0, 4, 2, inf, inf, inf [(2,C),(4,B)]
C (2) {A,C} 0, 3, 2, 10, 12, inf [(3,B),(10,D),(12,E)] -- (4,B) now stale
B (3) {A,C,B} 0, 3, 2, 8, 12, inf [(8,D),(12,E)] -- (4,B),(10,D) stale
D (8) {A,C,B,D} 0, 3, 2, 8, 10, 14 [(10,E),(14,F)] -- (10,D),(12,E) stale
E (10) {A,C,B,D,E} 0, 3, 2, 8, 10, 13 [(13,F)] -- (12,E),(14,F) stale
F (13) {A,C,B,D,E,F} 0, 3, 2, 8, 10, 13 [] -- (14,F) popped, also stale

Final distances from A: B=3 via A->C->B (cheaper than the direct edge's 4), C=2, D=8 via A->C->B->D, E=10, F=13, in increasing-distance pop order A, C, B, D, E, F — no path still in the queue can ever beat one already popped, since every edge weight is non-negative.

Dijkstra's is wrong on negative weights — silently

A negative edge breaks the "never gets shorter" assumption: A→B=5, A→C=6, C→B=−4 finalises B at 5, then discovers A→C→B costs 2 — but B is already done and never updated. No error, no warning; the distance is simply wrong. If any weight can be negative, use Bellman–Ford.

Bellman–Ford​

Relax every edge, V−1 times. Slower, but it makes no assumption about sign, and it can detect negative cycles:

def bellman_ford(vertices, edges, start):
"""edges: [(u, v, weight), ...]; weights may be negative."""
dist = {v: float("inf") for v in vertices}
dist[start] = 0

for _ in range(len(vertices) - 1): # any shortest path has ≤ V−1 edges
changed = False
for u, v, w in edges:
if dist[u] + w < dist[v]:
dist[v] = dist[u] + w
changed = True
if not changed: # early exit once stable
break

for u, v, w in edges: # a further improvement means a negative cycle
if dist[u] + w < dist[v]:
raise ValueError("negative cycle reachable from start")
return dist

The V−1 bound is the whole proof: a shortest path visits each vertex at most once, so it has at most V−1 edges, and each pass extends every path by at least one edge. Negative cycles make "shortest path" meaningless — you can loop forever, getting cheaper each time — and detecting them is a feature: it is how currency-arbitrage detection works, with weights set to −log(exchange rate).

Floyd–Warshall: every pair at once​

Running Dijkstra's or Bellman–Ford from every vertex answers "all pairs" too, but Floyd–Warshall often wins on dense graphs by asking, for each pair (i, j): does routing through k shorten the known path? Trying every k as an intermediate, in order, is the whole algorithm:

def floyd_warshall(n, edges):
"""edges: [(u, v, weight), ...]; weights may be negative, but not a negative cycle."""
INF = float("inf")
dist = [[0 if i == j else INF for j in range(n)] for i in range(n)]
for u, v, w in edges:
dist[u][v] = min(dist[u][v], w)

for k in range(n): # k is the newly-allowed intermediate vertex
for i in range(n):
for j in range(n):
through_k = dist[i][k] + dist[k][j]
if through_k < dist[i][j]:
dist[i][j] = through_k
return dist

# vertices 0..5 = A..F; the shared weighted graph, both directions
graph = {0:[(1,4),(2,2)], 1:[(0,4),(2,1),(3,5)], 2:[(0,2),(1,1),(3,8),(4,10)],
3:[(1,5),(2,8),(4,2),(5,6)], 4:[(2,10),(3,2),(5,3)], 5:[(3,6),(4,3)]}
expected = [0, 3, 2, 8, 10, 13] # the traced distances from A
dist, _ = dijkstra(graph, 0)
assert [dist[v] for v in range(6)] == expected

edges = [(u, v, w) for u in graph for v, w in graph[u]] # flatten, already both directions
assert [bellman_ford(range(6), edges, 0)[v] for v in range(6)] == expected
assert floyd_warshall(6, edges)[0] == expected # row 0 (from A) agrees with both above

The k loop must be outermost, per the DP recurrence dp[k][i][j] = min(dp[k-1][i][j], dp[k-1][i][k] + dp[k-1][k][j]) — swapping k for i still compiles and terminates, but silently computes the wrong answer for some pairs. A negative cycle shows up afterwards as dist[i][i] < 0.

A*: Dijkstra's with a hint​

For a path to one target, A* orders the queue by dist[node] + heuristic(node, goal) instead of dist[node] alone. An admissible heuristic (never overestimates) still returns the true shortest path while exploring far fewer vertices; zero turns A* back into Dijkstra's exactly — see A* & Heuristic Search for the implementation.

Practical Usage​

DomainVerticesWeights
NavigationIntersectionsTravel time — the reason A* dominates here
Network routingRoutersLink cost — OSPF is Dijkstra's, RIP is Bellman–Ford
Game AI pathfindingGrid cells or waypointsMovement cost

Edge Cases & Pitfalls​

  • Unreachable vertices stay at infinity — decide whether that is an error or an expected result.
  • Reconstructing the path needs the prev map, followed backwards from the goal and reversed — returning distances alone is a common oversight.
  • Ties in the priority queue compare the second tuple element — insert a counter if nodes are not orderable, the same hazard as on the heaps page.
  • Dijkstra's on a dense graph is O(V2)O(V^2) with a plain array, beating the heap's O((V+E)log⁡V)O((V+E)\log V) once E approaches V2V^2.
  • An inadmissible A* heuristic returns fast paths that are not necessarily shortest — fine for games, a bug for navigation.

Comparisons​

BFSDijkstra'sBellman–FordFloyd–Warshall
Weights, sources, best forNone, one, unweightedNon-negative, one, usual caseAny, one, cycle checkAny, all pairs, small dense
Complexity (worst)O(V+E)O(V + E)O((V+E)log⁡V)O((V+E)\log V)O(V⋅E)O(V \cdot E)O(V3)O(V^3)
Detects negative cycles—NoYesYes

Recall​

References​

  • Dijkstra, E.W. (1959), "A note on two problems in connexion with graphs", Numerische Mathematik — the original two-page paper.
  • Cormen, Leiserson, Rivest & Stein, Introduction to Algorithms, 4th ed., Ch. 22–23 — single-source and all-pairs shortest paths, including Floyd-Warshall's DP formulation and correctness proof.
  • Hart, Nilsson & Raphael (1968), "A Formal Basis for the Heuristic Determination of Minimum Cost Paths" — the A* paper.
  • VisuAlgo — Shortest Paths — run Dijkstra's and Bellman–Ford on the same graph, including one with negative edges.