Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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)
*
* <p>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.
*
* <p>Stops early once the target node is settled, so it does not necessarily
* visit the entire graph.
*
* <p>Time: O(E log E)
* <p>Space: O(V + E)
*
* @author William Fiset, william.alexandre.fiset@gmail.com
*/
Expand All @@ -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<List<int[]>> graph;

private int n;
private double[] dist;
private Integer[] prev;
private List<List<Edge>> graph;

private Comparator<Node> comparator =
new Comparator<Node>() {
@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<Node> 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<List<Edge>> 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<Integer> 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<Integer> 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<Node> 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<double[]> 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<Edge> 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<Integer> reconstructPath(int start, int end) {
double d = dijkstra(start, end);
if (d == Double.POSITIVE_INFINITY) {
return List.of();
}
List<Integer> 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));
}
}
Loading