Shortest Path Algorithms

Dijkstra's & Bellman-Ford

A ---4--- B ---2--- E | | / | 2 1 3 6 | | / | C ---5--- D ---1--- F

CS205 Data Structures

Use arrow keys or buttons to navigate

1 / 20

The Shortest Path Problem

Given a weighted graph, find the path from a source vertex to a destination vertex that has the minimum total weight.

  • Edges have numeric weights (costs, distances, times)
  • The "shortest" path = lowest sum of edge weights
  • Not necessarily the fewest edges!

Analogy: GPS Navigation

Your GPS doesn't find the route with the fewest turns -- it finds the route with the least total travel time. Each road segment has a "weight" (time to drive it). The shortest path algorithm finds the optimal route.

A weighted directed graph: A / \ 4 2 / \ B C \ / \ 3 1 6 \ / \ D --5-- E Path A->B->D = 4 + 3 = 7 Path A->C->D = 2 + 1 = 3 <-- shortest! Path A->C->E = 2 + 6 = 8 Path A->C->D->E = 2 + 1 + 5 = 8

Key Idea

A->C->D costs only 3, even though A->B->D has fewer intermediate steps. Weights matter more than hop count.

2 / 20

Single-Source vs All-Pairs

Single-Source Shortest Path (SSSP)

Find shortest paths from one source vertex to all other vertices.

Source = A A -> B : 4 A -> C : 2 A -> D : 3 (via C) A -> E : 8 (via C->D or C)
  • Dijkstra's Algorithm -- greedy, fast, no negative weights
  • Bellman-Ford -- slower, handles negative weights

All-Pairs Shortest Path (APSP)

Find shortest paths between every pair of vertices.

A B C D E A [ 0 4 2 3 8 ] B [ - 0 - 3 8 ] C [ - - 0 1 6 ] D [ - - - 0 5 ] E [ - - - - 0 ]
  • Floyd-Warshall -- O(V^3), dynamic programming
  • Or: run Dijkstra from every vertex

Our Focus

This deck covers single-source algorithms: Dijkstra's and Bellman-Ford. These solve the most common shortest-path scenario.

3 / 20

Greedy Approach: Dijkstra's Algorithm

Invented by Edsger Dijkstra in 1956. The core idea is a greedy strategy:

  • Maintain a set of vertices whose shortest distance is finalized
  • Always pick the unvisited vertex with the smallest known distance
  • Use it to relax (improve) distances to its neighbors
  • Mark it as finalized and repeat

Analogy: Expanding Cloud

Imagine a "cloud" of certainty expanding from the source. At each step, the closest unvisited vertex joins the cloud. Once inside the cloud, its shortest distance is guaranteed correct.

Step-by-step "cloud" expansion: Start: only source in cloud +---------+ | S (d=0) | --> explore neighbors +---------+ Step 1: closest neighbor joins +----------------+ | S (0) A (2) | --> explore A's neighbors +----------------+ Step 2: next closest joins +-----------------------+ | S (0) A (2) B (4) | --> explore B's neighbors +-----------------------+ ... until all vertices are in the cloud

Why Greedy Works

If all edge weights are non-negative, there's no way a later vertex could provide a shorter path to an already-finalized vertex.

4 / 20

Dijkstra's Algorithm -- Pseudocode

DIJKSTRA(G, source): for each vertex v in G: dist[v] = INFINITY parent[v] = NULL visited[v] = false dist[source] = 0 PQ = min-priority-queue PQ.insert(source, 0) while PQ is not empty: u = PQ.extractMin() if visited[u]: continue visited[u] = true for each neighbor v of u: w = weight(u, v) // RELAXATION STEP if dist[u] + w < dist[v]: dist[v] = dist[u] + w parent[v] = u PQ.insert(v, dist[v]) return dist[], parent[]

Three Key Data Structures

  • dist[] -- current best-known distance from source to each vertex
  • parent[] -- predecessor of each vertex on the shortest path (for path reconstruction)
  • PQ -- min-priority queue ordered by distance (tells us which vertex to process next)

Initialization

dist[source] = 0 and all others = infinity. We "know" the source is distance 0 from itself.

Relaxation

The heart of the algorithm: "Can we improve the path to v by going through u?" If yes, update.

5 / 20

Edge Relaxation

The fundamental operation in shortest-path algorithms

Relaxing edge (u, v) means checking:

Is dist[u] + w(u,v) < dist[v] ?

If yes: we found a shorter path to v via u. Update!

If no: the current path to v is already better. Do nothing.

Why "Relaxation"?

Think of dist[v] as an overestimate that we gradually "relax" (tighten) downward until it reaches the true shortest distance.

Before Relaxation

dist[u] = 3 dist[v] = 10 (u) ----7---- (v) Check: 3 + 7 = 10. 10 < 10? NO. No update. Current path to v is tied.

Successful Relaxation

dist[u] = 3 dist[v] = 12 (u) ----7---- (v) Check: 3 + 7 = 10. 10 < 12? YES! Update: dist[v] = 10, parent[v] = u dist[u] = 3 dist[v] = 10 (u) ----7---- (v) ^ updated!
6 / 20

Dijkstra's Step-by-Step Example

Our Graph (source = A)

A ---4--- B | \ | 2 1 3 | \ | C ---5--- D ---2--- E

Edges: A-B:4, A-C:2, A-D:1, B-D:3, C-D:5, D-E:2

Initialization

VertexABCDE
dist[]0
parent[]-----
visitedFFFFF

PQ: {(A, 0)}

Step 1: Process A (dist=0)

Extract A from PQ. Mark visited. Relax neighbors:

Relax A->B: dist[A]+4 = 4 < inf YES dist[B] = 4, parent[B] = A Relax A->C: dist[A]+2 = 2 < inf YES dist[C] = 2, parent[C] = A Relax A->D: dist[A]+1 = 1 < inf YES dist[D] = 1, parent[D] = A
VertexABCDE
dist[]0421
parent[]-AAA-
visitedTFFFF

PQ: {(D,1), (C,2), (B,4)}

7 / 20

Dijkstra's Trace (continued)

Step 2: Process D (dist=1)

Extract D (smallest in PQ). Mark visited. Relax:

Relax D->B: dist[D]+3 = 4. 4 < 4? NO (tied, no update) Relax D->C: dist[D]+5 = 6. 6 < 2? NO (current path to C is better) Relax D->E: dist[D]+2 = 3. 3 < inf? YES dist[E] = 3, parent[E] = D
VertexABCDE
dist[]04213
parent[]-AAAD
visitedTFFTF

PQ: {(C,2), (E,3), (B,4)}

Step 3: Process C (dist=2)

Extract C. Mark visited. Relax:

Relax C->D: dist[C]+5 = 7. 7 < 1? NO (D already has shorter path) C's only unvisited neighbor via edge C-D is already finalized. No updates.
VertexABCDE
dist[]04213
parent[]-AAAD
visitedTFTTF

PQ: {(E,3), (B,4)}

Notice

C's neighbors are all either visited (D) or lead to no improvement. The cloud grows with no distance changes.

8 / 20

Dijkstra's Trace (complete)

Step 4: Process E (dist=3)

Extract E. Mark visited. E's neighbor D is already visited. No updates.

VertexABCDE
dist[]04213
visitedTFTTT

PQ: {(B,4)}

Step 5: Process B (dist=4)

Extract B. Mark visited. B's neighbor D is visited. No updates.

VertexABCDE
dist[]04213
visitedTTTTT

PQ: {} (empty -- done!)

Final Shortest Path Tree

Source: A Final distances: A=0, B=4, C=2, D=1, E=3 Shortest Path Tree (parent edges): A (0) /|\ / | \ 4 2 1 / | \ B(4) C(2) D(1) \ 2 \ E(3) Paths from A: A -> B = 4 A -> C = 2 A -> D = 1 A -> D -> E = 3

Processing Order

A(0) -> D(1) -> C(2) -> E(3) -> B(4). The vertices were processed in increasing order of their shortest distance from the source.

9 / 20

Reconstructing the Path

Dijkstra gives us the distance to every vertex, but how do we find the actual path?

Use the parent[] array! Trace backwards from the destination to the source.

parent[] from our example: parent[A] = - (source) parent[B] = A parent[C] = A parent[D] = A parent[E] = D

Algorithm

PATH(parent, destination): path = empty stack v = destination while v != NULL: path.push(v) v = parent[v] return path // pop for correct order

Example: Path to E

Goal: find path A -> ... -> E Start at E: E -> parent[E] = D D -> parent[D] = A A -> parent[A] = NULL (stop!) Stack contents: [E, D, A] Pop to get path: A -> D -> E Total distance: dist[E] = 3 (correct!)

Example: Path to B

Goal: find path A -> ... -> B Start at B: B -> parent[B] = A A -> parent[A] = NULL (stop!) Stack contents: [B, A] Pop to get path: A -> B Total distance: dist[B] = 4 (correct!)

Key Idea

The parent pointers form a tree rooted at the source. Every path in this tree is a shortest path.

10 / 20

Dijkstra's Correctness

Why the greedy choice is safe

The Claim

When Dijkstra's extracts vertex u from the priority queue, dist[u] equals the true shortest-path distance.

Proof Sketch (by contradiction)

  • Suppose vertex u is the first vertex extracted with an incorrect distance
  • There must be a true shortest path S -> ... -> x -> y -> ... -> u where y is the first vertex on this path not yet visited
  • Since x is visited, dist[y] ≤ dist[x] + w(x,y) = true distance to y
  • Since all weights ≥ 0: dist[y] ≤ true dist to u
  • But u was extracted before y, so dist[u] ≤ dist[y]
  • Combining: dist[u] ≤ true dist to u
  • But dist[u] ≥ true dist (it's an overestimate). Contradiction!
Proof visualization: Visited cloud Unvisited +-------------+ | S ... x |--w(x,y)-- y ... u +-------------+ ^ ^ correct first "wrong"? distances Impossible! Since w(x,y) >= 0 and w(y...u) >= 0: dist[y] <= dist to u (true) dist[u] <= dist[y] (u extracted first) => dist[u] <= true dist to u => dist[u] IS correct (contradiction)

Critical Assumption

This proof requires all edge weights to be non-negative. If any edge weight is negative, the "cloud" property breaks down -- a later vertex could create a shortcut through a negative edge.

Invariant

Every vertex extracted from the PQ has its correct final shortest distance. This is the loop invariant that makes Dijkstra's correct.

11 / 20

Dijkstra's Time Complexity

With Binary Heap (Min-Priority Queue)

OperationCountCost EachTotal
extractMinVO(log V)O(V log V)
insert / decreaseKeyEO(log V)O(E log V)

Total: O((V + E) log V)

For connected graphs where E ≥ V, this simplifies to O(E log V).

With Simple Array (No Heap)

OperationCountCost EachTotal
findMin (scan)VO(V)O(V^2)
update distEO(1)O(E)

Total: O(V^2)

Simpler to implement but slower. Better only for dense graphs where E is close to V^2.

Which to Use?

Sparse graph (E ~ V): Heap: O(V log V) << Array: O(V^2) --> Use heap! Dense graph (E ~ V^2): Heap: O(V^2 log V) > Array: O(V^2) --> Use array! Most real-world graphs are sparse, so the heap version wins. Even better: Fibonacci Heap gives O(V log V + E) -- optimal! (But rarely used in practice due to complexity.)
12 / 20

Dijkstra's Limitation: Negative Weights

Dijkstra FAILS with Negative Edge Weights!

Once a vertex is "finalized," Dijkstra never reconsiders it. A negative edge can create a shorter path after finalization.

Counterexample

A ---1--- B \ | 4 -6 \ | C --- D (C-D has weight 0)

Edges: A->B:1, A->C:4, B->D:-6, C->D:0

Source: A, Destination: D

What Dijkstra Does (WRONG)

Init: dist = [A:0, B:inf, C:inf, D:inf] Step 1: Process A (d=0) Relax A->B: dist[B] = 1 Relax A->C: dist[C] = 4 PQ: {(B,1), (C,4)} Step 2: Process B (d=1) -- FINALIZED! Relax B->D: dist[D] = 1+(-6) = -5 PQ: {(C,4), (D,-5)} Step 3: Process D (d=-5) -- FINALIZED! PQ: {(C,4)} Step 4: Process C (d=4) -- FINALIZED! Relax C->D: 4+0 = 4. 4 < -5? NO. Dijkstra says: dist[D] = -5 ✔

Wait, that looks correct here. Let's tweak it:

Revised: A->B:1, A->C:3, B->D:5, C->D:1, C->B:-4 A ---1--- B \ ^| 3 -4/ |5 \ / | C --1- D Dijkstra processes A(0), then B(1): Finalizes B with dist=1. Then C(3): Relax C->B: 3+(-4)= -1 < 1? YES! But B is already finalized! Dijkstra WON'T fix B's distance! Dijkstra: dist[B]=1, dist[D]=6 Correct: dist[B]=-1, dist[D]=4

The Problem

Dijkstra finalized B with dist=1, but the path A->C->B costs -1. Once finalized, it is never reconsidered.

13 / 20

Bellman-Ford Algorithm

Handles negative edge weights! Invented independently by Richard Bellman (1958) and Lester Ford Jr. (1956).

Core Idea

  • Instead of being greedy, relax ALL edges in each iteration
  • Repeat for V - 1 iterations
  • Why V-1? A shortest path has at most V-1 edges
  • Each iteration guarantees one more "level" of correctness

Analogy: Ripple Effect

Imagine dropping a stone in a pond. Each "ripple" extends the correct distances by one hop. After V-1 ripples, every vertex is reached.

Pseudocode

BELLMAN-FORD(G, source): for each vertex v in G: dist[v] = INFINITY parent[v] = NULL dist[source] = 0 // Main loop: V-1 iterations for i = 1 to |V| - 1: for each edge (u, v, w) in G: // Relaxation if dist[u] + w < dist[v]: dist[v] = dist[u] + w parent[v] = u // Negative cycle detection for each edge (u, v, w) in G: if dist[u] + w < dist[v]: return "NEGATIVE CYCLE!" return dist[], parent[]

Time Complexity: O(V * E)

Slower than Dijkstra's, but works with negative weights and can detect negative cycles.

14 / 20

Bellman-Ford Step-by-Step

Graph (source = A)

A ---1--- B \ ^| 3 -4/ |5 \ / | C --1- D Edges (processed in this order): (A,B,1) (A,C,3) (B,D,5) (C,B,-4) (C,D,1)

Iteration 1 (of V-1 = 3)

Relax (A,B,1): 0+1=1 < inf YES dist[B]=1, parent[B]=A Relax (A,C,3): 0+3=3 < inf YES dist[C]=3, parent[C]=A Relax (B,D,5): 1+5=6 < inf YES dist[D]=6, parent[D]=B Relax (C,B,-4): 3+(-4)=-1 < 1 YES dist[B]=-1, parent[B]=C Relax (C,D,1): 3+1=4 < 6 YES dist[D]=4, parent[D]=C
ABCD
dist0-134
par-CAC

Iteration 2

Relax (A,B,1): 0+1=1 < -1? NO Relax (A,C,3): 0+3=3 < 3? NO Relax (B,D,5): -1+5=4 < 4? NO Relax (C,B,-4): 3+(-4)=-1<-1? NO Relax (C,D,1): 3+1=4 < 4? NO No changes! Algorithm has converged.
ABCD
dist0-134
par-CAC

Iteration 3

Same as iteration 2. No changes.

Final Shortest Paths

A -> B : A -> C -> B dist = -1 A -> C : A -> C dist = 3 A -> D : A -> C -> D dist = 4 Bellman-Ford finds the correct -1 for B that Dijkstra missed!
15 / 20

Bellman-Ford: Detecting Negative Cycles

The Extra Pass

After V-1 iterations, do one more relaxation pass over all edges.

  • If no distance decreases: all shortest paths are correct
  • If any distance decreases: there is a negative cycle reachable from the source!
// After main V-1 loop: for each edge (u, v, w) in G: if dist[u] + w < dist[v]: return "NEGATIVE CYCLE DETECTED!"

Why V-1 Iterations Suffice

A shortest path (without cycles) visits at most V vertices, so it has at most V-1 edges. Each iteration correctly extends paths by one edge. After V-1 iterations, all shortest paths are found -- unless a negative cycle exists.

Example: Negative Cycle Detected

Graph with negative cycle: A --1-- B | -2 | D --3-- C | -4 | B (B->C->D->B = -2+3+(-4) = -3) Cycle: B -> C -> D -> B cost = -3 Each trip around = -3 more!
After V-1 = 3 iterations: dist[B] keeps getting smaller... V-th pass check: Relax B->C: dist[B]+(-2) < dist[C]? YES! Distance still decreasing! ==> NEGATIVE CYCLE DETECTED

What to Report

When a negative cycle is detected, the algorithm should report that no valid shortest path exists (for vertices reachable through the cycle). The distance is effectively negative infinity.

16 / 20

Negative Cycles Explained

What Is a Negative Cycle?

A cycle in the graph where the total weight of all edges is negative.

A negative cycle: B --(-2)--> C ^ | | v +----3-----D ^ | | v +-(-4)-+ B -> C -> D -> B cost: -2 + 3 + (-4) = -3 Going around once = -3 Going around twice = -6 Going around n times = -3n As n -> infinity, cost -> -infinity

Why This Breaks Shortest Path

If you can reach a negative cycle from the source, and your destination is reachable from the cycle, then:

Source --> ... --> [negative cycle] --> ... --> Dest Path cost = (prefix) + n*(-3) + (suffix) Want shorter? Just go around more! n=1: prefix + (-3) + suffix n=2: prefix + (-6) + suffix n=10: prefix + (-30) + suffix n=inf: -INFINITY The "shortest path" is undefined!

Analogy: Infinite Money Glitch

Imagine currency exchange where converting USD -> EUR -> GBP -> USD gives you more money than you started with. You'd loop forever, making infinite money. That's a negative cycle in a currency exchange graph!

Important Distinction

Negative edges are fine (Bellman-Ford handles them). Negative cycles make the problem ill-defined.

17 / 20

Dijkstra vs Bellman-Ford

Choosing the right algorithm

Property Dijkstra's Bellman-Ford
Strategy Greedy Dynamic Programming (relaxation)
Time (binary heap) O((V+E) log V) O(V * E)
Time (array) O(V^2) O(V * E)
Negative edge weights NOT supported Supported
Negative cycle detection No Yes
Space O(V) + PQ O(V)
Best for Non-negative weights, speed Negative weights, cycle detection

When to Use Dijkstra's

  • All edge weights are non-negative
  • You need the fastest algorithm
  • GPS navigation, network routing, game pathfinding

When to Use Bellman-Ford

  • Graph may have negative edge weights
  • You need to detect negative cycles
  • Currency arbitrage, some network protocols (RIP)
18 / 20

Real-World Applications

GPS Navigation

Vertices = intersections Edges = road segments Weights = travel time (or distance) +--School--+ | 2min | Home 3min --> Dijkstra's | 5min | finds fastest +---Mall---+ route!

Google Maps, Apple Maps, Waze all use variants of Dijkstra's with optimizations (A*, contraction hierarchies).

Network Routing (OSPF)

Routers = vertices Links = edges (weight = latency/cost) Router A ---10ms--- Router B | | 5ms 15ms | | Router C ---8ms---- Router D OSPF protocol uses Dijkstra's to compute shortest paths between routers.

Game AI Pathfinding

Grid map with terrain costs: . . . # # . . . = grass (1) . . . # . . . # = wall (inf) S . . . . . . ~ = water (3) ~ ~ . . # . . S = start ~ ~ ~ . # . G G = goal Dijkstra (or A*) finds cheapest path from S to G, avoiding walls and preferring grass over water.

Currency Arbitrage

Convert weights to -log(exchange_rate) Then negative cycle = arbitrage opportunity! USD --0.85--> EUR --0.78--> GBP ^ | +--------1.30---------------+ If 1 * 0.85 * 0.78 * 1.30 > 1.0 ==> Profit! (Bellman-Ford detects this)

Bellman-Ford in Practice

The RIP (Routing Information Protocol) uses a distributed version of Bellman-Ford. Each router shares its distance table with neighbors.

19 / 20

Summary & Cheat Sheet

Dijkstra's Algorithm

1. dist[src] = 0, all others = inf 2. PQ.insert(src, 0) 3. While PQ not empty: a. u = PQ.extractMin() b. Skip if visited c. For each neighbor v of u: if dist[u]+w < dist[v]: dist[v] = dist[u]+w parent[v] = u PQ.insert(v, dist[v]) 4. Use parent[] to reconstruct path Time: O((V+E) log V) with heap Requires: non-negative weights

Edge Relaxation

if dist[u] + w(u,v) < dist[v]:
  dist[v] = dist[u] + w(u,v)

Can I improve the path to v by going through u?

Bellman-Ford Algorithm

1. dist[src] = 0, all others = inf 2. Repeat V-1 times: For each edge (u, v, w): if dist[u]+w < dist[v]: dist[v] = dist[u]+w parent[v] = u 3. One more pass: if any dist decreases -> NEGATIVE CYCLE! Time: O(V * E) Handles: negative weights + detection

Decision Flowchart

Need shortest path? | +-- All weights >= 0? | YES --> Dijkstra's (faster) | NO --> Bellman-Ford | +-- Need negative cycle detection? | YES --> Bellman-Ford | +-- All-pairs needed? YES --> Floyd-Warshall (or run Dijkstra from every vertex)

Common Exam Pitfalls

  • Using Dijkstra with negative weights
  • Forgetting V-1 iterations in Bellman-Ford
  • Confusing negative edges with negative cycles
20 / 20