Skip to content

Commit c458e2f

Browse files
authored
Add A* (A-star) Search Algorithm in R (#224)
1 parent 60af194 commit c458e2f

File tree

1 file changed

+237
-0
lines changed

1 file changed

+237
-0
lines changed

graph_algorithms/a_star_search.r

Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
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

Comments
 (0)