|
| 1 | +# A* (A-star) Search Algorithm |
| 2 | +# |
| 3 | +# A* finds the least-cost path from a start node to a target node in a weighted graph. |
| 4 | +# It combines the path cost from the start (g-score) with a heuristic estimate (h-score) |
| 5 | +# to the goal: f = g + h. With an admissible and consistent heuristic, A* is optimal. |
| 6 | +# |
| 7 | +# Time Complexity: O((V + E) log V) with a binary heap priority queue |
| 8 | +# Space Complexity: O(V) |
| 9 | +# |
| 10 | +# Graph Input: adjacency list like other files in this folder, where each entry is a list of |
| 11 | +# edges with fields `vertex` and `weight`. Vertices are numeric indices. |
| 12 | +# Heuristic: a function h(v) that estimates the remaining cost from v to the goal. By default |
| 13 | +# it is 0 for all vertices (A* reduces to Dijkstra). |
| 14 | +# Output: A list containing g_scores (distances), f_scores, predecessor, a found flag, and path. |
| 15 | + |
| 16 | +# --------------------------- |
| 17 | +# Priority queue (min-heap-ish) using data.frame for clarity (educational) |
| 18 | +# --------------------------- |
| 19 | +create_priority_queue <- function() { |
| 20 | + list( |
| 21 | + elements = data.frame(vertex = integer(0), f = numeric(0), g = numeric(0), h = numeric(0)), |
| 22 | + size = 0 |
| 23 | + ) |
| 24 | +} |
| 25 | + |
| 26 | +pq_insert <- function(pq, vertex, f, g, h) { |
| 27 | + pq$elements <- rbind(pq$elements, data.frame(vertex = vertex, f = f, g = g, h = h)) |
| 28 | + pq$size <- pq$size + 1 |
| 29 | + pq |
| 30 | +} |
| 31 | + |
| 32 | +pq_extract_min <- function(pq) { |
| 33 | + if (pq$size == 0) { |
| 34 | + return(list(pq = pq, min_element = NULL)) |
| 35 | + } |
| 36 | + min_idx <- which.min(pq$elements$f) |
| 37 | + min_element <- pq$elements[min_idx, ] |
| 38 | + pq$elements <- pq$elements[-min_idx, ] |
| 39 | + pq$size <- pq$size - 1 |
| 40 | + list(pq = pq, min_element = min_element) |
| 41 | +} |
| 42 | + |
| 43 | +pq_is_empty <- function(pq) { |
| 44 | + pq$size == 0 |
| 45 | +} |
| 46 | + |
| 47 | +# --------------------------- |
| 48 | +# Main A* implementation over adjacency-list graph |
| 49 | +# --------------------------- |
| 50 | +a_star_search <- function(graph, start, goal, heuristic = function(v) 0) { |
| 51 | + # Collect all vertices (numeric indices expected as in other files) |
| 52 | + all_vertices <- unique(c(names(graph), unlist(lapply(graph, function(x) sapply(x, function(e) e$vertex))))) |
| 53 | + all_vertices <- as.numeric(all_vertices) |
| 54 | + num_vertices <- max(all_vertices) |
| 55 | + |
| 56 | + # Initialize scores and bookkeeping |
| 57 | + g_scores <- rep(Inf, num_vertices) # cost from start |
| 58 | + f_scores <- rep(Inf, num_vertices) # g + h |
| 59 | + predecessor <- rep(-1, num_vertices) |
| 60 | + closed <- rep(FALSE, num_vertices) |
| 61 | + |
| 62 | + g_scores[start] <- 0 |
| 63 | + f_scores[start] <- heuristic(start) |
| 64 | + |
| 65 | + pq <- create_priority_queue() |
| 66 | + pq <- pq_insert(pq, start, f_scores[start], g_scores[start], heuristic(start)) |
| 67 | + |
| 68 | + found <- FALSE |
| 69 | + |
| 70 | + while (!pq_is_empty(pq)) { |
| 71 | + res <- pq_extract_min(pq) |
| 72 | + pq <- res$pq |
| 73 | + current <- res$min_element |
| 74 | + if (is.null(current)) break |
| 75 | + |
| 76 | + u <- as.integer(current$vertex) |
| 77 | + if (closed[u]) next |
| 78 | + |
| 79 | + # If target popped, we're done |
| 80 | + if (u == goal) { |
| 81 | + found <- TRUE |
| 82 | + break |
| 83 | + } |
| 84 | + |
| 85 | + closed[u] <- TRUE |
| 86 | + |
| 87 | + # Explore neighbors |
| 88 | + if (as.character(u) %in% names(graph)) { |
| 89 | + for (edge in graph[[as.character(u)]]) { |
| 90 | + v <- edge$vertex |
| 91 | + w <- edge$weight |
| 92 | + |
| 93 | + if (closed[v]) next |
| 94 | + |
| 95 | + tentative_g <- g_scores[u] + w |
| 96 | + if (tentative_g < g_scores[v]) { |
| 97 | + predecessor[v] <- u |
| 98 | + g_scores[v] <- tentative_g |
| 99 | + f_scores[v] <- tentative_g + heuristic(v) |
| 100 | + pq <- pq_insert(pq, v, f_scores[v], g_scores[v], heuristic(v)) |
| 101 | + } |
| 102 | + } |
| 103 | + } |
| 104 | + } |
| 105 | + |
| 106 | + # Build path if found or reachable |
| 107 | + path <- NULL |
| 108 | + if (is.finite(g_scores[goal])) { |
| 109 | + found <- TRUE |
| 110 | + path <- reconstruct_a_star_path(predecessor, start, goal) |
| 111 | + } |
| 112 | + |
| 113 | + list( |
| 114 | + g_scores = g_scores, |
| 115 | + f_scores = f_scores, |
| 116 | + predecessor = predecessor, |
| 117 | + found = found, |
| 118 | + path = path |
| 119 | + ) |
| 120 | +} |
| 121 | + |
| 122 | +reconstruct_a_star_path <- function(predecessor, start, goal) { |
| 123 | + path <- c() |
| 124 | + cur <- goal |
| 125 | + while (cur != -1) { |
| 126 | + path <- c(cur, path) |
| 127 | + if (cur == start) break |
| 128 | + cur <- predecessor[cur] |
| 129 | + } |
| 130 | + path |
| 131 | +} |
| 132 | + |
| 133 | +# --------------------------- |
| 134 | +# Grid helpers (to mirror the provided Java example on a 2D grid) |
| 135 | +# --------------------------- |
| 136 | +# Convert a 2D grid (matrix with 1 as free cell, 0 as blocked) to an adjacency list graph |
| 137 | +# Returns: list(graph = adjacency_list, index_of = function(r,c) -> vertex, coords = data.frame(row,col)) |
| 138 | +grid_to_graph <- function(grid) { |
| 139 | + stopifnot(is.matrix(grid)) |
| 140 | + nrow_g <- nrow(grid) |
| 141 | + ncol_g <- ncol(grid) |
| 142 | + |
| 143 | + index_of <- function(r, c) { |
| 144 | + # 1-based indexing for vertices |
| 145 | + (r - 1) * ncol_g + c |
| 146 | + } |
| 147 | + |
| 148 | + coords <- data.frame(row = integer(), col = integer()) |
| 149 | + adj <- list() |
| 150 | + |
| 151 | + dirs <- rbind(c(-1, 0), c(1, 0), c(0, -1), c(0, 1)) # up, down, left, right |
| 152 | + |
| 153 | + for (r in 1:nrow_g) { |
| 154 | + for (c in 1:ncol_g) { |
| 155 | + v <- index_of(r, c) |
| 156 | + coords[v, c("row", "col")] <- c(r, c) |
| 157 | + if (grid[r, c] == 1) { |
| 158 | + edges <- list() |
| 159 | + for (k in 1:nrow(dirs)) { |
| 160 | + nr <- r + dirs[k, 1] |
| 161 | + nc <- c + dirs[k, 2] |
| 162 | + if (nr >= 1 && nr <= nrow_g && nc >= 1 && nc <= ncol_g && grid[nr, nc] == 1) { |
| 163 | + edges[[length(edges) + 1]] <- list(vertex = index_of(nr, nc), weight = 1) |
| 164 | + } |
| 165 | + } |
| 166 | + adj[[as.character(v)]] <- edges |
| 167 | + } else { |
| 168 | + adj[[as.character(v)]] <- list() |
| 169 | + } |
| 170 | + } |
| 171 | + } |
| 172 | + |
| 173 | + list(graph = adj, index_of = index_of, coords = coords) |
| 174 | +} |
| 175 | + |
| 176 | +# Manhattan heuristic factory for grid graphs |
| 177 | +make_manhattan_heuristic <- function(goal_vertex, coords) { |
| 178 | + function(v) { |
| 179 | + dv <- coords[v, ] |
| 180 | + dg <- coords[goal_vertex, ] |
| 181 | + abs(dv$row - dg$row) + abs(dv$col - dg$col) |
| 182 | + } |
| 183 | +} |
| 184 | + |
| 185 | +# --------------------------- |
| 186 | +# Example usage and tests |
| 187 | +# --------------------------- |
| 188 | +cat("=== A* (A-star) Search Algorithm ===\n") |
| 189 | + |
| 190 | +# Example 1: Grid-based example mirroring the provided Java snippet |
| 191 | +cat("\n-- Grid example (5x5, 4-neighborhood, Manhattan heuristic) --\n") |
| 192 | +grid <- matrix( |
| 193 | + c(1, 1, 1, 1, 1, |
| 194 | + 0, 1, 0, 1, 0, |
| 195 | + 1, 1, 1, 1, 1, |
| 196 | + 1, 0, 0, 0, 1, |
| 197 | + 1, 1, 1, 1, 1), |
| 198 | + nrow = 5, ncol = 5, byrow = TRUE |
| 199 | +) |
| 200 | + |
| 201 | +gg <- grid_to_graph(grid) |
| 202 | +start_rc <- c(1, 1) # row, col (1-based) |
| 203 | +goal_rc <- c(5, 5) |
| 204 | +start_v <- gg$index_of(start_rc[1], start_rc[2]) |
| 205 | +goal_v <- gg$index_of(goal_rc[1], goal_rc[2]) |
| 206 | +h_manhattan <- make_manhattan_heuristic(goal_v, gg$coords) |
| 207 | + |
| 208 | +cat("Running A* from (", start_rc[1], ", ", start_rc[2], ") to (", goal_rc[1], ", ", goal_rc[2], ")\n", sep = "") |
| 209 | +astar_res <- a_star_search(gg$graph, start_v, goal_v, heuristic = h_manhattan) |
| 210 | + |
| 211 | +if (astar_res$found && !is.null(astar_res$path)) { |
| 212 | + cat("Path found (as grid coordinates):\n") |
| 213 | + for (v in astar_res$path) { |
| 214 | + rc <- gg$coords[v, ] |
| 215 | + cat("(", rc$row, ", ", rc$col, ")\n", sep = "") |
| 216 | + } |
| 217 | + cat("Total steps:", length(astar_res$path) - 1, "\n") |
| 218 | +} else { |
| 219 | + cat("No path found!\n") |
| 220 | +} |
| 221 | + |
| 222 | +# Example 2: Generic adjacency list (A* with zero heuristic behaves like Dijkstra) |
| 223 | +cat("\n-- Adjacency list example (zero heuristic -> Dijkstra behavior) --\n") |
| 224 | +weighted_graph <- list( |
| 225 | + "1" = list(list(vertex = 2, weight = 1), list(vertex = 3, weight = 4)), |
| 226 | + "2" = list(list(vertex = 3, weight = 2), list(vertex = 4, weight = 5)), |
| 227 | + "3" = list(list(vertex = 4, weight = 1)), |
| 228 | + "4" = list() |
| 229 | +) |
| 230 | + |
| 231 | +start <- 1 |
| 232 | +goal <- 4 |
| 233 | +res2 <- a_star_search(weighted_graph, start, goal) |
| 234 | +if (res2$found) { |
| 235 | + cat("Shortest path from", start, "to", goal, ": ", paste(res2$path, collapse = " -> "), |
| 236 | + " (distance:", res2$g_scores[goal], ")\n", sep = "") |
| 237 | +} |
0 commit comments