Skip to content

Commit 421a3f6

Browse files
Merge branch 'master' into vf2_graph_isomorphism
2 parents 4165f1b + f0c8fcd commit 421a3f6

File tree

4 files changed

+706
-0
lines changed

4 files changed

+706
-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+
}
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
# ==============================================================
2+
# Bidirectional Breadth-First Search (BFS) Shortest Path Algorithm
3+
# ==============================================================
4+
#
5+
# Description:
6+
# Finds the shortest path between a source and target in an
7+
# unweighted graph using Bidirectional BFS.
8+
#
9+
# Time Complexity: O(b^(d/2)) — much faster than normal BFS O(b^d)
10+
# Space Complexity: O(V)
11+
#
12+
# Input:
13+
# graph - adjacency list (list of integer vectors)
14+
# source - integer (starting vertex)
15+
# target - integer (destination vertex)
16+
#
17+
# Output:
18+
# A list containing:
19+
# path - vector of vertices representing the path
20+
# distance - number of edges in the shortest path
21+
# found - logical flag (TRUE if path found, else FALSE)
22+
#
23+
# Example usage at bottom of file.
24+
# ==============================================================
25+
26+
bidirectional_bfs <- function(graph, source, target) {
27+
if (source == target) {
28+
return(list(path = c(source), distance = 0, found = TRUE))
29+
}
30+
31+
# Initialize BFS from both ends
32+
visited_from_source <- setNames(rep(FALSE, length(graph)), names(graph))
33+
visited_from_target <- setNames(rep(FALSE, length(graph)), names(graph))
34+
35+
parent_from_source <- rep(NA, length(graph))
36+
parent_from_target <- rep(NA, length(graph))
37+
38+
queue_source <- c(source)
39+
queue_target <- c(target)
40+
41+
visited_from_source[source] <- TRUE
42+
visited_from_target[target] <- TRUE
43+
44+
meeting_node <- NA
45+
46+
# Function to check intersection
47+
get_intersection <- function() {
48+
common <- which(visited_from_source & visited_from_target)
49+
if (length(common) > 0) return(common[1])
50+
return(NA)
51+
}
52+
53+
# Main loop
54+
while (length(queue_source) > 0 && length(queue_target) > 0) {
55+
# Expand one level from source side
56+
next_queue <- c()
57+
for (u in queue_source) {
58+
for (v in graph[[as.character(u)]]) {
59+
if (!visited_from_source[v]) {
60+
visited_from_source[v] <- TRUE
61+
parent_from_source[v] <- u
62+
next_queue <- c(next_queue, v)
63+
}
64+
}
65+
}
66+
queue_source <- next_queue
67+
68+
# Check intersection
69+
meeting_node <- get_intersection()
70+
if (!is.na(meeting_node)) break
71+
72+
# Expand one level from target side
73+
next_queue <- c()
74+
for (u in queue_target) {
75+
for (v in graph[[as.character(u)]]) {
76+
if (!visited_from_target[v]) {
77+
visited_from_target[v] <- TRUE
78+
parent_from_target[v] <- u
79+
next_queue <- c(next_queue, v)
80+
}
81+
}
82+
}
83+
queue_target <- next_queue
84+
85+
# Check intersection again
86+
meeting_node <- get_intersection()
87+
if (!is.na(meeting_node)) break
88+
}
89+
90+
if (is.na(meeting_node)) {
91+
return(list(path = NULL, distance = Inf, found = FALSE))
92+
}
93+
94+
# Reconstruct path from source → meeting_node
95+
path1 <- c()
96+
node <- meeting_node
97+
while (!is.na(node)) {
98+
path1 <- c(node, path1)
99+
node <- parent_from_source[node]
100+
}
101+
102+
# Reconstruct path from meeting_node → target
103+
path2 <- c()
104+
node <- parent_from_target[meeting_node]
105+
while (!is.na(node)) {
106+
path2 <- c(path2, node)
107+
node <- parent_from_target[node]
108+
}
109+
110+
full_path <- c(path1, path2)
111+
return(list(path = full_path, distance = length(full_path) - 1, found = TRUE))
112+
}
113+
114+
# ==============================================================
115+
# Example Usage and Test
116+
# ==============================================================
117+
118+
cat("=== Bidirectional BFS Shortest Path ===\n")
119+
120+
# Example Graph (Unweighted)
121+
# 1 -- 2 -- 3
122+
# | |
123+
# 4 -- 5 -- 6
124+
125+
graph <- list(
126+
"1" = c(2, 4),
127+
"2" = c(1, 3, 5),
128+
"3" = c(2, 6),
129+
"4" = c(1, 5),
130+
"5" = c(2, 4, 6),
131+
"6" = c(3, 5)
132+
)
133+
134+
cat("Graph adjacency list:\n")
135+
for (v in names(graph)) {
136+
cat("Vertex", v, "-> [", paste(graph[[v]], collapse = ", "), "]\n")
137+
}
138+
139+
cat("\nRunning Bidirectional BFS from 1 to 6...\n")
140+
result <- bidirectional_bfs(graph, 1, 6)
141+
142+
if (result$found) {
143+
cat("Shortest Path Found!\n")
144+
cat("Path:", paste(result$path, collapse = " -> "), "\n")
145+
cat("Distance:", result$distance, "\n")
146+
} else {
147+
cat("No path found between source and target.\n")
148+
}
149+
return(list(
150+
distances = distances,
151+
predecessor = predecessor,
152+
found = found
153+
))

0 commit comments

Comments
 (0)