|
| 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