diff --git a/src/main/java/com/williamfiset/algorithms/graphtheory/DijkstrasShortestPathAdjacencyList.java b/src/main/java/com/williamfiset/algorithms/graphtheory/DijkstrasShortestPathAdjacencyList.java index f1639bc04..3305b7f66 100644 --- a/src/main/java/com/williamfiset/algorithms/graphtheory/DijkstrasShortestPathAdjacencyList.java +++ b/src/main/java/com/williamfiset/algorithms/graphtheory/DijkstrasShortestPathAdjacencyList.java @@ -11,18 +11,13 @@ import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; -import java.util.Comparator; +import java.util.LinkedList; 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. + // Represents a directed edge between two nodes with a certain cost. public static class Edge { double cost; int from, to; @@ -34,8 +29,8 @@ public Edge(int from, int to, double cost) { } } - // Node class to track the nodes to visit while running Dijkstra's - public static class Node { + // Represents a node-distance pair for the priority queue. + public static class Node implements Comparable { int id; double value; @@ -43,52 +38,42 @@ public Node(int id, double value) { this.id = id; this.value = value; } + + @Override + public int compareTo(Node other) { + return Double.compare(this.value, other.value); + } } - private int n; + private final 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; - } - }; + private final List> graph; /** - * Initialize the solver by providing the graph size and a starting node. Use the {@link #addEdge} - * method to actually add edges to the graph. + * Initialize the solver by providing the graph size. Use the {@link #addEdge} method to 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 to - The index of the node the directed edge ends at. * @param cost - The cost of the edge. */ 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; } @@ -96,35 +81,34 @@ public List> getGraph() { /** * Reconstructs the shortest path (of nodes) from 'start' to 'end' inclusive. * - * @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 An array of node indexes of the shortest path from 'start' to 'end'. If 'start' and + * 'end' are not connected then an empty list is returned. */ 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); + dijkstra(start, end); + LinkedList path = new LinkedList<>(); + if (dist[end] == Double.POSITIVE_INFINITY) + return path; + for (int at = end; at != start; at = prev[at]) + path.addFirst(at); + path.addFirst(start); 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. + /** + * Runs 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 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); + PriorityQueue pq = new PriorityQueue<>(); 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]; @@ -132,17 +116,13 @@ public double dijkstra(int start, int end) { 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); + // We already found a better path before we got to processing this node. + if (dist[node.id] < node.value) + continue; - // You cannot get a shorter path by revisiting - // a node you have already visited before. - if (visited[edge.to]) continue; + for (Edge edge : graph.get(node.id)) { + if (visited[edge.to]) + continue; // Relax edge by updating minimum cost if applicable. double newDist = dist[edge.from] + edge.cost; @@ -152,18 +132,12 @@ public double dijkstra(int start, int end) { pq.offer(new Node(edge.to, dist[edge.to])); } } - // 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]; + + // Once we've processed the end node we can return early because the + // distance cannot improve after this point. + if (node.id == 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<>()); - } } diff --git a/src/main/java/com/williamfiset/algorithms/graphtheory/DijkstrasShortestPathAdjacencyListWithDHeap.java b/src/main/java/com/williamfiset/algorithms/graphtheory/DijkstrasShortestPathAdjacencyListWithDHeap.java index 0536c4e26..3ec8a0373 100644 --- a/src/main/java/com/williamfiset/algorithms/graphtheory/DijkstrasShortestPathAdjacencyListWithDHeap.java +++ b/src/main/java/com/williamfiset/algorithms/graphtheory/DijkstrasShortestPathAdjacencyListWithDHeap.java @@ -12,14 +12,13 @@ import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; +import java.util.LinkedList; import java.util.List; import java.util.NoSuchElementException; public class DijkstrasShortestPathAdjacencyListWithDHeap { - // An edge class to represent a directed edge - // between two nodes with a certain cost. + // Represents a directed edge to a node with a certain cost. public static class Edge { int to; double cost; @@ -35,30 +34,26 @@ public Edge(int to, double cost) { private int edgeCount; private double[] dist; private Integer[] prev; - private List> graph; + private final List> graph; /** - * Initialize the solver by providing the graph size and a starting node. Use the {@link #addEdge} - * method to actually add edges to the graph. + * Initialize the solver by providing the graph size. Use the {@link #addEdge} method to add edges + * to the graph. * * @param n - The number of nodes in the graph. */ public DijkstrasShortestPathAdjacencyListWithDHeap(int n) { this.n = n; - createEmptyGraph(); - } - - // 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<>()); + 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 to - The index of the node the directed edge ends at. * @param cost - The cost of the edge. */ public void addEdge(int from, int to, int cost) { @@ -66,27 +61,22 @@ public void addEdge(int from, int to, int cost) { graph.get(from).add(new Edge(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; } - // 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. + /** + * Runs 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 Double.POSITIVE_INFINITY. + */ public double dijkstra(int start, int end) { - // Keep an Indexed Priority Queue (ipq) of the next most promising node - // to visit. + // Keep an Indexed Priority Queue (ipq) of the next most promising node to visit. int degree = edgeCount / n; MinIndexedDHeap ipq = new MinIndexedDHeap<>(degree, n); ipq.insert(start, 0.0); - // Maintain an array of the minimum distance to each node. dist = new double[n]; Arrays.fill(dist, Double.POSITIVE_INFINITY); dist[start] = 0.0; @@ -100,15 +90,13 @@ public double dijkstra(int start, int end) { visited[nodeId] = true; double minValue = ipq.pollMinValue(); - // We already found a better path before we got to - // processing this node so we can ignore it. - if (minValue > dist[nodeId]) continue; + // We already found a better path before we got to processing this node. + if (minValue > dist[nodeId]) + continue; for (Edge edge : graph.get(nodeId)) { - - // We cannot get a shorter path by revisiting - // a node we have already visited before. - if (visited[edge.to]) continue; + if (visited[edge.to]) + continue; // Relax edge by updating minimum cost if applicable. double newDist = dist[nodeId] + edge.cost; @@ -117,17 +105,18 @@ public double dijkstra(int start, int end) { dist[edge.to] = newDist; // Insert the cost of going to a node for the first time in the PQ, // or try and update it to a better value by calling decrease. - if (!ipq.contains(edge.to)) ipq.insert(edge.to, newDist); - else ipq.decrease(edge.to, newDist); + if (!ipq.contains(edge.to)) + ipq.insert(edge.to, newDist); + else + ipq.decrease(edge.to, newDist); } } - // Once we've processed the end node we can return early (without - // necessarily visiting the whole graph) because we know we cannot get a - // shorter path by routing through any other nodes since Dijkstra's is - // greedy and there are no negative edge weights. - if (nodeId == end) return dist[end]; + + // Once we've processed the end node we can return early because the + // distance cannot improve after this point. + if (nodeId == end) + return dist[end]; } - // End node is unreachable. return Double.POSITIVE_INFINITY; } @@ -135,16 +124,16 @@ public double dijkstra(int start, int end) { * Reconstructs the shortest path (of nodes) from 'start' to 'end' inclusive. * * @return An array of node indexes of the shortest path from 'start' to 'end'. If 'start' and - * 'end' are not connected then an empty array is returned. + * 'end' are not connected then an empty list is returned. */ 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"); - List path = new ArrayList<>(); - double dist = dijkstra(start, end); - if (dist == Double.POSITIVE_INFINITY) return path; - for (Integer at = end; at != null; at = prev[at]) path.add(at); - Collections.reverse(path); + dijkstra(start, end); + LinkedList path = new LinkedList<>(); + if (dist[end] == Double.POSITIVE_INFINITY) + return path; + for (int at = end; at != start; at = prev[at]) + path.addFirst(at); + path.addFirst(start); return path; } @@ -171,13 +160,14 @@ private static class MinIndexedDHeap> { // 'im' and 'pm' are inverses of each other, so: pm[im[i]] = im[pm[i]] = i public final int[] im; - // The values associated with the keys. It is very important to note + // The values associated with the keys. It is very important to note // that this array is indexed by the key indexes (aka 'ki'). public final Object[] values; // Initializes a D-ary heap with a maximum capacity of maxSize. public MinIndexedDHeap(int degree, int maxSize) { - if (maxSize <= 0) throw new IllegalArgumentException("maxSize <= 0"); + if (maxSize <= 0) + throw new IllegalArgumentException("maxSize <= 0"); D = max(2, degree); N = max(D + 1, maxSize); @@ -232,7 +222,8 @@ public T pollMinValue() { } public void insert(int ki, T value) { - if (contains(ki)) throw new IllegalArgumentException("index already exists; received: " + ki); + if (contains(ki)) + throw new IllegalArgumentException("index already exists; received: " + ki); valueNotNullOrThrow(value); pm[ki] = sz; im[sz] = ki; @@ -309,7 +300,9 @@ private void swim(int i) { // From the parent node at index i find the minimum child below it private int minChild(int i) { int index = -1, from = child[i], to = min(sz, from + D); - for (int j = from; j < to; j++) if (less(j, i)) index = i = j; + for (int j = from; j < to; j++) + if (less(j, i)) + index = i = j; return index; } @@ -335,14 +328,16 @@ private boolean less(Object obj1, Object obj2) { @Override public String toString() { List lst = new ArrayList<>(sz); - for (int i = 0; i < sz; i++) lst.add(im[i]); + for (int i = 0; i < sz; i++) + lst.add(im[i]); return lst.toString(); } /* Helper functions to make the code more readable. */ private void isNotEmptyOrThrow() { - if (isEmpty()) throw new NoSuchElementException("Priority queue underflow"); + if (isEmpty()) + throw new NoSuchElementException("Priority queue underflow"); } private void keyExistsAndValueNotNullOrThrow(int ki, Object value) { @@ -351,11 +346,13 @@ private void keyExistsAndValueNotNullOrThrow(int ki, Object value) { } private void keyExistsOrThrow(int ki) { - if (!contains(ki)) throw new NoSuchElementException("Index does not exist; received: " + ki); + if (!contains(ki)) + throw new NoSuchElementException("Index does not exist; received: " + ki); } private void valueNotNullOrThrow(Object value) { - if (value == null) throw new IllegalArgumentException("value cannot be null"); + if (value == null) + throw new IllegalArgumentException("value cannot be null"); } private void keyInBoundsOrThrow(int ki) { diff --git a/src/test/java/com/williamfiset/algorithms/graphtheory/BUILD b/src/test/java/com/williamfiset/algorithms/graphtheory/BUILD index 930c75d75..965b96e39 100644 --- a/src/test/java/com/williamfiset/algorithms/graphtheory/BUILD +++ b/src/test/java/com/williamfiset/algorithms/graphtheory/BUILD @@ -178,5 +178,27 @@ java_test( deps = TEST_DEPS, ) +# bazel test //src/test/java/com/williamfiset/algorithms/graphtheory:DijkstrasShortestPathAdjacencyListTest +java_test( + name = "DijkstrasShortestPathAdjacencyListTest", + srcs = ["DijkstrasShortestPathAdjacencyListTest.java"], + main_class = "org.junit.platform.console.ConsoleLauncher", + use_testrunner = False, + args = ["--select-class=com.williamfiset.algorithms.graphtheory.DijkstrasShortestPathAdjacencyListTest"], + runtime_deps = JUNIT5_RUNTIME_DEPS, + deps = TEST_DEPS, +) + +# bazel test //src/test/java/com/williamfiset/algorithms/graphtheory:DijkstrasShortestPathAdjacencyListWithDHeapTest +java_test( + name = "DijkstrasShortestPathAdjacencyListWithDHeapTest", + srcs = ["DijkstrasShortestPathAdjacencyListWithDHeapTest.java"], + main_class = "org.junit.platform.console.ConsoleLauncher", + use_testrunner = False, + args = ["--select-class=com.williamfiset.algorithms.graphtheory.DijkstrasShortestPathAdjacencyListWithDHeapTest"], + runtime_deps = JUNIT5_RUNTIME_DEPS, + deps = TEST_DEPS, +) + # Run all tests # bazel test //src/test/java/com/williamfiset/algorithms/graphtheory:all diff --git a/src/test/java/com/williamfiset/algorithms/graphtheory/DijkstrasShortestPathAdjacencyListTest.java b/src/test/java/com/williamfiset/algorithms/graphtheory/DijkstrasShortestPathAdjacencyListTest.java new file mode 100644 index 000000000..c4925721b --- /dev/null +++ b/src/test/java/com/williamfiset/algorithms/graphtheory/DijkstrasShortestPathAdjacencyListTest.java @@ -0,0 +1,144 @@ +package com.williamfiset.algorithms.graphtheory; + +import static com.google.common.truth.Truth.assertThat; + +import java.util.List; +import org.junit.jupiter.api.Test; + +public class DijkstrasShortestPathAdjacencyListTest { + + @Test + public void singleNode() { + DijkstrasShortestPathAdjacencyList solver = new DijkstrasShortestPathAdjacencyList(1); + double dist = solver.dijkstra(0, 0); + assertThat(dist).isEqualTo(0.0); + } + + @Test + public void twoNodesDirectEdge() { + DijkstrasShortestPathAdjacencyList solver = new DijkstrasShortestPathAdjacencyList(2); + solver.addEdge(0, 1, 5); + + double dist = solver.dijkstra(0, 1); + + assertThat(dist).isEqualTo(5.0); + } + + @Test + public void unreachableNodeReturnsInfinity() { + DijkstrasShortestPathAdjacencyList solver = new DijkstrasShortestPathAdjacencyList(3); + solver.addEdge(0, 1, 5); + + double dist = solver.dijkstra(0, 2); + + assertThat(dist).isPositiveInfinity(); + } + + @Test + public void shortestPathChosenOverLonger() { + DijkstrasShortestPathAdjacencyList solver = new DijkstrasShortestPathAdjacencyList(3); + solver.addEdge(0, 2, 10); + solver.addEdge(0, 1, 3); + solver.addEdge(1, 2, 4); + + double dist = solver.dijkstra(0, 2); + + assertThat(dist).isEqualTo(7.0); + } + + @Test + public void reconstructPathSimple() { + DijkstrasShortestPathAdjacencyList solver = new DijkstrasShortestPathAdjacencyList(4); + solver.addEdge(0, 1, 1); + solver.addEdge(1, 2, 1); + solver.addEdge(2, 3, 1); + + List path = solver.reconstructPath(0, 3); + + assertThat(path).containsExactly(0, 1, 2, 3).inOrder(); + } + + @Test + public void reconstructPathChoosesShortest() { + DijkstrasShortestPathAdjacencyList solver = new DijkstrasShortestPathAdjacencyList(3); + solver.addEdge(0, 2, 10); + solver.addEdge(0, 1, 3); + solver.addEdge(1, 2, 4); + + List path = solver.reconstructPath(0, 2); + + assertThat(path).containsExactly(0, 1, 2).inOrder(); + } + + @Test + public void reconstructPathUnreachableReturnsEmpty() { + DijkstrasShortestPathAdjacencyList solver = new DijkstrasShortestPathAdjacencyList(3); + solver.addEdge(0, 1, 5); + + List path = solver.reconstructPath(0, 2); + + assertThat(path).isEmpty(); + } + + @Test + public void reconstructPathStartEqualsEnd() { + DijkstrasShortestPathAdjacencyList solver = new DijkstrasShortestPathAdjacencyList(2); + solver.addEdge(0, 1, 1); + + List path = solver.reconstructPath(0, 0); + + assertThat(path).containsExactly(0); + } + + @Test + public void directedEdgeNotTraversedBackwards() { + DijkstrasShortestPathAdjacencyList solver = new DijkstrasShortestPathAdjacencyList(2); + solver.addEdge(0, 1, 5); + + double dist = solver.dijkstra(1, 0); + + assertThat(dist).isPositiveInfinity(); + } + + @Test + public void longerPathWithMoreHops() { + // 0→1→2→3→4 with cost 1 each (total 4) vs 0→4 with cost 100 + DijkstrasShortestPathAdjacencyList solver = new DijkstrasShortestPathAdjacencyList(5); + solver.addEdge(0, 4, 100); + solver.addEdge(0, 1, 1); + solver.addEdge(1, 2, 1); + solver.addEdge(2, 3, 1); + solver.addEdge(3, 4, 1); + + double dist = solver.dijkstra(0, 4); + List path = solver.reconstructPath(0, 4); + + assertThat(dist).isEqualTo(4.0); + assertThat(path).containsExactly(0, 1, 2, 3, 4).inOrder(); + } + + @Test + public void disconnectedGraph() { + DijkstrasShortestPathAdjacencyList solver = new DijkstrasShortestPathAdjacencyList(4); + solver.addEdge(0, 1, 3); + solver.addEdge(2, 3, 1); + + assertThat(solver.dijkstra(0, 1)).isEqualTo(3.0); + assertThat(solver.dijkstra(0, 2)).isPositiveInfinity(); + assertThat(solver.dijkstra(0, 3)).isPositiveInfinity(); + } + + @Test + public void getGraphReturnsAdjacencyList() { + DijkstrasShortestPathAdjacencyList solver = new DijkstrasShortestPathAdjacencyList(3); + solver.addEdge(0, 1, 5); + solver.addEdge(0, 2, 10); + + List> graph = solver.getGraph(); + + assertThat(graph).hasSize(3); + assertThat(graph.get(0)).hasSize(2); + assertThat(graph.get(1)).isEmpty(); + assertThat(graph.get(2)).isEmpty(); + } +} diff --git a/src/test/java/com/williamfiset/algorithms/graphtheory/DijkstrasShortestPathAdjacencyListWithDHeapTest.java b/src/test/java/com/williamfiset/algorithms/graphtheory/DijkstrasShortestPathAdjacencyListWithDHeapTest.java new file mode 100644 index 000000000..b2b8da55b --- /dev/null +++ b/src/test/java/com/williamfiset/algorithms/graphtheory/DijkstrasShortestPathAdjacencyListWithDHeapTest.java @@ -0,0 +1,155 @@ +package com.williamfiset.algorithms.graphtheory; + +import static com.google.common.truth.Truth.assertThat; + +import java.util.List; +import org.junit.jupiter.api.Test; + +public class DijkstrasShortestPathAdjacencyListWithDHeapTest { + + @Test + public void singleNode() { + DijkstrasShortestPathAdjacencyListWithDHeap solver = + new DijkstrasShortestPathAdjacencyListWithDHeap(1); + double dist = solver.dijkstra(0, 0); + assertThat(dist).isEqualTo(0.0); + } + + @Test + public void twoNodesDirectEdge() { + DijkstrasShortestPathAdjacencyListWithDHeap solver = + new DijkstrasShortestPathAdjacencyListWithDHeap(2); + solver.addEdge(0, 1, 5); + + double dist = solver.dijkstra(0, 1); + + assertThat(dist).isEqualTo(5.0); + } + + @Test + public void unreachableNodeReturnsInfinity() { + DijkstrasShortestPathAdjacencyListWithDHeap solver = + new DijkstrasShortestPathAdjacencyListWithDHeap(3); + solver.addEdge(0, 1, 5); + + double dist = solver.dijkstra(0, 2); + + assertThat(dist).isPositiveInfinity(); + } + + @Test + public void shortestPathChosenOverLonger() { + DijkstrasShortestPathAdjacencyListWithDHeap solver = + new DijkstrasShortestPathAdjacencyListWithDHeap(3); + solver.addEdge(0, 2, 10); + solver.addEdge(0, 1, 3); + solver.addEdge(1, 2, 4); + + double dist = solver.dijkstra(0, 2); + + assertThat(dist).isEqualTo(7.0); + } + + @Test + public void reconstructPathSimple() { + DijkstrasShortestPathAdjacencyListWithDHeap solver = + new DijkstrasShortestPathAdjacencyListWithDHeap(4); + solver.addEdge(0, 1, 1); + solver.addEdge(1, 2, 1); + solver.addEdge(2, 3, 1); + + List path = solver.reconstructPath(0, 3); + + assertThat(path).containsExactly(0, 1, 2, 3).inOrder(); + } + + @Test + public void reconstructPathChoosesShortest() { + DijkstrasShortestPathAdjacencyListWithDHeap solver = + new DijkstrasShortestPathAdjacencyListWithDHeap(3); + solver.addEdge(0, 2, 10); + solver.addEdge(0, 1, 3); + solver.addEdge(1, 2, 4); + + List path = solver.reconstructPath(0, 2); + + assertThat(path).containsExactly(0, 1, 2).inOrder(); + } + + @Test + public void reconstructPathUnreachableReturnsEmpty() { + DijkstrasShortestPathAdjacencyListWithDHeap solver = + new DijkstrasShortestPathAdjacencyListWithDHeap(3); + solver.addEdge(0, 1, 5); + + List path = solver.reconstructPath(0, 2); + + assertThat(path).isEmpty(); + } + + @Test + public void reconstructPathStartEqualsEnd() { + DijkstrasShortestPathAdjacencyListWithDHeap solver = + new DijkstrasShortestPathAdjacencyListWithDHeap(2); + solver.addEdge(0, 1, 1); + + List path = solver.reconstructPath(0, 0); + + assertThat(path).containsExactly(0); + } + + @Test + public void directedEdgeNotTraversedBackwards() { + DijkstrasShortestPathAdjacencyListWithDHeap solver = + new DijkstrasShortestPathAdjacencyListWithDHeap(2); + solver.addEdge(0, 1, 5); + + double dist = solver.dijkstra(1, 0); + + assertThat(dist).isPositiveInfinity(); + } + + @Test + public void longerPathWithMoreHops() { + DijkstrasShortestPathAdjacencyListWithDHeap solver = + new DijkstrasShortestPathAdjacencyListWithDHeap(5); + solver.addEdge(0, 4, 100); + solver.addEdge(0, 1, 1); + solver.addEdge(1, 2, 1); + solver.addEdge(2, 3, 1); + solver.addEdge(3, 4, 1); + + double dist = solver.dijkstra(0, 4); + List path = solver.reconstructPath(0, 4); + + assertThat(dist).isEqualTo(4.0); + assertThat(path).containsExactly(0, 1, 2, 3, 4).inOrder(); + } + + @Test + public void disconnectedGraph() { + DijkstrasShortestPathAdjacencyListWithDHeap solver = + new DijkstrasShortestPathAdjacencyListWithDHeap(4); + solver.addEdge(0, 1, 3); + solver.addEdge(2, 3, 1); + + assertThat(solver.dijkstra(0, 1)).isEqualTo(3.0); + assertThat(solver.dijkstra(0, 2)).isPositiveInfinity(); + assertThat(solver.dijkstra(0, 3)).isPositiveInfinity(); + } + + @Test + public void getGraphReturnsAdjacencyList() { + DijkstrasShortestPathAdjacencyListWithDHeap solver = + new DijkstrasShortestPathAdjacencyListWithDHeap(3); + solver.addEdge(0, 1, 5); + solver.addEdge(0, 2, 10); + + List> graph = solver.getGraph(); + + assertThat(graph).hasSize(3); + assertThat(graph.get(0)).hasSize(2); + assertThat(graph.get(1)).isEmpty(); + assertThat(graph.get(2)).isEmpty(); + } +}