Skip to content

Commit 6b2854b

Browse files
committed
Add Adaptive A* pathfinding with semantic risk cost
1 parent 978a306 commit 6b2854b

2 files changed

Lines changed: 428 additions & 0 deletions

File tree

Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
package com.thealgorithms.datastructures.graphs;
2+
3+
import java.util.ArrayList;
4+
import java.util.Arrays;
5+
import java.util.Comparator;
6+
import java.util.List;
7+
import java.util.PriorityQueue;
8+
9+
/**
10+
* Adaptive A* (A-star) pathfinding algorithm with semantic cost weighting.
11+
*
12+
* This implementation extends the classical A* algorithm by introducing
13+
* a semantic risk cost layer, as proposed in:
14+
*
15+
*
16+
* Hong Yun, "An Adaptive Path Planning Method for Indoor and Outdoor
17+
* Integrated Navigation," 2025 IEEE International Conference on Machine
18+
* Learning and Intelligent Systems Engineering (MLISE 2025).
19+
*
20+
*
21+
* Cost Function
22+
* f(n) = g(n) + h(n) + lambda * R_sem(n)
23+
*
24+
*
25+
*
26+
* g(n) — actual cost from the start node to node n</li>
27+
* h(n) — heuristic estimate from node n to the goal</li>
28+
* lambda — global semantic weight multiplier</li>
29+
* R_sem(n) — per-node semantic risk value
30+
* (e.g., 2.0 for construction zones, 0.5 for sidewalks, 0.0 for normal)
31+
*
32+
*
33+
* The semantic cost enables the algorithm to prefer safer or more convenient
34+
* routes in indoor/outdoor navigation scenarios, such as avoiding construction
35+
* areas, preferring well-lit paths at night, or prioritizing barrier-free routes.
36+
*
37+
* When all semantic risk values are zero and lambda is zero, the algorithm
38+
* behaves identically to classical A*.
39+
*
40+
* Time Complexity: O((V + E) log V) where V is the number of vertices
41+
* and E is the number of edges. In the worst case, this reduces to O(E) when
42+
* the heuristic provides perfect guidance.
43+
*
44+
* @see <a href="https://github.com/TheAlgorithms/Java/blob/master/src/main/java/com/thealgorithms/datastructures/graphs/AStar.java">
45+
* Classical AStar (without semantic cost)</a>
46+
*/
47+
public final class AdaptiveAStar {
48+
49+
private AdaptiveAStar() {
50+
}
51+
52+
/**
53+
* Directed or undirected edge in the graph.
54+
*/
55+
public static class Edge {
56+
private final int from;
57+
private final int to;
58+
private final int weight;
59+
60+
public Edge(int from, int to, int weight) {
61+
this.from = from;
62+
this.to = to;
63+
this.weight = weight;
64+
}
65+
66+
public int getFrom() {
67+
return from;
68+
}
69+
70+
public int getTo() {
71+
return to;
72+
}
73+
74+
public int getWeight() {
75+
return weight;
76+
}
77+
}
78+
79+
/**
80+
* Graph represented as an adjacency list.
81+
*/
82+
public static class Graph {
83+
private final ArrayList<ArrayList<Edge>> adjacencyList;
84+
85+
public Graph(int nodeCount) {
86+
this.adjacencyList = new ArrayList<>(nodeCount);
87+
for (int i = 0; i < nodeCount; i++) {
88+
this.adjacencyList.add(new ArrayList<>());
89+
}
90+
}
91+
92+
/**
93+
* Adds a bidirectional (undirected) edge.
94+
*/
95+
public void addBidirectionalEdge(int from, int to, int weight) {
96+
adjacencyList.get(from).add(new Edge(from, to, weight));
97+
adjacencyList.get(to).add(new Edge(to, from, weight));
98+
}
99+
100+
/**
101+
* Adds a directed edge.
102+
*/
103+
public void addDirectedEdge(int from, int to, int weight) {
104+
adjacencyList.get(from).add(new Edge(from, to, weight));
105+
}
106+
107+
public int nodeCount() {
108+
return adjacencyList.size();
109+
}
110+
111+
public ArrayList<Edge> getNeighbors(int node) {
112+
return adjacencyList.get(node);
113+
}
114+
}
115+
116+
/**
117+
* Holds the result of a pathfinding operation.
118+
*/
119+
public static class PathResult {
120+
private final int totalCost;
121+
private final List<Integer> path;
122+
private final boolean found;
123+
124+
public PathResult(int totalCost, List<Integer> path, boolean found) {
125+
this.totalCost = totalCost;
126+
this.path = path;
127+
this.found = found;
128+
}
129+
130+
public int getTotalCost() {
131+
return totalCost;
132+
}
133+
134+
public List<Integer> getPath() {
135+
return path;
136+
}
137+
138+
public boolean isFound() {
139+
return found;
140+
}
141+
}
142+
143+
/**
144+
* Internal node wrapper used in the priority queue.
145+
*/
146+
private static class NodeState {
147+
final int node;
148+
final int gCost; // actual cost from start
149+
final int fCost; // f(n) = g(n) + h(n) + lambda * R_sem(n)
150+
151+
NodeState(int node, int gCost, int fCost) {
152+
this.node = node;
153+
this.gCost = gCost;
154+
this.fCost = fCost;
155+
}
156+
}
157+
158+
/**
159+
* Runs the Adaptive A* algorithm.
160+
*
161+
* @param start the starting node index
162+
* @param goal the target node index
163+
* @param graph the graph (adjacency list)
164+
* @param heuristic heuristic values h[n] for each node (e.g., Euclidean distance to goal)
165+
* @param semanticRisk per-node semantic risk values (e.g., 0.0 = normal, 2.0 = construction zone)
166+
* @param lambda global semantic weight multiplier
167+
* @return a {@link PathResult} containing the total cost and path if found
168+
*/
169+
public static PathResult findPath(int start, int goal, Graph graph,
170+
int[] heuristic, double[] semanticRisk,
171+
double lambda) {
172+
int nodeCount = graph.nodeCount();
173+
if (start < 0 || start >= nodeCount || goal < 0 || goal >= nodeCount) {
174+
return new PathResult(-1, null, false);
175+
}
176+
177+
// gCost[i] = actual cost from start to node i
178+
int[] gCost = new int[nodeCount];
179+
Arrays.fill(gCost, Integer.MAX_VALUE);
180+
gCost[start] = 0;
181+
182+
// parent[i] = predecessor of node i on the best path
183+
int[] parent = new int[nodeCount];
184+
Arrays.fill(parent, -1);
185+
186+
// closed[i] = true if node i has been fully explored
187+
boolean[] closed = new boolean[nodeCount];
188+
189+
// Priority queue orders by fCost = gCost + heuristic + semantic penalty
190+
PriorityQueue<NodeState> openSet = new PriorityQueue<>(
191+
Comparator.comparingInt(ns -> ns.fCost));
192+
193+
int initialFCost = computeFCost(0, heuristic[start],
194+
semanticRisk[start], lambda);
195+
openSet.add(new NodeState(start, 0, initialFCost));
196+
197+
while (!openSet.isEmpty()) {
198+
NodeState current = openSet.poll();
199+
200+
// If the current node is the goal, reconstruct and return the path
201+
if (current.node == goal) {
202+
List<Integer> path = reconstructPath(parent, goal);
203+
return new PathResult(current.gCost, path, true);
204+
}
205+
206+
if (closed[current.node]) {
207+
continue;
208+
}
209+
closed[current.node] = true;
210+
211+
// Expand neighbors
212+
for (Edge edge : graph.getNeighbors(current.node)) {
213+
int neighbor = edge.getTo();
214+
215+
if (closed[neighbor]) {
216+
continue;
217+
}
218+
219+
int tentativeGCost = current.gCost + edge.getWeight();
220+
221+
if (tentativeGCost < gCost[neighbor]) {
222+
gCost[neighbor] = tentativeGCost;
223+
parent[neighbor] = current.node;
224+
225+
int fCost = computeFCost(tentativeGCost, heuristic[neighbor],
226+
semanticRisk[neighbor], lambda);
227+
openSet.add(new NodeState(neighbor, tentativeGCost, fCost));
228+
}
229+
}
230+
}
231+
232+
return new PathResult(-1, null, false);
233+
}
234+
235+
/**
236+
* Computes the adaptive cost function:
237+
* <pre>f(n) = g(n) + h(n) + lambda * R_sem(n)</pre>
238+
*
239+
* @param gCost actual cost from start to current node
240+
* @param heuristic heuristic estimate to goal
241+
* @param semanticRisk per-node semantic risk
242+
* @param lambda semantic weight multiplier
243+
* @return the total f-cost
244+
*/
245+
private static int computeFCost(int gCost, int heuristic,
246+
double semanticRisk, double lambda) {
247+
int semanticPenalty = (int) Math.round(lambda * semanticRisk);
248+
return gCost + heuristic + semanticPenalty;
249+
}
250+
251+
/**
252+
* Reconstructs the path from start to goal using the parent array.
253+
*/
254+
private static List<Integer> reconstructPath(int[] parent, int goal) {
255+
List<Integer> path = new ArrayList<>();
256+
int current = goal;
257+
while (current != -1) {
258+
path.add(0, current);
259+
current = parent[current];
260+
}
261+
return path;
262+
}
263+
}

0 commit comments

Comments
 (0)