Skip to content

Commit 60af194

Browse files
authored
Johnson’s All-Pairs Shortest Paths Algorithm in R (#223)
1 parent b083bc9 commit 60af194

File tree

1 file changed

+214
-0
lines changed

1 file changed

+214
-0
lines changed
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
# Johnson's All-Pairs Shortest Paths Algorithm
2+
#
3+
# Johnson's algorithm computes shortest paths between all pairs of vertices
4+
# in a sparse, weighted directed graph that may have negative edge weights
5+
# (but no negative cycles). It reweights edges using Bellman-Ford to remove
6+
# negative weights and then runs Dijkstra from each vertex.
7+
#
8+
# Time Complexity: O(V * E + V * (E log V)) with a binary heap priority queue
9+
# Space Complexity: O(V + E)
10+
#
11+
# Graph representation matches other files in this folder:
12+
# - Adjacency list: a named list where each name is a vertex index as a string
13+
# - Each entry is a list of edges: list(vertex = <int>, weight = <numeric>)
14+
15+
# ----------------------------
16+
# Priority Queue (simple) API
17+
# ----------------------------
18+
create_priority_queue <- function() {
19+
list(
20+
elements = data.frame(vertex = integer(0), distance = numeric(0)),
21+
size = 0
22+
)
23+
}
24+
25+
pq_insert <- function(pq, vertex, distance) {
26+
pq$elements <- rbind(pq$elements, data.frame(vertex = vertex, distance = distance))
27+
pq$size <- pq$size + 1
28+
return(pq)
29+
}
30+
31+
pq_extract_min <- function(pq) {
32+
if (pq$size == 0) {
33+
return(list(pq = pq, min_element = NULL))
34+
}
35+
min_idx <- which.min(pq$elements$distance)
36+
min_element <- pq$elements[min_idx, ]
37+
pq$elements <- pq$elements[-min_idx, ]
38+
pq$size <- pq$size - 1
39+
return(list(pq = pq, min_element = min_element))
40+
}
41+
42+
pq_is_empty <- function(pq) {
43+
return(pq$size == 0)
44+
}
45+
46+
# -----------------------------------------------
47+
# Bellman-Ford potentials (no explicit supersource)
48+
# -----------------------------------------------
49+
# Equivalent to adding a new super-source s with 0-weight edges to all vertices,
50+
# by initializing h[i] = 0 for all i and relaxing edges (V-1) times.
51+
bellman_ford_potentials <- function(graph) {
52+
# Collect all vertices appearing as sources or targets
53+
all_vertices <- unique(c(names(graph), unlist(lapply(graph, function(x) sapply(x, function(e) e$vertex)))))
54+
all_vertices <- as.numeric(all_vertices)
55+
V <- max(all_vertices)
56+
57+
# Initialize h with zeros (super-source trick)
58+
h <- rep(0, V)
59+
60+
# Relax edges V-1 times
61+
for (i in 1:(V - 1)) {
62+
updated <- FALSE
63+
for (u_char in as.character(1:V)) {
64+
if (!(u_char %in% names(graph))) next
65+
u <- as.numeric(u_char)
66+
for (edge in graph[[u_char]]) {
67+
v <- edge$vertex
68+
w <- edge$weight
69+
if (h[u] + w < h[v]) {
70+
h[v] <- h[u] + w
71+
updated <- TRUE
72+
}
73+
}
74+
}
75+
if (!updated) break
76+
}
77+
78+
# Check for negative-weight cycles
79+
negative_cycle <- FALSE
80+
for (u_char in as.character(1:V)) {
81+
if (!(u_char %in% names(graph))) next
82+
u <- as.numeric(u_char)
83+
for (edge in graph[[u_char]]) {
84+
v <- edge$vertex
85+
w <- edge$weight
86+
if (h[u] + w < h[v]) {
87+
negative_cycle <- TRUE
88+
break
89+
}
90+
}
91+
if (negative_cycle) break
92+
}
93+
94+
list(h = h, V = V, negative_cycle = negative_cycle)
95+
}
96+
97+
# ---------------------------
98+
# Dijkstra on reweighted graph
99+
# ---------------------------
100+
dijkstra_on_adj <- function(graph, source, V) {
101+
distances <- rep(Inf, V)
102+
previous <- rep(-1, V)
103+
visited <- rep(FALSE, V)
104+
105+
distances[source] <- 0
106+
pq <- create_priority_queue()
107+
pq <- pq_insert(pq, source, 0)
108+
109+
while (!pq_is_empty(pq)) {
110+
res <- pq_extract_min(pq)
111+
pq <- res$pq
112+
cur <- res$min_element
113+
if (is.null(cur)) break
114+
115+
u <- cur$vertex
116+
if (visited[u]) next
117+
visited[u] <- TRUE
118+
119+
u_char <- as.character(u)
120+
if (u_char %in% names(graph)) {
121+
for (edge in graph[[u_char]]) {
122+
v <- edge$vertex
123+
w <- edge$weight
124+
if (!visited[v] && distances[u] + w < distances[v]) {
125+
distances[v] <- distances[u] + w
126+
previous[v] <- u
127+
pq <- pq_insert(pq, v, distances[v])
128+
}
129+
}
130+
}
131+
}
132+
133+
list(distances = distances, previous = previous)
134+
}
135+
136+
# ---------------------
137+
# Johnson's main driver
138+
# ---------------------
139+
johnson_shortest_paths <- function(graph) {
140+
# Ensure all vertices 1..V exist as keys (even if empty list)
141+
all_vertices <- unique(c(names(graph), unlist(lapply(graph, function(x) sapply(x, function(e) e$vertex)))))
142+
all_vertices <- as.numeric(all_vertices)
143+
V <- max(all_vertices)
144+
for (u in as.character(1:V)) {
145+
if (!(u %in% names(graph))) graph[[u]] <- list()
146+
}
147+
148+
# Step 1: Bellman-Ford to get potentials h
149+
bf <- bellman_ford_potentials(graph)
150+
if (bf$negative_cycle) {
151+
return(list(
152+
distances = NULL,
153+
negative_cycle = TRUE,
154+
message = "Graph contains a negative-weight cycle; shortest paths undefined"
155+
))
156+
}
157+
h <- bf$h
158+
159+
# Step 2: Reweight edges to eliminate negative weights
160+
reweighted <- vector("list", V)
161+
names(reweighted) <- as.character(1:V)
162+
for (i in 1:V) reweighted[[i]] <- list()
163+
for (u_char in as.character(1:V)) {
164+
u <- as.numeric(u_char)
165+
for (edge in graph[[u_char]]) {
166+
v <- edge$vertex
167+
w <- edge$weight
168+
w_prime <- w + h[u] - h[v]
169+
reweighted[[u_char]] <- append(reweighted[[u_char]], list(list(vertex = v, weight = w_prime)))
170+
}
171+
}
172+
173+
# Step 3: Run Dijkstra from each vertex on the reweighted graph
174+
dist_matrix <- matrix(Inf, nrow = V, ncol = V)
175+
for (s in 1:V) {
176+
dj <- dijkstra_on_adj(reweighted, s, V)
177+
# Convert distances back to original weights
178+
for (v in 1:V) {
179+
if (!is.infinite(dj$distances[v])) {
180+
dist_matrix[s, v] <- dj$distances[v] - h[s] + h[v]
181+
}
182+
}
183+
}
184+
185+
list(
186+
distances = dist_matrix,
187+
negative_cycle = FALSE
188+
)
189+
}
190+
191+
# -----------------
192+
# Example / Demo
193+
# -----------------
194+
cat("=== Johnson's All-Pairs Shortest Paths Algorithm ===\n")
195+
196+
# Convert the Java example (0-based) to 1-based indices used here
197+
# Java edges:
198+
# 0->1(3), 0->2(8), 0->4(-4), 1->3(1), 1->4(7), 2->1(4), 3->0(2), 3->2(-5), 4->3(6)
199+
j_graph <- list(
200+
"1" = list(list(vertex = 2, weight = 3), list(vertex = 3, weight = 8), list(vertex = 5, weight = -4)),
201+
"2" = list(list(vertex = 4, weight = 1), list(vertex = 5, weight = 7)),
202+
"3" = list(list(vertex = 2, weight = 4)),
203+
"4" = list(list(vertex = 1, weight = 2), list(vertex = 3, weight = -5)),
204+
"5" = list(list(vertex = 4, weight = 6))
205+
)
206+
207+
cat("Running Johnson on 5-vertex graph (with negative edges, no negative cycles) ...\n")
208+
j_res <- johnson_shortest_paths(j_graph)
209+
if (isTRUE(j_res$negative_cycle)) {
210+
cat("Negative cycle detected. Shortest paths undefined.\n")
211+
} else {
212+
cat("All-pairs shortest path distance matrix (rows = sources, cols = targets):\n")
213+
print(j_res$distances)
214+
}

0 commit comments

Comments
 (0)