⚙️AlgorithmAdvanced

Johnson's Algorithm

Key Points

  • •
    Johnson's Algorithm computes all-pairs shortest paths on sparse graphs by first removing negative edges via reweighting, then running Dijkstra from every vertex.
  • •
    It runs Bellman–Ford once from a super-source to get a potential function that guarantees all reweighted edges are non-negative.
  • •
    After reweighting, Dijkstra's Algorithm can be safely used V times, once per source, to obtain all-pairs distances efficiently.
  • •
    The final distances are corrected back to the original weights using the same potentials, ensuring exact equivalence to original shortest paths.
  • •
    Time complexity is O(V E + V) with Fibonacci heaps and O(V E V) with binary heaps, typically beating Floyd–Warshall on sparse graphs.
  • •
    If Bellman–Ford detects a negative cycle, Johnson's Algorithm reports failure because distances are undefined.
  • •
    The reweighting uses h(v) = distance from the super-source to v, and w'(u,v) = w(u,v) + h(u) - h(v).
  • •
    Use Johnson when E , edges may be negative, and you need all-pairs distances.

Prerequisites

  • →Graph representations (adjacency lists and matrices) — Johnson relies on efficient edge iteration and memory layout, especially for sparse graphs.
  • →Single-source shortest paths (Dijkstra) — Johnson runs Dijkstra from each vertex after reweighting to non-negative edges.
  • →Bellman–Ford algorithm — Potentials are computed using Bellman–Ford; understanding its relaxation and negative cycle detection is essential.
  • →Priority queues and heaps — They determine Dijkstra’s time complexity and practical performance.
  • →Asymptotic analysis (Big-O) — To compare Johnson’s complexity with Floyd–Warshall and understand sparsity benefits.
  • →Shortest path properties (triangle inequality, path optimality) — These justify the reweighting’s non-negativity and correctness proofs.

Detailed Explanation

Tap terms for definitions

01Overview

Johnson's Algorithm is a technique to compute shortest paths between every pair of vertices in a weighted directed graph, even when some edge weights are negative, as long as there is no negative cycle. The idea is to transform (reweight) the graph once so that all edges become non-negative, and then run Dijkstra's Algorithm from each vertex. The reweighting does not change the relative costs of any path, so the true shortest path distances can be recovered by undoing the transformation. The algorithm proceeds in two main stages. First, it adds a new super-source connected to every vertex with a zero-weight edge and runs Bellman–Ford to compute a potential h(v) for each vertex v. These potentials satisfy a triangle-inequality-like property that guarantees non-negative reweighted edges. Second, it defines new weights w'(u,v) = w(u,v) + h(u) - h(v) and runs Dijkstra from each vertex on the reweighted graph to get distances d'(. , .). Finally, it converts d' back to original distances with d(u,v) = d'(u,v) - h(u) + h(v). If Bellman–Ford finds a negative cycle, the algorithm reports that shortest paths are undefined. Johnson's Algorithm is especially efficient on sparse graphs, where E is much smaller than V^2. Compared to Floyd–Warshall's O(V^3) time, Johnson's typically achieves O(V E \log V) with binary heaps, or O(V E + V^2 \log V) with Fibonacci heaps.

02Intuition & Analogies

Imagine a city where roads can go uphill or downhill, and travel times reflect slopes: uphill (positive) slows you, downhill (negative) speeds you. Dijkstra refuses to work with downhill shortcuts that are too generous (negative edges) because it might pick a seemingly short route and then discover an even shorter one later, breaking its greedy promise. Johnson’s trick is to assign an altitude (potential) to each intersection so that, after a coordinate change, every road looks flat or uphill—never downhill. How? We build an artificial vantage point (a super-source) and compute each intersection’s altitude h(v) as the shortest distance from this vantage point to v using Bellman–Ford (which can handle negative slopes safely). This altitude assignment ensures that when we measure the perceived slope of a road from u to v as w'(u,v) = w(u,v) + h(u) - h(v), no road appears downhill anymore. Intuitively, we 'credit' uphill starts and 'debit' downhill finishes so the net effect makes all edges non-negative. Once all roads look non-negative, Dijkstra can be used reliably to find the fastest route from any starting point. But we still care about real travel times in the original city. Fortunately, this altitude change is just a perspective shift: when we convert the Dijkstra distances back by subtracting the altitude difference, we recover the true times. If there is a negative cycle—a loop whose total downhill gain beats all uphill losses—then no consistent altitude assignment can fix it, and travel times become undefined (you could loop forever to reduce time), so the algorithm must stop.

03Formal Definition

Given a directed graph G = (V, E) with weight function w: E , Johnson's Algorithm computes all-pairs shortest paths if G contains no negative-weight cycles. Construct G' by adding a super-source s and edges (s, v) with weight 0 for all v V. Run Bellman–Ford from s to compute h(v) = (s, v), the shortest-path potential for each vertex v, or detect a negative cycle reachable from s. Define the reweighted edge function w': E by w'(u, v) = w(u, v) + h(u) - h(v). For any path p = , , , , its reweighted length satisfies w'(p) = w(p) + h() - h(). In particular, for every edge (u, v) E, Bellman–Ford’s optimality implies h(v) h(u) + w(u, v) so w'(u, v) 0. Run Dijkstra's Algorithm from each source u V on the non-negative graph (V, E, w') to obtain d'_u(v), the shortest reweighted distances. Then convert to original distances by (v) = d'_u(v) - h(u) + h(v). Correctness follows because reweighting preserves the order of path lengths between any fixed endpoints; therefore, shortest paths in w correspond to shortest paths in w'. If Bellman–Ford reports a negative cycle, then some pair distances are undefined (unbounded below), and Johnson's Algorithm halts with failure.

04When to Use

Use Johnson's Algorithm when you need all-pairs shortest paths on a directed graph that may include negative edges but has no negative cycles, and the graph is sparse (E is much smaller than V^2). Typical scenarios include routing in large networks, dependency analysis with small negative adjustments, and pathfinding where penalties or credits are modeled as negative weights. It is particularly advantageous when you must run many single-source queries on the same graph. Instead of re-running Bellman–Ford for each source, you pay for Bellman–Ford once to eliminate negatives and then run the faster Dijkstra repeatedly. If you have heavy-tailed degree distributions (few hubs, many leaves), adjacency lists and Johnson perform well. If your graph is dense (E \approx V^2) or you only need distances for a few sources, consider alternatives: Floyd–Warshall is simpler and competitive for very dense graphs or small V, and plain Bellman–Ford or Dijkstra may suffice for single-source tasks. If any negative cycle exists, no algorithm can produce finite all-pairs distances; you must detect or handle cycles first.

⚠️Common Mistakes

Common pitfalls include:

  • Forgetting to add the super-source with zero-weight edges before running Bellman–Ford. Without this, the potentials h(v) may not reflect the correct lower bounds across all vertices, breaking the non-negativity guarantee.
  • Miscomputing the reweighting: using w'(u, v) = w(u, v) + h(v) - h(u) (reversed sign) will generally produce negative edges and invalidate Dijkstra. The correct formula is w'(u, v) = w(u, v) + h(u) - h(v).
  • Not converting distances back: reporting d'_u(v) instead of d_u(v) = d'_u(v) - h(u) + h(v) gives wrong answers in the original metric. Always undo the potential shift.
  • Using types that overflow. Path sums can exceed 32-bit limits; prefer 64-bit integers (long long) or double if rational/real weights are needed. Guard INF carefully to avoid wrap-around.
  • Running Dijkstra on graphs that still have negative edges (e.g., because Bellman–Ford failed or reweighting was skipped). Dijkstra’s correctness depends on non-negative edges.
  • Ignoring negative cycles. If Bellman–Ford detects further relaxation on the V-th iteration, the graph has a negative cycle reachable from the super-source; Johnson must abort and report failure.
  • Inefficient data structures. With a binary heap priority queue, the runtime is O(V E \log V); with Fibonacci heaps, it is O(V E + V^2 \log V). Choosing adjacency matrices for sparse graphs causes unnecessary O(V^2) memory/time overhead.

Key Formulas

Reweighting Rule

Explanation: This transforms each edge weight using vertex potentials. It preserves the order of path lengths but eliminates negative edge weights if h comes from shortest-path distances.

Potential from Super-source

Explanation: The potential at v is the shortest distance from the super-source s to v computed by Bellman–Ford. These values satisfy a triangle inequality necessary for non-negativity.

Non-negativity Guarantee

Explanation: Bellman–Ford ensures h(v) is no more than the cost of reaching v via u, so rearranging yields non-negative reweighted edges.

Path-Length Preservation

Explanation: Telescoping the potentials along a path shows all reweighted path lengths differ from original by the same constant h(s) - h(t), preserving which path is shortest.

Distance Conversion (forward)

Explanation: Shortest reweighted distances equal original distances plus a source-target dependent constant. This justifies running Dijkstra on w'.

Distance Conversion (back)

Explanation: To recover original distances from reweighted results, subtract the source potential and add the target potential.

Johnson Complexity with Fibonacci Heaps

Explanation: Bellman–Ford is O(VE). Each Dijkstra run is O(E + V V) with a Fibonacci heap, and we run it V times.

Johnson Complexity with Binary Heaps

Explanation: Using a binary heap, each Dijkstra is O(E V); running it V times gives O(VE V) overall (plus O(VE) for Bellman–Ford).

Floyd–Warshall Complexity

Explanation: The dynamic programming triple loop evaluates all intermediate vertices for all ordered pairs, taking cubic time.

Bellman–Ford Negative Cycle Behavior

Explanation: If an edge can still be relaxed on the V-th iteration, some distances are unbounded below due to a negative cycle.

Complexity Analysis

Let V = and E = . Johnson’s Algorithm has two stages. Stage 1 runs Bellman–Ford once on the augmented graph with a super-source and zero-weight edges to all vertices, which costs O(VE). This step either returns a shortest-path potential h(v) for every vertex or detects a negative cycle, in which case the algorithm halts. Stage 2 transforms edge weights with w'(u,v) = w(u,v) + h(u) - h(v), after which all weights are non-negative. We then run Dijkstra’s Algorithm from each vertex u. With a binary heap priority queue (std::priorit), each Dijkstra run costs O(E log V), giving O(VE log V) across all sources. Adding the O(VE) from Bellman–Ford does not change the asymptotic bound. If we implement Dijkstra with a Fibonacci heap or another decrease-key-friendly structure, a single run achieves O(E + V log V). Over all sources this yields O(VE + log V). This bound is typically presented as Johnson’s theoretical complexity. In practice, binary heaps are faster constants and simpler, and on sparse graphs ) the runtime becomes roughly O( log V), which often beats Floyd–Warshall’s O(). Space usage is O(V + E) to store the adjacency lists, plus O(V) for distances, potentials, and the priority queue per run. If we store the full all-pairs distance matrix, that adds O() space for the output, which may dominate memory on large graphs.

Code Examples

Complete Johnson's Algorithm (detects negative cycles, returns APSP)
1#include <bits/stdc++.h>
2using namespace std;
3
4// Johnson's Algorithm implementation using adjacency lists and binary-heap Dijkstra.
5// - Handles negative edges, detects negative cycles.
6// - Returns all-pairs shortest path matrix or reports failure.
7// Graph is 0-indexed.
8
9struct Edge {
10 int u, v; // directed edge u -> v
11 long long w; // weight (can be negative)
12};
13
14const long long INF = (1LL<<60);
15
16// Bellman-Ford from a super-source s that has edges (s -> v) of weight 0 for all v.
17// We implement it by initializing dist[v] = 0 for all v and relaxing all real edges V-1 times.
18bool bellman_ford_potentials(int n, const vector<Edge>& edges, vector<long long>& h) {
19 h.assign(n, 0LL); // start as if super-source connects to all with 0; this is equivalent
20
21 // Relax edges n-1 times
22 for (int i = 0; i < n - 1; ++i) {
23 bool changed = false;
24 for (const auto& e : edges) {
25 if (h[e.u] != INF && h[e.v] > h[e.u] + e.w) {
26 h[e.v] = h[e.u] + e.w;
27 changed = true;
28 }
29 }
30 if (!changed) break; // early stop if no change
31 }
32
33 // Check for negative cycle: any further improvement implies a reachable negative cycle
34 for (const auto& e : edges) {
35 if (h[e.u] != INF && h[e.v] > h[e.u] + e.w) {
36 return false; // negative cycle detected
37 }
38 }
39 return true;
40}
41
42// Dijkstra on reweighted graph: w'(u,v) = w(u,v) + h[u] - h[v] (non-negative)
43vector<long long> dijkstra_reweighted(int n, const vector<vector<pair<int,long long>>>& adj, int src) {
44 vector<long long> dist(n, INF);
45 dist[src] = 0;
46 using P = pair<long long,int>; // (distance, node)
47 priority_queue<P, vector<P>, greater<P>> pq;
48 pq.push({0, src});
49
50 while (!pq.empty()) {
51 auto [d, u] = pq.top(); pq.pop();
52 if (d != dist[u]) continue; // stale entry
53 for (auto [v, wprime] : adj[u]) {
54 if (dist[v] > d + wprime) {
55 dist[v] = d + wprime;
56 pq.push({dist[v], v});
57 }
58 }
59 }
60 return dist;
61}
62
63int main() {
64 ios::sync_with_stdio(false);
65 cin.tie(nullptr);
66
67 int n, m;
68 // Input format:
69 // n m
70 // m lines: u v w (0-indexed)
71 if (!(cin >> n >> m)) {
72 cerr << "Provide n, m, then m edges (u v w).\n";
73 return 0;
74 }
75 vector<Edge> edges;
76 edges.reserve(m);
77 for (int i = 0; i < m; ++i) {
78 int u, v; long long w; cin >> u >> v >> w;
79 edges.push_back({u, v, w});
80 }
81
82 // 1) Compute potentials h via Bellman-Ford from implicit super-source
83 vector<long long> h;
84 if (!bellman_ford_potentials(n, edges, h)) {
85 cout << "NEGATIVE CYCLE\n";
86 return 0;
87 }
88
89 // 2) Build reweighted adjacency list with non-negative weights
90 vector<vector<pair<int,long long>>> adj(n);
91 adj.assign(n, {});
92 for (const auto& e : edges) {
93 long long wprime = e.w + h[e.u] - h[e.v];
94 // Guard: due to correctness, wprime >= 0 if no negative cycles exist
95 adj[e.u].push_back({e.v, wprime});
96 }
97
98 // 3) Run Dijkstra from each source and convert back to original distances
99 vector<vector<long long>> all(n, vector<long long>(n, INF));
100 for (int s = 0; s < n; ++s) {
101 vector<long long> dprime = dijkstra_reweighted(n, adj, s);
102 for (int v = 0; v < n; ++v) {
103 if (dprime[v] < INF/2) {
104 // Convert back: d(s,v) = d'(s,v) - h(s) + h(v)
105 all[s][v] = dprime[v] - h[s] + h[v];
106 }
107 }
108 }
109
110 // 4) Output matrix: INF for unreachable pairs
111 for (int i = 0; i < n; ++i) {
112 for (int j = 0; j < n; ++j) {
113 if (all[i][j] >= INF/2) cout << "INF";
114 else cout << all[i][j];
115 if (j + 1 < n) cout << ' ';
116 }
117 cout << '\n';
118 }
119 return 0;
120}
121

This program reads a directed weighted graph, computes vertex potentials using Bellman–Ford, reweights edges to be non-negative, runs Dijkstra from every vertex, and converts distances back to the original metric. If a negative cycle is detected, it reports failure. The adjacency list is built once for the reweighted graph to avoid recomputing w'.

Time: O(VE log V) with the binary heap priority queue (plus O(VE) for Bellman–Ford).Space: O(V + E) for the graph and O(V^2) for the output matrix.
Verify Reweighting Correctness on a Small Graph
1#include <bits/stdc++.h>
2using namespace std;
3
4struct Edge { int u, v; long long w; };
5const long long INF = (1LL<<60);
6
7bool bellman_ford_potentials(int n, const vector<Edge>& edges, vector<long long>& h) {
8 h.assign(n, 0LL);
9 for (int i = 0; i < n - 1; ++i) {
10 bool changed = false;
11 for (auto &e : edges) {
12 if (h[e.v] > h[e.u] + e.w) { h[e.v] = h[e.u] + e.w; changed = true; }
13 }
14 if (!changed) break;
15 }
16 for (auto &e : edges) if (h[e.v] > h[e.u] + e.w) return false; // negative cycle
17 return true;
18}
19
20vector<long long> dijkstra_reweighted(int n, const vector<vector<pair<int,long long>>>& adj, int s) {
21 vector<long long> d(n, INF); d[s] = 0;
22 using P = pair<long long,int>;
23 priority_queue<P, vector<P>, greater<P>> pq; pq.push({0,s});
24 while(!pq.empty()){
25 auto [du,u] = pq.top(); pq.pop(); if (du != d[u]) continue;
26 for (auto [v,w]: adj[u]) if (d[v] > du + w) { d[v] = du + w; pq.push({d[v], v}); }
27 }
28 return d;
29}
30
31int main(){
32 // Build a small graph with negative edges but no negative cycles
33 // 0->1 (-2), 0->2 (4), 1->2 (3), 1->3 (2), 2->3 (-1)
34 int n = 4; vector<Edge> edges = {
35 {0,1,-2}, {0,2,4}, {1,2,3}, {1,3,2}, {2,3,-1}
36 };
37
38 vector<long long> h;
39 if (!bellman_ford_potentials(n, edges, h)) {
40 cout << "NEGATIVE CYCLE\n"; return 0;
41 }
42
43 // Build reweighted graph
44 vector<vector<pair<int,long long>>> adj(n);
45 for (auto &e : edges) {
46 long long wprime = e.w + h[e.u] - h[e.v];
47 adj[e.u].push_back({e.v, wprime});
48 }
49
50 // Run Dijkstra from source s = 0 on reweighted graph
51 int s = 0; auto dprime = dijkstra_reweighted(n, adj, s);
52
53 // Convert back to original distances and print both
54 cout << fixed;
55 for (int v = 0; v < n; ++v) {
56 if (dprime[v] >= INF/2) {
57 cout << "v=" << v << ": unreachable\n";
58 } else {
59 long long d_orig = dprime[v] - h[s] + h[v];
60 cout << "v=" << v << ": d'(reweighted)=" << dprime[v]
61 << ", d(original)=" << d_orig << "\n";
62 }
63 }
64 return 0;
65}
66

This program constructs a small graph with negative edges but no negative cycles, computes potentials, reweights edges, runs a single Dijkstra from vertex 0, and converts results back. The printed pairs d'(0,v) and d(0,v) illustrate the formula d(0,v) = d'(0,v) - h(0) + h(v), confirming correctness of reweighting.

Time: O(E log V) for the single Dijkstra run, plus O(VE) for the initial Bellman–Ford.Space: O(V + E).
Choosing Between Johnson and Floyd–Warshall Based on Density
1#include <bits/stdc++.h>
2using namespace std;
3
4struct Edge { int u, v; long long w; };
5const long long INF = (1LL<<60);
6
7bool bellman_ford_potentials(int n, const vector<Edge>& edges, vector<long long>& h) {
8 h.assign(n, 0LL);
9 for (int i = 0; i < n - 1; ++i) {
10 bool changed = false;
11 for (auto &e : edges) {
12 if (h[e.v] > h[e.u] + e.w) { h[e.v] = h[e.u] + e.w; changed = true; }
13 }
14 if (!changed) break;
15 }
16 for (auto &e : edges) if (h[e.v] > h[e.u] + e.w) return false;
17 return true;
18}
19
20vector<vector<long long>> floyd_warshall(int n, const vector<Edge>& edges) {
21 vector<vector<long long>> dist(n, vector<long long>(n, INF));
22 for (int i = 0; i < n; ++i) dist[i][i] = 0;
23 for (auto &e: edges) dist[e.u][e.v] = min(dist[e.u][e.v], e.w);
24 for (int k = 0; k < n; ++k)
25 for (int i = 0; i < n; ++i) if (dist[i][k] < INF/2)
26 for (int j = 0; j < n; ++j) if (dist[k][j] < INF/2)
27 dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j]);
28 return dist;
29}
30
31vector<vector<long long>> johnson(int n, const vector<Edge>& edges) {
32 vector<long long> h;
33 if (!bellman_ford_potentials(n, edges, h)) return {}; // empty => negative cycle
34 vector<vector<pair<int,long long>>> adj(n);
35 for (auto &e : edges) {
36 long long wprime = e.w + h[e.u] - h[e.v];
37 adj[e.u].push_back({e.v, wprime});
38 }
39 const long long INFLL = INF;
40 vector<vector<long long>> ans(n, vector<long long>(n, INFLL));
41 using P = pair<long long,int>;
42 for (int s = 0; s < n; ++s) {
43 vector<long long> d(n, INFLL); d[s] = 0;
44 priority_queue<P, vector<P>, greater<P>> pq; pq.push({0, s});
45 while(!pq.empty()) {
46 auto [du,u] = pq.top(); pq.pop(); if (du != d[u]) continue;
47 for (auto [v,w]: adj[u]) if (d[v] > du + w) { d[v] = du + w; pq.push({d[v], v}); }
48 }
49 for (int v = 0; v < n; ++v) if (d[v] < INFLL/2) ans[s][v] = d[v] - h[s] + h[v];
50 }
51 return ans;
52}
53
54int main(){
55 ios::sync_with_stdio(false);
56 cin.tie(nullptr);
57
58 int n, m; cin >> n >> m;
59 vector<Edge> edges(m);
60 for (int i = 0; i < m; ++i) cin >> edges[i].u >> edges[i].v >> edges[i].w;
61
62 // Heuristic: if m > n * (n / 8), treat as dense, else sparse.
63 // Tune threshold empirically; here we pick 12.5% density as a demo cutoff.
64 long long threshold = 1LL * n * max(1, n / 8);
65 if ((long long)m > threshold) {
66 auto dist = floyd_warshall(n, edges);
67 // Output
68 for (int i = 0; i < n; ++i) {
69 for (int j = 0; j < n; ++j) {
70 if (dist[i][j] >= INF/2) cout << "INF"; else cout << dist[i][j];
71 if (j+1<n) cout << ' ';
72 }
73 cout << '\n';
74 }
75 } else {
76 auto dist = johnson(n, edges);
77 if (dist.empty()) { cout << "NEGATIVE CYCLE\n"; return 0; }
78 for (int i = 0; i < n; ++i) {
79 for (int j = 0; j < n; ++j) {
80 if (dist[i][j] >= INF/2) cout << "INF"; else cout << dist[i][j];
81 if (j+1<n) cout << ' ';
82 }
83 cout << '\n';
84 }
85 }
86 return 0;
87}
88

This program includes both Johnson and Floyd–Warshall. It chooses a method based on a simple density heuristic. This demonstrates how to deploy Johnson for sparse graphs and fall back to Floyd–Warshall when the graph is dense or potentials are unnecessary.

Time: Johnson path: O(VE log V); Floyd–Warshall path: O(V^3).Space: Johnson path: O(V + E) plus O(V^2) for output; Floyd–Warshall: O(V^2).