What is Johnson's Algorithm?
Johnson’s Algorithm is an efficient way to find the shortest paths between all pairs of vertices in a sparse, directed, weighted graph. It handles negative edge weights (but not negative weight cycles) by using the Bellman-Ford algorithm to mathematically reweight all edges to be non-negative. Once all edges are non-negative, it runs Dijkstra’s algorithm from every vertex to find the shortest paths, achieving much better performance on sparse graphs than the Floyd-Warshall algorithm.
Explanation
- The Core Problem: Dijkstra’s algorithm is fast ( per vertex) but fails on negative weights. Bellman-Ford handles negative weights but is slow ( per vertex). If we want all-pairs shortest paths on a sparse graph with negative weights, running Bellman-Ford from every node takes , which is too slow.
- The Solution: Johnson’s algorithm cleverly transforms the graph so that all edge weights become non-negative without altering the relative shortest paths between nodes. Then, it safely applies Dijkstra’s algorithm from every vertex.
The Reweighting Technique (Telescoping Sum)
- Suppose we assign a potential or weight to every vertex .
- We define a new edge weight:
- Any path from to will have its new weight calculated as:
- Because and are constants for any paths between and , the path that strictly minimizes also strictly minimizes . Thus, shortest paths are preserved!
How to Compute Valid Potentials
- We need .
- This implies .
- This is exactly the triangle inequality maintained by shortest paths!
- Therefore, we can add a dummy vertex with 0-weight edges to all other vertices, run Bellman-Ford from , and use the resulting shortest distances as our potentials .
How It Works
Step-by-Step Execution
-
- Add Dummy Source: Add a new vertex to the graph. Add directed edges of weight 0 from to all other vertices in the graph.
-
- Run Bellman-Ford: Run the Bellman-Ford algorithm starting from .
- If Bellman-Ford detects a negative weight cycle, abort. The shortest paths are undefined.
- Otherwise, record the shortest path distances from to every node as .
-
- Reweight Edges: For every original edge with weight , compute the new non-negative weight:
-
- Run Dijkstra: Remove the dummy vertex . Run Dijkstra’s algorithm using the non-negative weights from every vertex to find all-pairs shortest paths in the reweighted graph.
-
- Restore Distances: Convert the Dijkstra distances back to the original graph distances:
flowchart TD A["Start: Graph G(V, E) with negative weights"] --> B["Add dummy node S\nConnect S to all v with weight 0"] B --> C["Run Bellman-Ford from S\nto find distances h(v)"] C --> D{"Negative Cycle?"} D -- Yes --> E["ABORT\nGraph contains negative cycle"] D -- No --> F["Reweight all edges:\nw'(u,v) = w(u,v) + h(u) - h(v)"] F --> G["Remove S.\nAll w'(u,v) are now ≥ 0"] G --> H["Run Dijkstra from EVERY vertex u\nusing weights w' to find dist'(u, v)"] H --> I["Restore original distances:\ndist(u,v) = dist'(u,v) - h(u) + h(v)"]
Complexity Analysis
| Step | Time Complexity | Space Complexity |
|---|---|---|
| Bellman-Ford | ||
| Reweighting | ||
| V × Dijkstra | (to store all-pairs distances) | |
| Total (Average/Worst) | O(V² log V + VE) | O(V² + E) |
Why not Floyd-Warshall?
- Floyd-Warshall always runs in .
- Johnson’s Algorithm runs in .
- If the graph is dense (), Johnson’s is , making Floyd-Warshall faster due to lower constant factors.
- If the graph is sparse (), Johnson’s is , which is drastically faster than .
Implementation
-
All-Pairs Shortest Path via Johnson's Algorithm
The following implementations output the full shortest-path distance matrix. If a path does not exist, it prints “INF”. If a negative cycle is detected by the initial Bellman-Ford pass, the algorithms terminate early.
import heapq
class Edge:
def __init__(self, dest, weight):
self.dest = dest
self.weight = weight
def bellman_ford(n, edges, start, h):
h[start] = 0
# Relax edges V times
for i in range(n):
for u, v, w in edges:
if h[u] != float('inf') and h[u] + w < h[v]:
h[v] = h[u] + w
# If we can relax on the Nth iteration, there's a negative cycle
if i == n - 1:
return False
return True
def dijkstra(n, adj, start, dist_prime):
pq = [(0, start)]
dist_prime[start] = 0
while pq:
d, u = heapq.heappop(pq)
if d > dist_prime[u]:
continue
for edge in adj[u]:
v = edge.dest
w = edge.weight
if dist_prime[u] + w < dist_prime[v]:
dist_prime[v] = dist_prime[u] + w
heapq.heappush(pq, (dist_prime[v], v))
def johnsons_algorithm(n, edges):
# Add dummy node n connected to all other nodes with weight 0
augmented_edges = edges[:]
for i in range(n):
augmented_edges.append((n, i, 0))
h = [float('inf')] * (n + 1)
if not bellman_ford(n + 1, augmented_edges, n, h):
print("Graph contains a negative weight cycle.")
return None
# Reweight edges: w'(u, v) = w(u, v) + h(u) - h(v)
adj = [[] for _ in range(n)]
for u, v, w in edges:
new_w = w + h[u] - h[v]
adj[u].append(Edge(v, new_w))
# Run Dijkstra from every node
all_pairs_dist = []
for u in range(n):
dist_prime = [float('inf')] * n
dijkstra(n, adj, u, dist_prime)
# Convert distances back
dist = []
for v in range(n):
if dist_prime[v] == float('inf'):
dist.append(float('inf'))
else:
dist.append(dist_prime[v] - h[u] + h[v])
all_pairs_dist.append(dist)
return all_pairs_dist
if __name__ == "__main__":
V = 4
# Edges as (u, v, w)
edges = [
(0, 1, -5),
(0, 2, 2),
(0, 3, 3),
(1, 2, 4),
(2, 3, 1)
]
dists = johnsons_algorithm(V, edges)
if dists:
print("All-pairs shortest paths:")
for i in range(V):
row = ["INF" if d == float('inf') else str(d) for d in dists[i]]
print(f"From {i}: {', '.join(row)}")#include <iostream>
#include <vector>
#include <queue>
#include <climits>
using namespace std;
const int INF = 1e9;
struct Edge {
int src, dest, weight;
};
struct AdjEdge {
int dest, weight;
};
bool bellmanFord(int V, const vector<Edge>& edges, int src, vector<int>& h) {
h.assign(V, INF);
h[src] = 0;
for (int i = 0; i < V; ++i) {
for (const auto& e : edges) {
if (h[e.src] != INF && h[e.src] + e.weight < h[e.dest]) {
h[e.dest] = h[e.src] + e.weight;
if (i == V - 1) return false; // Negative cycle
}
}
}
return true;
}
void dijkstra(int V, const vector<vector<AdjEdge>>& adj, int src, vector<int>& dist) {
dist.assign(V, INF);
dist[src] = 0;
priority_queue<pair<int, int>, vector<pair<int, int>>, greater<pair<int, int>>> pq;
pq.push({0, src});
while (!pq.empty()) {
int d = pq.top().first;
int u = pq.top().second;
pq.pop();
if (d > dist[u]) continue;
for (const auto& e : adj[u]) {
if (dist[u] + e.weight < dist[e.dest]) {
dist[e.dest] = dist[u] + e.weight;
pq.push({dist[e.dest], e.dest});
}
}
}
}
void johnsonAlgorithm(int V, const vector<Edge>& edges) {
vector<Edge> augmentedEdges = edges;
int dummyNode = V;
for (int i = 0; i < V; ++i) {
augmentedEdges.push_back({dummyNode, i, 0});
}
vector<int> h;
if (!bellmanFord(V + 1, augmentedEdges, dummyNode, h)) {
cout << "Graph contains a negative weight cycle.\n";
return;
}
vector<vector<AdjEdge>> adj(V);
for (const auto& e : edges) {
int newWeight = e.weight + h[e.src] - h[e.dest];
adj[e.src].push_back({e.dest, newWeight});
}
cout << "All-pairs shortest paths:\n";
for (int u = 0; u < V; ++u) {
vector<int> distPrime;
dijkstra(V, adj, u, distPrime);
cout << "From " << u << ": ";
for (int v = 0; v < V; ++v) {
if (distPrime[v] == INF) {
cout << "INF ";
} else {
int actualDist = distPrime[v] - h[u] + h[v];
cout << actualDist << " ";
}
}
cout << "\n";
}
}
int main() {
int V = 4;
vector<Edge> edges = {
{0, 1, -5}, {0, 2, 2}, {0, 3, 3},
{1, 2, 4}, {2, 3, 1}
};
johnsonAlgorithm(V, edges);
return 0;
}class PriorityQueue {
constructor() {
this.values = [];
}
enqueue(val, priority) {
this.values.push({val, priority});
this.sort();
}
dequeue() {
return this.values.shift();
}
sort() {
this.values.sort((a, b) => a.priority - b.priority);
}
isEmpty() {
return this.values.length === 0;
}
}
function bellmanFord(V, edges, src, h) {
for (let i = 0; i < V; i++) h[i] = Infinity;
h[src] = 0;
for (let i = 0; i < V; i++) {
for (const {u, v, w} of edges) {
if (h[u] !== Infinity && h[u] + w < h[v]) {
h[v] = h[u] + w;
if (i === V - 1) return false;
}
}
}
return true;
}
function dijkstra(V, adj, src, dist) {
for (let i = 0; i < V; i++) dist[i] = Infinity;
dist[src] = 0;
const pq = new PriorityQueue();
pq.enqueue(src, 0);
while (!pq.isEmpty()) {
const {val: u, priority: d} = pq.dequeue();
if (d > dist[u]) continue;
for (const {v, w} of adj[u]) {
if (dist[u] + w < dist[v]) {
dist[v] = dist[u] + w;
pq.enqueue(v, dist[v]);
}
}
}
}
function johnsonsAlgorithm(V, edges) {
const augmentedEdges = [...edges];
const dummy = V;
for (let i = 0; i < V; i++) {
augmentedEdges.push({u: dummy, v: i, w: 0});
}
const h = new Array(V + 1);
if (!bellmanFord(V + 1, augmentedEdges, dummy, h)) {
console.log("Graph contains a negative weight cycle.");
return;
}
const adj = Array.from({length: V}, () => []);
for (const {u, v, w} of edges) {
const newW = w + h[u] - h[v];
adj[u].push({v, w: newW});
}
console.log("All-pairs shortest paths:");
for (let u = 0; u < V; u++) {
const distPrime = new Array(V);
dijkstra(V, adj, u, distPrime);
let row = `From ${u}: `;
for (let v = 0; v < V; v++) {
if (distPrime[v] === Infinity) {
row += "INF ";
} else {
const actualDist = distPrime[v] - h[u] + h[v];
row += actualDist + " ";
}
}
console.log(row);
}
}
const V = 4;
const edges = [
{u: 0, v: 1, w: -5}, {u: 0, v: 2, w: 2}, {u: 0, v: 3, w: 3},
{u: 1, v: 2, w: 4}, {u: 2, v: 3, w: 1}
];
johnsonsAlgorithm(V, edges);import java.util.*;
class Edge {
int src, dest, weight;
Edge(int s, int d, int w) { src = s; dest = d; weight = w; }
}
class AdjEdge {
int dest, weight;
AdjEdge(int d, int w) { dest = d; weight = w; }
}
public class JohnsonAlgorithm {
static final int INF = (int)1e9;
static boolean bellmanFord(int V, List<Edge> edges, int src, int[] h) {
Arrays.fill(h, INF);
h[src] = 0;
for (int i = 0; i < V; i++) {
for (Edge e : edges) {
if (h[e.src] != INF && h[e.src] + e.weight < h[e.dest]) {
h[e.dest] = h[e.src] + e.weight;
if (i == V - 1) return false;
}
}
}
return true;
}
static void dijkstra(int V, List<List<AdjEdge>> adj, int src, int[] dist) {
Arrays.fill(dist, INF);
dist[src] = 0;
PriorityQueue<int[]> pq = new PriorityQueue<>(Comparator.comparingInt(a -> a[0]));
pq.add(new int[]{0, src});
while (!pq.isEmpty()) {
int[] curr = pq.poll();
int d = curr[0];
int u = curr[1];
if (d > dist[u]) continue;
for (AdjEdge e : adj.get(u)) {
if (dist[u] + e.weight < dist[e.dest]) {
dist[e.dest] = dist[u] + e.weight;
pq.add(new int[]{dist[e.dest], e.dest});
}
}
}
}
public static void johnsonAlgorithm(int V, List<Edge> edges) {
List<Edge> augEdges = new ArrayList<>(edges);
int dummy = V;
for (int i = 0; i < V; i++) {
augEdges.add(new Edge(dummy, i, 0));
}
int[] h = new int[V + 1];
if (!bellmanFord(V + 1, augEdges, dummy, h)) {
System.out.println("Graph contains a negative weight cycle.");
return;
}
List<List<AdjEdge>> adj = new ArrayList<>();
for (int i = 0; i < V; i++) adj.add(new ArrayList<>());
for (Edge e : edges) {
int newW = e.weight + h[e.src] - h[e.dest];
adj.get(e.src).add(new AdjEdge(e.dest, newW));
}
System.out.println("All-pairs shortest paths:");
for (int u = 0; u < V; u++) {
int[] distPrime = new int[V];
dijkstra(V, adj, u, distPrime);
System.out.print("From " + u + ": ");
for (int v = 0; v < V; v++) {
if (distPrime[v] == INF) {
System.out.print("INF ");
} else {
int actualDist = distPrime[v] - h[u] + h[v];
System.out.print(actualDist + " ");
}
}
System.out.println();
}
}
public static void main(String[] args) {
int V = 4;
List<Edge> edges = new ArrayList<>();
edges.add(new Edge(0, 1, -5));
edges.add(new Edge(0, 2, 2));
edges.add(new Edge(0, 3, 3));
edges.add(new Edge(1, 2, 4));
edges.add(new Edge(2, 3, 1));
johnsonAlgorithm(V, edges);
}
}#include <stdio.h>
#include <stdlib.h>
#define INF 1000000000
struct Edge {
int src, dest, weight;
};
int bellmanFord(int V, int E, struct Edge* edges, int src, int* h) {
for (int i = 0; i < V; i++) h[i] = INF;
h[src] = 0;
for (int i = 0; i < V; i++) {
for (int j = 0; j < E; j++) {
int u = edges[j].src;
int v = edges[j].dest;
int w = edges[j].weight;
if (h[u] != INF && h[u] + w < h[v]) {
h[v] = h[u] + w;
if (i == V - 1) return 0; // Negative cycle
}
}
}
return 1;
}
void dijkstra(int V, int u, int* adj_dest, int* adj_weight, int* head, int* next, int* dist) {
int* visited = (int*)calloc(V, sizeof(int));
for (int i = 0; i < V; i++) dist[i] = INF;
dist[u] = 0;
for (int i = 0; i < V; i++) {
int min_d = INF, min_u = -1;
for (int j = 0; j < V; j++) {
if (!visited[j] && dist[j] < min_d) {
min_d = dist[j];
min_u = j;
}
}
if (min_u == -1) break;
visited[min_u] = 1;
for (int e = head[min_u]; e != -1; e = next[e]) {
int v = adj_dest[e];
int w = adj_weight[e];
if (dist[min_u] + w < dist[v]) {
dist[v] = dist[min_u] + w;
}
}
}
free(visited);
}
void johnsonAlgorithm(int V, int E, struct Edge* edges) {
int aug_E = E + V;
struct Edge* augEdges = (struct Edge*)malloc(aug_E * sizeof(struct Edge));
for (int i = 0; i < E; i++) augEdges[i] = edges[i];
for (int i = 0; i < V; i++) {
augEdges[E + i].src = V;
augEdges[E + i].dest = i;
augEdges[E + i].weight = 0;
}
int* h = (int*)malloc((V + 1) * sizeof(int));
if (!bellmanFord(V + 1, aug_E, augEdges, V, h)) {
printf("Graph contains a negative weight cycle.\n");
free(augEdges); free(h); return;
}
int* head = (int*)malloc(V * sizeof(int));
int* next = (int*)malloc(E * sizeof(int));
int* adj_dest = (int*)malloc(E * sizeof(int));
int* adj_weight = (int*)malloc(E * sizeof(int));
for (int i = 0; i < V; i++) head[i] = -1;
for (int i = 0; i < E; i++) {
int u = edges[i].src;
int v = edges[i].dest;
int w = edges[i].weight + h[u] - h[v];
adj_dest[i] = v;
adj_weight[i] = w;
next[i] = head[u];
head[u] = i;
}
printf("All-pairs shortest paths:\n");
int* dist = (int*)malloc(V * sizeof(int));
for (int u = 0; u < V; u++) {
dijkstra(V, u, adj_dest, adj_weight, head, next, dist);
printf("From %d: ", u);
for (int v = 0; v < V; v++) {
if (dist[v] == INF) printf("INF ");
else printf("%d ", dist[v] - h[u] + h[v]);
}
printf("\n");
}
free(augEdges); free(h); free(head); free(next); free(adj_dest); free(adj_weight); free(dist);
}
int main() {
int V = 4, E = 5;
struct Edge edges[] = {
{0, 1, -5}, {0, 2, 2}, {0, 3, 3},
{1, 2, 4}, {2, 3, 1}
};
johnsonAlgorithm(V, E, edges);
return 0;
}
When to Use Johnson’s Algorithm
flowchart TD Q{"Are there negative\nedge weights?"} Q -- No --> R1["✅ Use Dijkstra's Algorithm from all nodes\n(Faster and simpler)"] Q -- Yes --> S1{"Is the graph\nDense or Sparse?"} S1 -- Dense (E ≈ V²) --> R2["✅ Use Floyd-Warshall Algorithm\n(O(V³), highly vectorized array operations)"] S1 -- Sparse (E ≈ V) --> R3["✅ Use Johnson's Algorithm\n(O(V² log V), scales better for low edge counts)"]
✅ Use Johnson’s Algorithm When
- You need all-pairs shortest paths in a graph that contains negative edge weights.
- The graph is sparse (). Johnson’s algorithm capitalizes on being small via Dijkstra’s priority queues.
- Negative weight cycles must be detected gracefully.
❌ Avoid Johnson’s Algorithm When
- The graph has no negative edge weights (skip Bellman-Ford reweighting entirely and just run Dijkstra from every node).
- The graph is dense (Floyd-Warshall is easier to write, uses simple array operations, and acts much faster in practice on caches).
Key Takeaways
- Hybrid Approach — combines Bellman-Ford’s robustness to negative weights with Dijkstra’s blazing speed.
- Telescoping Potential Function — reweights edges safely such that shortest paths mathematically preserve their topological integrity ().
- Sparse Superiority — destroys Floyd-Warshall in time complexity ( vs ) precisely when the number of edges is limited.
- Negative Cycle Guard — naturally inherits Bellman-Ford’s ability to reject graphs containing uncomputable shortest paths.