Data Structure 📂 Simulators · 2 of 3 94 min read

Shortest Path Algorithms — Dijkstra, Bellman-Ford, Floyd-Warshall, Johnson's, A* and 0-1 BFS (Interactive Simulator)

Learn six ways to find the shortest path in a graph. Dijkstra is for weights of 0 or more. Bellman-Ford handles negative weights and finds negative cycles. Floyd-Warshall and Johnson's find all pairs. A* uses a guess to reach a target fast. 0-1 BFS uses a deque. Draw a graph and step through each one.

Section 01

The Story — The Delivery Rider's Question

Ravi Delivers Food Across the City
Ravi is a delivery rider. The city is a graph. Crossings are nodes. Roads are edges. Each road has a weight: the minutes it takes.

Every order asks the same question: "What is the fastest way from here to there?" But the details change the best method.

Some roads give a cashback bonus (a negative weight). Sometimes the company wants travel times between every pair of shops. Sometimes Ravi knows the rough direction of the customer (a heuristic). Sometimes a road is either free (0) or costs one token (1).

Each case has its own algorithm. This tutorial covers all six.

A shortest path is the path between two nodes with the smallest total weight. It is not always the path with the fewest edges. Three short roads can beat one long road.

💡
The One Big Idea — Relaxation

All six algorithms use the same small move, called relaxing an edge. For an edge u → v with weight w, ask: "Is going through u a cheaper way to reach v?" If dist[u] + w < dist[v], update dist[v]. The algorithms differ only in which edge they relax next.

Relaxation Rule
dist[v] = min(dist[v], dist[u] + w)
If going through u is cheaper, remember the new cost and set prev[v] = u.
Negative Cycle
sum of loop weights < 0
You can go round the loop forever and the cost keeps falling. No shortest path exists.

Section 02

The Six Algorithms at a Glance

📍
Dijkstra
Single source · Priority queue
Always takes the closest unfinished node next. Fast and simple. Needs no negative weights.
🔄
Bellman-Ford
Single source · Edge passes
Relaxes every edge, V − 1 times. Slower, but allows negative weights and detects negative cycles.
🔢
Floyd-Warshall
All pairs · Matrix
Fills a V × V table. For each middle node k, tries i → k → j. Three short loops. Great for small, dense graphs.
🧩
Johnson's
All pairs · Reweight + Dijkstra
Uses Bellman-Ford once to remove negative weights. Then runs Dijkstra from every node. Best for sparse graphs.
⭐
A* (A-star)
One target · f = g + h
Dijkstra plus a guess of the distance left (heuristic h). It heads toward the goal. Used in games and maps.
⚖️
0-1 BFS
Single source · Deque
When weights are only 0 or 1. A 0-edge goes to the front of a deque, a 1-edge to the back. Linear time.

Section 03

Watch Dijkstra Work — Animated Diagram

Start node is A. Numbers on arrows are weights. The value under each node is its current best distance. Watch how B first gets 4, then improves to 3 through C. Crossed-out queue items are stale. They are old copies that get skipped.

🎬 Dijkstra — Live Animation

Final: A = 0, C = 1, B = 3, D = 4. Shortest path A → C → B → D.


Section 04

The Shortest Path Simulator — Build Your Own Graph

This is your lab. Draw nodes and weighted edges, then pick any of the six algorithms. Each Step runs one line of the algorithm. You will see the priority queue, deque, edge list or matrix change. You also see the distance table and the current line of code.

👉 How to Use the Simulator
Draw
Add Node mode: click an empty spot. Drag a node to move it. Double-click a node to rename it.
Join
Add Edge mode: type a weight in the box, then click two nodes. Tick Directed for one-way edges.
Weight
Double-click a weight label to change it. Negative numbers are allowed.
Run
Pick an algorithm, a start and a target. Press Step, Play, or End. Use Back to rewind.
🛣️ Shortest Path Simulator Not started
Draw Weight
Load
Algorithm Start Target h(n)
Speed
Priority Queue
Distance Table
Algorithm — Current Line
What Is Happening
    🎯 Predict first Shortest distance from ? to ? =
    ● Blue = current node ● Amber = waiting in queue ● Green = final (settled) ⎯ Dashed = edge being checked ⎯ Green = best-known edge (prev) ⎯ Purple = shortest path to target
    🧪
    Try These Experiments

    1. City Map + Dijkstra, then A* with the same start and target. Count how many nodes turn green. A* does less work.
    2. Dijkstra Trap: run Dijkstra, then Bellman-Ford. Dijkstra says B = 2, but the true answer is 1.
    3. Negative Cycle: run Bellman-Ford. The extra pass finds an edge that still improves. That proves a loop.
    4. Negative Edges: run Johnson's. Watch the purple labels. The new weights are all 0 or more.
    5. City Map + A* with h = "÷ 30 (too big!)". The guess is now too high, so A* can return a longer path.

    ℹ️
    Simulator Rules

    Up to 10 nodes. Neighbours are checked in label order. Bellman-Ford checks edges in the order you drew them. A* uses the drawn distance between nodes as its guess. Press Auto Weights so every weight is at least that distance. This keeps the guess safe. In an undirected graph, one negative edge is already a negative cycle (go back and forth).


    Section 05

    A* vs Dijkstra — Grid Race

    Games move characters on grids. Click cells to draw walls (drag to paint). Then press Race. Both algorithms find a shortest path of the same length. Green cells show how many cells each one had to explore.

    🎮 Grid Race — Click to Draw Walls

    Dijkstra — spreads in all directions

    explored: 0

    A* — pulled toward the goal

    explored: 0

    Blue = start, red = goal, grey = wall, green = explored, purple = final path. A* uses Manhattan distance |dx| + |dy| as h.


    Section 06

    Why Dijkstra Fails With Negative Weights

    Dijkstra makes a promise. Once a node is taken from the queue, its distance is final. That promise is only true if no edge can make a path cheaper later. A negative edge breaks it.

    ❌ Dijkstra on A→B (2), A→C (5), C→B (−4)
    StepWhat Happens
    1Settle A. B = 2, C = 5
    2Settle B = 2 (it looks closest)
    3Settle C = 5. C→B gives 1, but B is already final
    ResultB = 2 (wrong)
    ✅ Bellman-Ford on the same graph
    StepWhat Happens
    Pass 1B = 2, C = 5, then C→B makes B = 1
    Pass 2No change, so stop early
    CheckNo edge can still improve, so no negative cycle
    ResultB = 1 (correct)
    ⚠️
    Adding a Constant Does Not Fix It

    A common wrong idea: "Add 10 to every weight so all are positive." This favours paths with fewer edges. A path with 4 edges gets +40, but a path with 1 edge gets only +10. The answer changes. Johnson's algorithm fixes this the right way. It adds h[u] − h[v], and those terms cancel along any path.


    Section 07

    Step-by-Step Traces

    Dijkstra — Graph: A→B 4, A→C 1, C→B 2, C→D 5, B→D 1

    StepPopActionPriority Queue Afterdist (A, B, C, D)
    0—Start(0, A)0, ∞, ∞, ∞
    1(0, A)Settle A. B = 4, C = 1(1, C), (4, B)0, 4, 1, ∞
    2(1, C)Settle C. B = 3, D = 6(3, B), (4, B), (6, D)0, 3, 1, 6
    3(3, B)Settle B. D = 4(4, B), (4, D), (6, D)0, 3, 1, 4
    4(4, B)Stale — B already settled. Skip.(4, D), (6, D)0, 3, 1, 4
    5(4, D)Settle D(6, D)0, 3, 1, 4
    6(6, D)Stale. Skip. Queue empty.[ ]0, 3, 1, 4

    Bellman-Ford — Edge order: B→D 2, A→B 4, A→C 5, C→B −3

    PassEdges That Relaxdist (A, B, C, D)
    Start—0, ∞, ∞, ∞
    1A→B: B = 4 · A→C: C = 5 · C→B: B = 2 (B→D skipped, B was ∞)0, 2, 5, ∞
    2B→D: D = 40, 2, 5, 4
    3Nothing changes, so stop early0, 2, 5, 4

    Floyd-Warshall — 4 nodes, what each k changes

    Middle node kUpdates made: d[i][j] old → new
    StartEdges: 1→2 (4), 1→3 (11), 2→3 (2), 3→1 (3), 3→4 (1), 4→2 (−1)
    k = 1d[3][2]: ∞ → 7 (3→1→2)
    k = 2d[1][3]: 11 → 6 · d[4][3]: ∞ → 1
    k = 3d[1][4]: ∞ → 7 · d[2][1]: ∞ → 5 · d[2][4]: ∞ → 3 · d[4][1]: ∞ → 4
    k = 4d[3][2]: 7 → 0 (3→4→2)

    Section 08

    Python Implementation

    1. Dijkstra (heapq)

    import heapq
    
    graph = {
        'A': [('B', 4), ('C', 1)],
        'B': [('D', 1)],
        'C': [('B', 2), ('D', 5)],
        'D': [],
    }
    
    def dijkstra(graph, src):
        dist = {n: float('inf') for n in graph}
        prev = {n: None for n in graph}
        dist[src] = 0
        pq = [(0, src)]
        done = set()
        while pq:
            d, u = heapq.heappop(pq)      # smallest distance first
            if u in done:
                continue                    # stale entry
            done.add(u)
            for v, w in graph[u]:
                if d + w < dist[v]:          # relax
                    dist[v] = d + w
                    prev[v] = u
                    heapq.heappush(pq, (dist[v], v))
        return dist, prev
    
    def path(prev, t):
        p = []
        while t is not None:
            p.append(t)
            t = prev[t]
        return p[::-1]
    
    dist, prev = dijkstra(graph, 'A')
    print("Distances:", dist)
    print("Path A->D:", " -> ".join(path(prev, 'D')))
    OUTPUT
    Distances: {'A': 0, 'B': 3, 'C': 1, 'D': 4} Path A->D: A -> C -> B -> D

    2. Bellman-Ford (with negative cycle check)

    def bellman_ford(nodes, edges, src):
        dist = {n: float('inf') for n in nodes}
        dist[src] = 0
        for i in range(len(nodes) - 1):     # V - 1 passes
            changed = False
            for u, v, w in edges:
                if dist[u] + w < dist[v]:
                    dist[v] = dist[u] + w
                    changed = True
            print(f"Pass {i+1}: {dist}")
            if not changed:
                break                            # early stop
        for u, v, w in edges:                   # one extra check
            if dist[u] + w < dist[v]:
                raise ValueError("Negative cycle found!")
        return dist
    
    nodes = ['A', 'B', 'C', 'D']
    edges = [('B', 'D', 2), ('A', 'B', 4), ('A', 'C', 5), ('C', 'B', -3)]
    print("Final:", bellman_ford(nodes, edges, 'A'))
    
    try:
        bellman_ford(['X', 'Y', 'Z'],
                     [('X', 'Y', 1), ('Y', 'Z', -2), ('Z', 'Y', 1)], 'X')
    except ValueError as e:
        print("Error:", e)
    OUTPUT
    Pass 1: {'A': 0, 'B': 2, 'C': 5, 'D': inf} Pass 2: {'A': 0, 'B': 2, 'C': 5, 'D': 4} Pass 3: {'A': 0, 'B': 2, 'C': 5, 'D': 4} Final: {'A': 0, 'B': 2, 'C': 5, 'D': 4} Pass 1: {'X': 0, 'Y': 0, 'Z': -1} Pass 2: {'X': 0, 'Y': -1, 'Z': -2} Error: Negative cycle found!

    3. Floyd-Warshall

    INF = float('inf')
    
    def floyd_warshall(n, edges):
        d = [[0 if i == j else INF for j in range(n)] for i in range(n)]
        for u, v, w in edges:
            d[u][v] = min(d[u][v], w)
        for k in range(n):          # middle node
            for i in range(n):
                for j in range(n):
                    if d[i][k] + d[k][j] < d[i][j]:
                        d[i][j] = d[i][k] + d[k][j]
        return d                        # d[i][i] < 0 means negative cycle
    
    # nodes 1..4 stored as 0..3
    edges = [(0,1,4), (1,2,2), (0,2,11), (2,0,3), (2,3,1), (3,1,-1)]
    for row in floyd_warshall(4, edges):
        print(row)
    OUTPUT
    [0, 4, 6, 7] [5, 0, 2, 3] [3, 0, 0, 1] [4, -1, 1, 0]

    4. Johnson's Algorithm

    # uses bellman_ford (without prints) and dijkstra from above
    def johnson(nodes, edges):
        q = '__q__'                                   # new helper node
        extra = [(q, v, 0) for v in nodes]
        h = bellman_ford(nodes + [q], edges + extra, q)  # step 1: potentials
        g = {u: [] for u in nodes}
        for u, v, w in edges:
            g[u].append((v, w + h[u] - h[v]))         # step 2: new weight >= 0
        result = {}
        for u in nodes:                              # step 3: Dijkstra from each
            d, _ = dijkstra(g, u)
            result[u] = {v: d[v] - h[u] + h[v] for v in nodes}
        return h, result
    
    h, res = johnson(nodes, edges)    # same graph as Bellman-Ford
    print("h:", {k: v for k, v in h.items() if k != '__q__'})
    for u in nodes:
        print(u, res[u])
    OUTPUT
    h: {'A': 0, 'B': -3, 'C': 0, 'D': -1} A {'A': 0, 'B': 2, 'C': 5, 'D': 4} B {'A': inf, 'B': 0, 'C': inf, 'D': 2} C {'A': inf, 'B': -3, 'C': 0, 'D': -1} D {'A': inf, 'B': inf, 'C': inf, 'D': 0}
    🧠
    Why Reweighting Keeps Paths Correct

    New weight: w'(u,v) = w + h[u] − h[v]. Add it along a path s → a → b → t. The middle h values cancel. You get real cost + h[s] − h[t]. Every path from s to t shifts by the same amount, so the shortest one stays the shortest. Bellman-Ford makes sure every w' is 0 or more, so Dijkstra is safe.

    5. A* Search

    import heapq, math
    
    pos = {'S': (0, 0), 'A': (2, 1), 'B': (1, 3), 'C': (4, 2), 'G': (5, 4)}
    graph = {'S': [('A', 3), ('B', 4)], 'A': [('C', 3), ('B', 3)],
             'B': [('G', 6)], 'C': [('G', 3)], 'G': []}
    
    def h(n, goal):                         # straight-line guess
        (x1, y1), (x2, y2) = pos[n], pos[goal]
        return math.hypot(x1 - x2, y1 - y2)
    
    def a_star(graph, start, goal):
        g = {start: 0}
        prev = {start: None}
        open_pq = [(h(start, goal), start)]   # (f, node)
        closed, expanded = set(), []
        while open_pq:
            f, u = heapq.heappop(open_pq)
            if u in closed:
                continue
            expanded.append(u)
            if u == goal:                      # stop at the goal
                return g[u], path(prev, goal), expanded
            closed.add(u)
            for v, w in graph[u]:
                if g[u] + w < g.get(v, float('inf')):
                    g[v] = g[u] + w
                    prev[v] = u
                    heapq.heappush(open_pq, (g[v] + h(v, goal), v))
        return float('inf'), [], expanded
    
    print(a_star(graph, 'S', 'G'))
    OUTPUT — (cost, path, nodes expanded)
    (9, ['S', 'A', 'C', 'G'], ['S', 'A', 'B', 'C', 'G'])

    6. 0-1 BFS (deque)

    from collections import deque
    
    graph = {0: [(1, 0), (2, 1)], 1: [(3, 1)], 2: [(3, 0)], 3: [(4, 1)], 4: []}
    
    def zero_one_bfs(graph, src):
        dist = {n: float('inf') for n in graph}
        dist[src] = 0
        dq = deque([src])
        while dq:
            u = dq.popleft()
            for v, w in graph[u]:
                if dist[u] + w < dist[v]:
                    dist[v] = dist[u] + w
                    if w == 0:
                        dq.appendleft(v)   # same distance: FRONT
                    else:
                        dq.append(v)       # one more: BACK
        return dist
    
    print(zero_one_bfs(graph, 0))
    OUTPUT
    {0: 0, 1: 0, 2: 1, 3: 1, 4: 2}

    Section 09

    Time and Space Complexity

    V = number of nodes. E = number of edges.

    AlgorithmProblemTimeNegative WeightsNeg. Cycle Check
    Dijkstra (binary heap)Single sourceO((V + E) log V)NoNo
    Bellman-FordSingle sourceO(V · E)YesYes
    Floyd-WarshallAll pairsO(V³)YesYes (d[i][i] < 0)
    Johnson'sAll pairsO(V · E log V)YesYes (via Bellman-Ford)
    A*One source, one targetDepends on h. Worst case = DijkstraNoNo
    0-1 BFSSingle sourceO(V + E)Only 0 and 1Not needed
    📊
    Floyd-Warshall or Johnson's?

    In a dense graph, E is close to V². Then Johnson's costs about V³ log V, and Floyd-Warshall's simple V³ wins. In a sparse graph (like a road map), E is close to V. Johnson's then costs about V² log V, which is much faster.


    Section 10

    Which Algorithm Should I Use? — Interactive Chooser

    Answer the questions. The chooser picks the best algorithm for your problem.

    1. What do you need?
    2. What kind of edge weights?
    3. Do you have one target and a good distance guess (like map coordinates)?

    Section 11

    Where These Algorithms Are Used

    🗺️
    Maps and GPS
    Dijkstra and A* find the fastest route. Real map apps add speed-ups on top of them.
    Dijkstra · A*
    🎮
    Game Characters
    Enemies and units find their way around walls on a grid using A*.
    A*
    🌐
    Internet Routing
    OSPF routers run Dijkstra. RIP uses the distance-vector idea behind Bellman-Ford.
    Dijkstra · Bellman-Ford
    💱
    Currency Arbitrage
    Use weight = −log(rate). A negative cycle means a loop of trades that makes money.
    Bellman-Ford
    🚚
    Distance Tables
    A courier firm needs travel time between every pair of warehouses.
    Floyd-Warshall · Johnson's
    🧱
    Grid Puzzles
    "Move for free in the arrow's direction, pay 1 to change it." That is a classic 0-1 BFS problem.
    0-1 BFS

    Section 12

    Golden Rules

    🏆 Shortest Paths — Rules to Remember
    1
    Every algorithm is built on relaxation: if dist[u] + w < dist[v], update dist[v]. Learn this line well.
    2
    Never use Dijkstra or A* with negative weights. A settled node may later get a cheaper path, and Dijkstra will miss it.
    3
    Bellman-Ford needs V − 1 passes. If the V-th pass still changes something, there is a negative cycle.
    4
    For all pairs: Floyd-Warshall for small or dense graphs. Johnson's for large, sparse graphs.
    5
    An A* heuristic must never overestimate the real remaining cost. If it does, A* can return a longer path.
    6
    If all weights are equal, plain BFS is enough. If they are only 0 or 1, use 0-1 BFS. Both beat Dijkstra.
    7
    Keep a prev array. Distances tell you the cost. Only prev lets you rebuild the actual path.
    8
    In lazy Dijkstra, a node can sit in the heap many times. Skip stale entries when you pop them.