Skip to content

Commit f0cda94

Browse files
committed
Add A* Search Algorithm for Weighted Graph Pathfinding
1 parent 227355e commit f0cda94

File tree

2 files changed

+270
-0
lines changed

2 files changed

+270
-0
lines changed
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
package com.thealgorithms.others;
2+
3+
import java.util.*;
4+
5+
/**
6+
* A* (A-Star) Search Algorithm implementation for finding the shortest path
7+
* between two nodes in a graph.
8+
*
9+
* <p>This algorithm uses a heuristic to guide its search, making it more efficient
10+
* than Dijkstra’s algorithm in many cases.
11+
*
12+
* <p>f(n) = g(n) + h(n)
13+
* where:
14+
* - g(n): cost from start node to current node
15+
* - h(n): estimated cost from current node to goal (heuristic)
16+
*
17+
* <p>Time Complexity:
18+
* - Worst case: O(E log V)
19+
*
20+
* <p>Space Complexity:
21+
* - O(V)
22+
*
23+
* <p>Use Cases:
24+
* - Pathfinding (maps, games)
25+
* - AI planning
26+
*
27+
* @author Suraj Devatha
28+
*/
29+
public class AStarSearch {
30+
31+
/**
32+
* Finds shortest path using A*.
33+
*
34+
* @param graph adjacency list
35+
* @param start start node
36+
* @param goal goal node
37+
* @param heuristic heuristic function
38+
* @return list of nodes representing shortest path
39+
*/
40+
public static List<Node> findPath(
41+
Map<Node, List<Edge>> graph,
42+
Node start,
43+
Node goal,
44+
Heuristic heuristic
45+
) {
46+
47+
Map<Node, Double> gScore = new HashMap<>();
48+
Map<Node, Double> fScore = new HashMap<>();
49+
Map<Node, Node> cameFrom = new HashMap<>();
50+
51+
PriorityQueue<Node> openSet = new PriorityQueue<>(
52+
Comparator.comparingDouble(fScore::get)
53+
);
54+
55+
gScore.put(start, 0.0);
56+
fScore.put(start, heuristic.estimate(start, goal));
57+
58+
openSet.add(start);
59+
60+
while (!openSet.isEmpty()) {
61+
Node current = openSet.poll();
62+
63+
if (current.equals(goal)) {
64+
return reconstructPath(cameFrom, current);
65+
}
66+
67+
for (Edge edge : graph.getOrDefault(current, Collections.emptyList())) {
68+
Node neighbor = edge.target;
69+
double tentativeG = gScore.get(current) + edge.cost;
70+
71+
if (tentativeG < gScore.getOrDefault(neighbor, Double.POSITIVE_INFINITY)) {
72+
cameFrom.put(neighbor, current);
73+
gScore.put(neighbor, tentativeG);
74+
fScore.put(neighbor, tentativeG + heuristic.estimate(neighbor, goal));
75+
76+
if (!openSet.contains(neighbor)) {
77+
openSet.add(neighbor);
78+
}
79+
}
80+
}
81+
}
82+
83+
return Collections.emptyList(); // No path found
84+
}
85+
86+
/**
87+
* Reconstructs path from goal to start.
88+
*/
89+
private static List<Node> reconstructPath(Map<Node, Node> cameFrom, Node current) {
90+
List<Node> path = new ArrayList<>();
91+
path.add(current);
92+
93+
while (cameFrom.containsKey(current)) {
94+
current = cameFrom.get(current);
95+
path.add(current);
96+
}
97+
98+
Collections.reverse(path);
99+
return path;
100+
}
101+
102+
/**
103+
* Heuristic interface (can plug different heuristics).
104+
*/
105+
public interface Heuristic {
106+
double estimate(Node current, Node goal);
107+
}
108+
109+
/**
110+
* Node class representing a vertex in the graph.
111+
*/
112+
static class Node {
113+
String id;
114+
115+
Node(String id) {
116+
this.id = id;
117+
}
118+
119+
@Override
120+
public boolean equals(Object o) {
121+
if (this == o) return true;
122+
if (!(o instanceof Node)) return false;
123+
Node node = (Node) o;
124+
return Objects.equals(id, node.id);
125+
}
126+
127+
@Override
128+
public int hashCode() {
129+
return Objects.hash(id);
130+
}
131+
}
132+
133+
/**
134+
* Edge class representing weighted connections.
135+
*/
136+
static class Edge {
137+
Node target;
138+
double cost;
139+
140+
Edge(Node target, double cost) {
141+
this.target = target;
142+
this.cost = cost;
143+
}
144+
}
145+
}
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
package com.thealgorithms.others;
2+
3+
import org.junit.jupiter.api.BeforeAll;
4+
import org.junit.jupiter.api.Test;
5+
6+
import java.util.*;
7+
8+
import static org.junit.jupiter.api.Assertions.assertEquals;
9+
import static org.junit.jupiter.api.Assertions.assertTrue;
10+
11+
/**
12+
* Test cases for AStarSearch algorithm.
13+
*/
14+
class AStarSearchTest {
15+
16+
/**
17+
* Simple heuristic that returns 0 (reduces A* to Dijkstra).
18+
*/
19+
private static final AStarSearch.Heuristic ZERO_HEURISTIC = (node, goal) -> 0;
20+
private static Map<AStarSearch.Node, List<AStarSearch.Edge>> graph;
21+
private static AStarSearch.Node A;
22+
private static AStarSearch.Node B;
23+
private static AStarSearch.Node C;
24+
private static AStarSearch.Node D;
25+
26+
@BeforeAll
27+
static void setUp() {
28+
graph = new HashMap<>();
29+
30+
A = new AStarSearch.Node("A");
31+
B = new AStarSearch.Node("B");
32+
C = new AStarSearch.Node("C");
33+
D = new AStarSearch.Node("D");
34+
35+
graph.put(A, Arrays.asList(
36+
new AStarSearch.Edge(B, 1),
37+
new AStarSearch.Edge(C, 4)
38+
));
39+
40+
graph.put(B, Arrays.asList(
41+
new AStarSearch.Edge(C, 2),
42+
new AStarSearch.Edge(D, 5)
43+
));
44+
45+
graph.put(C, Arrays.asList(
46+
new AStarSearch.Edge(D, 1)
47+
));
48+
49+
graph.put(D, Collections.emptyList());
50+
}
51+
52+
@Test
53+
void testPathExists() {
54+
List<AStarSearch.Node> path =
55+
AStarSearch.findPath(graph, A, D, ZERO_HEURISTIC);
56+
57+
// Expected shortest path: A -> B -> C -> D
58+
List<AStarSearch.Node> expected = Arrays.asList(A, B, C, D);
59+
60+
assertEquals(expected, path);
61+
}
62+
63+
@Test
64+
void testDirectPath() {
65+
List<AStarSearch.Node> path =
66+
AStarSearch.findPath(graph, A, B, ZERO_HEURISTIC);
67+
68+
List<AStarSearch.Node> expected = Arrays.asList(A, B);
69+
70+
assertEquals(expected, path);
71+
}
72+
73+
@Test
74+
void testStartEqualsGoal() {
75+
List<AStarSearch.Node> path =
76+
AStarSearch.findPath(graph, A, A, ZERO_HEURISTIC);
77+
78+
List<AStarSearch.Node> expected = Collections.singletonList(A);
79+
80+
assertEquals(expected, path);
81+
}
82+
83+
@Test
84+
void testNoPathExists() {
85+
AStarSearch.Node E = new AStarSearch.Node("E");
86+
87+
List<AStarSearch.Node> path =
88+
AStarSearch.findPath(graph, E, A, ZERO_HEURISTIC);
89+
90+
assertTrue(path.isEmpty());
91+
}
92+
93+
@Test
94+
void testPathCostOptimality() {
95+
List<AStarSearch.Node> path =
96+
AStarSearch.findPath(graph, A, D, ZERO_HEURISTIC);
97+
98+
// Calculate total cost
99+
double cost = calculatePathCost(path);
100+
101+
// Expected shortest cost = 1 (A->B) + 2 (B->C) + 1 (C->D) = 4
102+
assertEquals(4.0, cost);
103+
}
104+
105+
/**
106+
* Utility method to calculate path cost.
107+
*/
108+
private double calculatePathCost(List<AStarSearch.Node> path) {
109+
double total = 0;
110+
111+
for (int i = 0; i < path.size() - 1; i++) {
112+
AStarSearch.Node current = path.get(i);
113+
AStarSearch.Node next = path.get(i + 1);
114+
115+
for (AStarSearch.Edge edge : graph.getOrDefault(current, Collections.emptyList())) {
116+
if (edge.target.equals(next)) {
117+
total += edge.cost;
118+
break;
119+
}
120+
}
121+
}
122+
123+
return total;
124+
}
125+
}

0 commit comments

Comments
 (0)