diff --git a/src/main/java/com/williamfiset/algorithms/graphtheory/DijkstrasShortestPathAdjacencyList.java b/src/main/java/com/williamfiset/algorithms/graphtheory/DijkstrasShortestPathAdjacencyList.java index f1639bc04..63de9dcbb 100644 --- a/src/main/java/com/williamfiset/algorithms/graphtheory/DijkstrasShortestPathAdjacencyList.java +++ b/src/main/java/com/williamfiset/algorithms/graphtheory/DijkstrasShortestPathAdjacencyList.java @@ -1,9 +1,16 @@ /** - * This file contains an implementation of Dijkstra's shortest path algorithm from a start node to a - * specific ending node. Dijkstra can also be modified to find the shortest path between a starting - * node and all other nodes in the graph. However, in this implementation since we're only going - * from a starting node to an ending node we can employ an optimization to stop early once we've - * visited all the neighbors of the ending node. + * Dijkstra's Shortest Path — Adjacency List (Lazy) + * + *

Finds the shortest path from a source node to a target node in a weighted + * directed graph with non-negative edge weights. Uses a lazy approach with a + * standard {@link java.util.PriorityQueue}: instead of decreasing keys, stale + * entries are simply skipped when polled. + * + *

Stops early once the target node is settled, so it does not necessarily + * visit the entire graph. + * + *

Time: O(E log E) + *

Space: O(V + E) * * @author William Fiset, william.alexandre.fiset@gmail.com */ @@ -12,158 +19,125 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; -import java.util.Comparator; import java.util.List; import java.util.PriorityQueue; public class DijkstrasShortestPathAdjacencyList { - // Small epsilon value to comparing double values. - private static final double EPS = 1e-6; - - // An edge class to represent a directed edge - // between two nodes with a certain cost. - public static class Edge { - double cost; - int from, to; - - public Edge(int from, int to, double cost) { - this.from = from; - this.to = to; - this.cost = cost; - } - } - - // Node class to track the nodes to visit while running Dijkstra's - public static class Node { - int id; - double value; - - public Node(int id, double value) { - this.id = id; - this.value = value; - } - } + private final int n; + private final List> graph; - private int n; private double[] dist; private Integer[] prev; - private List> graph; - - private Comparator comparator = - new Comparator() { - @Override - public int compare(Node node1, Node node2) { - if (Math.abs(node1.value - node2.value) < EPS) return 0; - return (node1.value - node2.value) > 0 ? +1 : -1; - } - }; - /** - * Initialize the solver by providing the graph size and a starting node. Use the {@link #addEdge} - * method to actually add edges to the graph. - * - * @param n - The number of nodes in the graph. - */ public DijkstrasShortestPathAdjacencyList(int n) { this.n = n; - createEmptyGraph(); - } - - public DijkstrasShortestPathAdjacencyList(int n, Comparator comparator) { - this(n); - if (comparator == null) throw new IllegalArgumentException("Comparator cannot be null"); - this.comparator = comparator; + this.graph = new ArrayList<>(n); + for (int i = 0; i < n; i++) { + graph.add(new ArrayList<>()); + } } /** - * Adds a directed edge to the graph. - * - * @param from - The index of the node the directed edge starts at. - * @param to - The index of the node the directed edge end at. - * @param cost - The cost of the edge. + * Adds a directed edge from node {@code from} to node {@code to} with the given cost. */ public void addEdge(int from, int to, int cost) { - graph.get(from).add(new Edge(from, to, cost)); - } - - // Use {@link #addEdge} method to add edges to the graph and use this method - // to retrieve the constructed graph. - public List> getGraph() { - return graph; + graph.get(from).add(new int[] {to, cost}); } /** - * Reconstructs the shortest path (of nodes) from 'start' to 'end' inclusive. + * Runs Dijkstra's algorithm from {@code start} to {@code end}. * - * @return An array of nodes indexes of the shortest path from 'start' to 'end'. If 'start' and - * 'end' are not connected then an empty array is returned. + * @return the shortest distance, or {@code Double.POSITIVE_INFINITY} if unreachable. */ - public List reconstructPath(int start, int end) { - if (end < 0 || end >= n) throw new IllegalArgumentException("Invalid node index"); - if (start < 0 || start >= n) throw new IllegalArgumentException("Invalid node index"); - double dist = dijkstra(start, end); - List path = new ArrayList<>(); - if (dist == Double.POSITIVE_INFINITY) return path; - for (Integer at = end; at != null; at = prev[at]) path.add(at); - Collections.reverse(path); - return path; - } - - // Run Dijkstra's algorithm on a directed graph to find the shortest path - // from a starting node to an ending node. If there is no path between the - // starting node and the destination node the returned value is set to be - // Double.POSITIVE_INFINITY. public double dijkstra(int start, int end) { - // Maintain an array of the minimum distance to each node dist = new double[n]; Arrays.fill(dist, Double.POSITIVE_INFINITY); dist[start] = 0; - // Keep a priority queue of the next most promising node to visit. - PriorityQueue pq = new PriorityQueue<>(2 * n, comparator); - pq.offer(new Node(start, 0)); - - // Array used to track which nodes have already been visited. - boolean[] visited = new boolean[n]; prev = new Integer[n]; + boolean[] visited = new boolean[n]; + + // PQ entries: {nodeId, distance} + PriorityQueue pq = new PriorityQueue<>((a, b) -> Double.compare(a[1], b[1])); + pq.offer(new double[] {start, 0}); while (!pq.isEmpty()) { - Node node = pq.poll(); - visited[node.id] = true; - - // We already found a better path before we got to - // processing this node so we can ignore it. - if (dist[node.id] < node.value) continue; - - List edges = graph.get(node.id); - for (int i = 0; i < edges.size(); i++) { - Edge edge = edges.get(i); - - // You cannot get a shorter path by revisiting - // a node you have already visited before. - if (visited[edge.to]) continue; - - // Relax edge by updating minimum cost if applicable. - double newDist = dist[edge.from] + edge.cost; - if (newDist < dist[edge.to]) { - prev[edge.to] = edge.from; - dist[edge.to] = newDist; - pq.offer(new Node(edge.to, dist[edge.to])); + double[] entry = pq.poll(); + int nodeId = (int) entry[0]; + visited[nodeId] = true; + + // Skip stale entries. + if (entry[1] > dist[nodeId]) { + continue; + } + + for (int[] edge : graph.get(nodeId)) { + int to = edge[0]; + int cost = edge[1]; + if (visited[to]) { + continue; + } + double newDist = dist[nodeId] + cost; + if (newDist < dist[to]) { + dist[to] = newDist; + prev[to] = nodeId; + pq.offer(new double[] {to, newDist}); } } - // Once we've visited all the nodes spanning from the end - // node we know we can return the minimum distance value to - // the end node because it cannot get any better after this point. - if (node.id == end) return dist[end]; + + if (nodeId == end) { + return dist[end]; + } } - // End node is unreachable + return Double.POSITIVE_INFINITY; } - // Construct an empty graph with n nodes including the source and sink nodes. - private void createEmptyGraph() { - graph = new ArrayList<>(n); - for (int i = 0; i < n; i++) graph.add(new ArrayList<>()); + /** + * Returns the shortest path from {@code start} to {@code end} as a list of node ids, + * or an empty list if unreachable. + */ + public List reconstructPath(int start, int end) { + double d = dijkstra(start, end); + if (d == Double.POSITIVE_INFINITY) { + return List.of(); + } + List path = new ArrayList<>(); + for (Integer at = end; at != null; at = prev[at]) { + path.add(at); + } + Collections.reverse(path); + return path; + } + + // ==================== Main ==================== + + // + // 0 ---5---> 1 ---2---> 3 + // | | ^ + // 1 3 | + // | | 1 + // v v | + // 2 ---6---> 4 --------- + // + // Shortest path 0 -> 3: [0, 1, 3] cost 7 + // Shortest path 0 -> 4: [0, 2, 4] cost 7 + // + public static void main(String[] args) { + DijkstrasShortestPathAdjacencyList solver = new DijkstrasShortestPathAdjacencyList(5); + + solver.addEdge(0, 1, 5); + solver.addEdge(0, 2, 1); + solver.addEdge(1, 3, 2); + solver.addEdge(1, 4, 3); + solver.addEdge(2, 4, 6); + solver.addEdge(4, 3, 1); + + System.out.println("Path 0->3: " + solver.reconstructPath(0, 3)); + System.out.printf("Cost 0->3: %.0f%n", solver.dijkstra(0, 3)); + + System.out.println("Path 0->4: " + solver.reconstructPath(0, 4)); + System.out.printf("Cost 0->4: %.0f%n", solver.dijkstra(0, 4)); } }