Skip to content

Commit 6409e8c

Browse files
piotrmachaabranhe
authored andcommitted
Add A* algorithm for pathfinding in a graph
1 parent 8d07bbc commit 6409e8c

File tree

1 file changed

+219
-0
lines changed

1 file changed

+219
-0
lines changed

graph/AStarPathfinding.java

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
import java.util.*;
2+
3+
/**
4+
* A* Pathfinding algorithm
5+
*
6+
* It searches for shortest path between two graph nodes using
7+
* heuristic estimation of a distance to lower the amount of
8+
* nodes to be visited.
9+
*
10+
* It works well in cases of finding a path between geographical
11+
* objects (like cities or intersections) using connections
12+
* between them (roads).
13+
*
14+
* It minimizes a function: f(x) = g(x) + h(x) where:
15+
* g(x) is computed distance from start to x node
16+
* h(x) is heuristic distance from x node to the target
17+
* f(x) is the total cost of getting from start to goal through x
18+
*
19+
* In this example the heurestic distance is an euclidean distance
20+
* (every node has a cartesian coordinates assigned)
21+
*
22+
* Wiki page about the algorithm:
23+
* https://en.wikipedia.org/wiki/A*_search_algorithm
24+
*
25+
* ===
26+
*
27+
* The algorithm is implemented in @see Graph.findShortestPathUsingAStar()
28+
* The rest of the code helps keeping implementation clear.
29+
*
30+
* ===
31+
*
32+
* @author Piotr Macha <piotr.macha@owlitdevelopment.com>
33+
*/
34+
35+
final class Graph {
36+
private final Map<String, Node> nodes = new HashMap<>();
37+
38+
List<Node> findShortestPathUsingAStar(String startName, String goalName) {
39+
Node start = nodes.get(startName);
40+
Node goal = nodes.get(goalName);
41+
42+
// Nodes already visited
43+
Set<Node> closedSet = new HashSet<>();
44+
45+
// Nodes already known but not visited, we put start node as first element
46+
Set<Node> openSet = new HashSet<>();
47+
openSet.add(start);
48+
49+
// Map to rebuild path when we found a route
50+
Map<Node, Node> cameFrom = new HashMap<>();
51+
52+
// g(x), g-score contains cost to get from start to given node
53+
// We initialize it as 0 for start node and infinite for others
54+
Map<Node, Distance> gScore = new HashMap<>();
55+
for (Node node : nodes.values()) {
56+
gScore.put(node, node == start ? Distance.real(0) : Distance.infinite());
57+
}
58+
59+
// f(x), f-score is total cost of getting from start to goal thought a specific node
60+
// We initialize it as heuristic distance estimate for start and infinity for others
61+
Map<Node, Distance> fScore = new HashMap<>();
62+
for (Node node : nodes.values()) {
63+
fScore.put(node, node == start ? node.euqlideanDistanceTo(goal) : Distance.infinite());
64+
}
65+
66+
while (!openSet.isEmpty()) {
67+
// Find a node in open set with the lowest f-score
68+
Node current = openSet.stream().min(Comparator.comparing(fScore::get)).get();
69+
70+
if (current == goal) {
71+
// We found the path and now we can reconstruct it using cameFrom map
72+
List<Node> path = new ArrayList<>();
73+
path.add(current);
74+
while (cameFrom.keySet().contains(current)) {
75+
current = cameFrom.get(current);
76+
path.add(current);
77+
}
78+
Collections.reverse(path);
79+
return path;
80+
}
81+
82+
// Mark node as visited by swapping its set
83+
openSet.remove(current);
84+
closedSet.add(current);
85+
86+
for (Map.Entry<Node, Distance> neighborEntry : current.edges.entrySet()) {
87+
Node neighbor = neighborEntry.getKey();
88+
Distance distance = neighborEntry.getValue();
89+
90+
if (closedSet.contains(neighbor)) {
91+
// Ignore neighbor if it was already evaluated
92+
continue;
93+
}
94+
95+
// Might be a new g-score for current
96+
Distance gScoreMaybe = gScore.get(current).add(distance);
97+
98+
if (!openSet.contains(neighbor)) {
99+
// We'll evaluate the newly discovered node later
100+
openSet.add(neighbor);
101+
} else if (gScoreMaybe.compareTo(gScore.get(neighbor)) >= 0) {
102+
// Path is worse than already discovered
103+
continue;
104+
}
105+
106+
// We'll use it later to reconstruct the path
107+
cameFrom.put(neighbor, current);
108+
109+
// We set f(x) as g(x) + h(x) (heuristic distance)
110+
gScore.put(neighbor, gScoreMaybe);
111+
fScore.put(neighbor, gScoreMaybe.add(neighbor.euqlideanDistanceTo(goal)));
112+
}
113+
}
114+
115+
throw new RuntimeException("A* reached end without finding a route (unexpected case)");
116+
}
117+
118+
Graph add(Node node) {
119+
this.nodes.put(node.name, node);
120+
return this;
121+
}
122+
123+
Graph edge(String from, String to, double distance) {
124+
Node a = nodes.get(from);
125+
Node b = nodes.get(to);
126+
a.edges.put(b, Distance.real(distance));
127+
b.edges.put(a, Distance.real(distance));
128+
return this;
129+
}
130+
131+
final static class Distance implements Comparable<Distance> {
132+
private final boolean infinite;
133+
private final double distance;
134+
135+
private Distance(boolean infinite, double distance) {
136+
this.infinite = infinite;
137+
this.distance = distance;
138+
}
139+
140+
Distance add(Distance o) {
141+
return new Distance(this.infinite && o.infinite, this.distance + o.distance);
142+
}
143+
144+
static Distance infinite() {
145+
return new Distance(true, 0);
146+
}
147+
148+
static Distance real(double distance) {
149+
return new Distance(false, distance);
150+
}
151+
152+
@Override
153+
public int compareTo(Distance o) {
154+
if (o.infinite && this.infinite || (!o.infinite && !this.infinite && o.distance == this.distance)) {
155+
return 0;
156+
}
157+
158+
if (o.infinite || (!this.infinite && this.distance < o.distance)) {
159+
return -1;
160+
}
161+
162+
return 1;
163+
}
164+
}
165+
166+
final static class Node {
167+
private final String name;
168+
private final double positionX;
169+
private final double positionY;
170+
private final Map<Node, Distance> edges;
171+
172+
Node(String name, double positionX, double positionY) {
173+
this.name = name;
174+
this.positionX = positionX;
175+
this.positionY = positionY;
176+
this.edges = new HashMap<>();
177+
}
178+
179+
String getName() {
180+
return name;
181+
}
182+
183+
Distance euqlideanDistanceTo(Node o) {
184+
return Distance.real(Math.sqrt(Math.pow(positionX - o.positionX, 2) + Math.pow(positionY - o.positionY, 2)));
185+
}
186+
}
187+
}
188+
189+
public class AStarPathfinding {
190+
public static void main(String args[]) {
191+
(new Graph())
192+
.add(new Graph.Node("A", 1, 4))
193+
.add(new Graph.Node("B", 1, 3))
194+
.add(new Graph.Node("C", 2, 3))
195+
.add(new Graph.Node("D", 3, 4))
196+
.add(new Graph.Node("E", 1, 2))
197+
.add(new Graph.Node("F", 3, 2))
198+
.add(new Graph.Node("G", 2, 1))
199+
.add(new Graph.Node("H", 1, 0))
200+
.add(new Graph.Node("I", 0, 2))
201+
.add(new Graph.Node("J", 0, 0))
202+
.edge("A", "B", 1.1)
203+
.edge("A", "C", 1.47)
204+
.edge("A", "D", 2.2)
205+
.edge("C", "E", 1.43)
206+
.edge("D", "F", 2.8)
207+
.edge("E", "I", 1.01)
208+
.edge("I", "J", 1.1)
209+
.edge("J", "H", 1.12)
210+
.edge("E", "G", 3.44)
211+
.edge("F", "G", 1.44)
212+
.edge("H", "G", 1.42)
213+
.findShortestPathUsingAStar("A", "H")
214+
.stream()
215+
.map(Graph.Node::getName)
216+
.forEach(name -> System.out.println("Route step: " + name));
217+
;
218+
}
219+
}

0 commit comments

Comments
 (0)