AlgoVerse

Johnson's algorithm

All-pairs shortest paths with negative edges: Bellman–Ford makes the weights non-negative, then Dijkstra runs from every node.

Dijkstra from every node gives all-pairs shortest paths in O(V · E log V); for sparse graphs that beats Floyd–Warshall (V³). But Dijkstra fails with negative edges. Johnson changes the weights so none is negative while the shortest paths stay the same.

Read the full explanation ↓
Input:
V=5 · E=9 · src=1PDF export

The same graph as Floyd–Warshall, with two negative edges; the potentials come out as h = (0, −1, −5, 0, −4) and the answer matches Floyd–Warshall.

  1. import heapq
  2. INF = float("inf")
  3. def johnson(graph, src):
  4. # 1. Bellman-Ford from a new node q with a 0-edge to every node.
  5. h = {v: 0 for v in graph}
  6. for round_no in range(len(graph)):
  7. changed = False
  8. for u in graph:
  9. for v, w in graph[u]:
  10. if h[u] + w < h[v]:
  11. h[v] = h[u] + w
  12. changed = True
  13. if not changed:
  14. break
  15. for u in graph:
  16. for v, w in graph[u]:
  17. if h[u] + w < h[v]:
  18. return None
  19. # 2. Dijkstra from every node on the reweighted edges.
  20. dist = {}
  21. for s in graph:
  22. d = dijkstra(graph, h, s)
  23. dist[s] = {v: d[v] - h[s] + h[v] for v in graph}
  24. return dist
  25. def dijkstra(graph, h, s):
  26. d = {v: INF for v in graph}
  27. d[s] = 0
  28. heap = [(0, s)]
  29. while heap:
  30. du, u = heapq.heappop(heap)
  31. if du > d[u]:
  32. continue
  33. for v, w in graph[u]:
  34. nd = du + w + h[u] - h[v]
  35. if nd < d[v]:
  36. d[v] = nd
  37. heapq.heappush(heap, (nd, v))
  38. return d

Invariant: After step 1, every edge has h[u] + w ≥ h[v], so the new weight w + h[u] − h[v] is never negative and Dijkstra works.

Start

38-41742-5612345
  • Current node (u)
  • Shortest-path tree so far
  • Edge being checked

Local variables

src'1'

Orange = changed in this step

graph dict
  • '1': [('2', 3), ('3', 8), ('5', -4)]
  • '2': [('4', 1), ('5', 7)]
  • '3': [('2', 4)]
  • '4': [('1', 2), ('3', -5)]
  • '5': [('4', 6)]

Call stack

  1. johnson()line 6
Step 1 of 487
Black = bf_round, row_done · Orange = key stepbf_round: 0/3 · potential: 0/3 · pop: 0/30 · reweight: 0/45 · relax: 0/25 · row_done: 0/5

    How the work grows with input size

    Average of 5 random graphs per n (fixed seeds). Hollow marker = the input on screen.

    • reweight
    • current input (n=5)
    02040604812n = number of nodesreweight
    Show as a table
    nreweight
    48.4
    838
    1254.8
    Explanation

    The idea

    Dijkstra from every node gives all-pairs shortest paths in O(V · E log V); for sparse graphs that beats Floyd–Warshall (V³). But Dijkstra fails with negative edges. Johnson changes the weights so none is negative while the shortest paths stay the same.

    Reweighting

    Give every node a potential h[v] and change each edge's weight to:

    w'(u, v) = w(u, v) + h[u] − h[v]
    

    The length of every path from s to t changes by the same constant h[s] − h[t], so the order of the paths does not change.

    Where do the potentials come from?

    Add a new node q with a 0-weight edge to everyone and run Bellman–Ford from q: h[v] = δ(q, v). By the triangle inequality h[v] ≤ h[u] + w, so w' ≥ 0. If Bellman–Ford finds a negative cycle, there is no answer.

    What to watch in the trace

    • First the potentials (h in the memory panel) are built; then Dijkstra runs from every node, and the numbers by the nodes are Dijkstra's distances in the new weights.