diff --git a/src/main/java/com/thealgorithms/others/AStarSearch.java b/src/main/java/com/thealgorithms/others/AStarSearch.java new file mode 100644 index 000000000000..013d597657f2 --- /dev/null +++ b/src/main/java/com/thealgorithms/others/AStarSearch.java @@ -0,0 +1,152 @@ +package com.thealgorithms.others; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.PriorityQueue; + +/** + * A* (A-Star) Search Algorithm implementation for finding the shortest path + * between two nodes in a graph. + * + *

This algorithm uses a heuristic to guide its search, making it more efficient + * than Dijkstra’s algorithm in many cases. + * + *

f(n) = g(n) + h(n) + * where: + * - g(n): cost from start node to current node + * - h(n): estimated cost from current node to goal (heuristic) + * + *

Time Complexity: + * - Worst case: O(E log V) + * + *

Space Complexity: + * - O(V) + * + *

Use Cases: + * - Pathfinding (maps, games) + * - AI planning + * + * @author Suraj Devatha + */ +public final class AStarSearch { + + private AStarSearch() { + } + + /** + * Finds shortest path using A*. + * + * @param graph adjacency list + * @param start start node + * @param goal goal node + * @param heuristic heuristic function + * @return list of nodes representing shortest path + */ + public static List findPath(Map> graph, Node start, Node goal, Heuristic heuristic) { + + Map gScore = new HashMap<>(); + Map fScore = new HashMap<>(); + Map cameFrom = new HashMap<>(); + + PriorityQueue openSet = new PriorityQueue<>(Comparator.comparingDouble(fScore::get)); + + gScore.put(start, 0.0); + fScore.put(start, heuristic.estimate(start, goal)); + + openSet.add(start); + + while (!openSet.isEmpty()) { + Node current = openSet.poll(); + + if (current.equals(goal)) { + return reconstructPath(cameFrom, current); + } + + for (Edge edge : graph.getOrDefault(current, Collections.emptyList())) { + Node neighbor = edge.target; + double tentativeG = gScore.get(current) + edge.cost; + + if (tentativeG < gScore.getOrDefault(neighbor, Double.POSITIVE_INFINITY)) { + cameFrom.put(neighbor, current); + gScore.put(neighbor, tentativeG); + fScore.put(neighbor, tentativeG + heuristic.estimate(neighbor, goal)); + + if (!openSet.contains(neighbor)) { + openSet.add(neighbor); + } + } + } + } + + return Collections.emptyList(); // No path found + } + + /** + * Reconstructs path from goal to start. + */ + private static List reconstructPath(Map cameFrom, Node current) { + List path = new ArrayList<>(); + path.add(current); + + while (cameFrom.containsKey(current)) { + current = cameFrom.get(current); + path.add(current); + } + + Collections.reverse(path); + return path; + } + + /** + * Heuristic interface (can plug different heuristics). + */ + public interface Heuristic { + double estimate(Node current, Node goal); + } + + /** + * Node class representing a vertex in the graph. + */ + static class Node { + String id; + + Node(String id) { + this.id = id; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof Node)) { + return false; + } + Node node = (Node) o; + return Objects.equals(id, node.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } + } + + /** + * Edge class representing weighted connections. + */ + static class Edge { + Node target; + double cost; + + Edge(Node target, double cost) { + this.target = target; + this.cost = cost; + } + } +} diff --git a/src/test/java/com/thealgorithms/others/AStarSearchTest.java b/src/test/java/com/thealgorithms/others/AStarSearchTest.java new file mode 100644 index 000000000000..62debd27a165 --- /dev/null +++ b/src/test/java/com/thealgorithms/others/AStarSearchTest.java @@ -0,0 +1,116 @@ +package com.thealgorithms.others; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +/** + * Test cases for AStarSearch algorithm. + */ +class AStarSearchTest { + + /** + * Simple heuristic that returns 0 (reduces A* to Dijkstra). + */ + private static final AStarSearch.Heuristic ZERO_HEURISTIC = (node, goal) -> 0; + private static Map> graph; + + private static AStarSearch.Node nodeA; + private static AStarSearch.Node nodeB; + private static AStarSearch.Node nodeC; + private static AStarSearch.Node nodeD; + + @BeforeAll + static void setUp() { + graph = new HashMap<>(); + + nodeA = new AStarSearch.Node("A"); + nodeB = new AStarSearch.Node("B"); + nodeC = new AStarSearch.Node("C"); + nodeD = new AStarSearch.Node("D"); + + graph.put(nodeA, Arrays.asList(new AStarSearch.Edge(nodeB, 1), new AStarSearch.Edge(nodeC, 4))); + + graph.put(nodeB, Arrays.asList(new AStarSearch.Edge(nodeC, 2), new AStarSearch.Edge(nodeD, 5))); + + graph.put(nodeC, Arrays.asList(new AStarSearch.Edge(nodeD, 1))); + + graph.put(nodeD, Collections.emptyList()); + } + + @Test + void testPathExists() { + List path = AStarSearch.findPath(graph, nodeA, nodeD, ZERO_HEURISTIC); + + // Expected shortest path: A -> B -> C -> D + List expected = Arrays.asList(nodeA, nodeB, nodeC, nodeD); + + assertEquals(expected, path); + } + + @Test + void testDirectPath() { + List path = AStarSearch.findPath(graph, nodeA, nodeB, ZERO_HEURISTIC); + + List expected = Arrays.asList(nodeA, nodeB); + + assertEquals(expected, path); + } + + @Test + void testStartEqualsGoal() { + List path = AStarSearch.findPath(graph, nodeA, nodeA, ZERO_HEURISTIC); + + List expected = Collections.singletonList(nodeA); + + assertEquals(expected, path); + } + + @Test + void testNoPathExists() { + AStarSearch.Node nodeE = new AStarSearch.Node("nodeE"); + + List path = AStarSearch.findPath(graph, nodeE, nodeA, ZERO_HEURISTIC); + + assertTrue(path.isEmpty()); + } + + @Test + void testPathCostOptimality() { + List path = AStarSearch.findPath(graph, nodeA, nodeD, ZERO_HEURISTIC); + + // Calculate total cost + double cost = calculatePathCost(path); + + // Expected shortest cost = 1 (A->B) + 2 (B->C) + 1 (C->D) = 4 + assertEquals(4.0, cost); + } + + /** + * Utility method to calculate path cost. + */ + private double calculatePathCost(List path) { + double total = 0; + + for (int i = 0; i < path.size() - 1; i++) { + AStarSearch.Node current = path.get(i); + AStarSearch.Node next = path.get(i + 1); + + for (AStarSearch.Edge edge : graph.getOrDefault(current, Collections.emptyList())) { + if (edge.target.equals(next)) { + total += edge.cost; + break; + } + } + } + + return total; + } +}