diff --git a/src/main/java/com/thealgorithms/datastructures/graphs/DialsAlgorithm.java b/src/main/java/com/thealgorithms/datastructures/graphs/DialsAlgorithm.java new file mode 100644 index 000000000000..da3fba88c618 --- /dev/null +++ b/src/main/java/com/thealgorithms/datastructures/graphs/DialsAlgorithm.java @@ -0,0 +1,114 @@ +package com.thealgorithms.datastructures.graphs; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * An implementation of Dial's Algorithm for the single-source shortest path problem. + * This algorithm is an optimization of Dijkstra's algorithm and is particularly + * efficient for graphs with small, non-negative integer edge weights. + * + * It uses a bucket queue (implemented here as a List of HashSets) to store vertices, + * where each bucket corresponds to a specific distance from the source. This is more + * efficient than a standard priority queue when the range of edge weights is small. + * + * Time Complexity: O(E + W * V), where E is the number of edges, V is the number + * of vertices, and W is the maximum weight of any edge. + * + * @see Wikipedia - Dial's Algorithm + */ +public final class DialsAlgorithm { + /** + * Private constructor to prevent instantiation of this utility class. + */ + private DialsAlgorithm() { + } + /** + * Represents an edge in the graph, connecting to a destination vertex with a given weight. + */ + public static class Edge { + private final int destination; + private final int weight; + + public Edge(int destination, int weight) { + this.destination = destination; + this.weight = weight; + } + + public int getDestination() { + return destination; + } + + public int getWeight() { + return weight; + } + } + /** + * Finds the shortest paths from a source vertex to all other vertices in a weighted graph. + * + * @param graph The graph represented as an adjacency list. + * @param source The source vertex to start from (0-indexed). + * @param maxEdgeWeight The maximum weight of any single edge in the graph. + * @return An array of integers where the value at each index `i` is the + * shortest distance from the source to vertex `i`. Unreachable vertices + * will have a value of Integer.MAX_VALUE. + * @throws IllegalArgumentException if the source vertex is out of bounds. + */ + public static int[] run(List> graph, int source, int maxEdgeWeight) { + int numVertices = graph.size(); + if (source < 0 || source >= numVertices) { + throw new IllegalArgumentException("Source vertex is out of bounds."); + } + + // Initialize distances array + int[] distances = new int[numVertices]; + Arrays.fill(distances, Integer.MAX_VALUE); + distances[source] = 0; + + // The bucket queue. Size is determined by the max possible path length. + int maxPathWeight = maxEdgeWeight * (numVertices > 0 ? numVertices - 1 : 0); + List> buckets = new ArrayList<>(maxPathWeight + 1); + for (int i = 0; i <= maxPathWeight; i++) { + buckets.add(new HashSet<>()); + } + + // Add the source vertex to the first bucket + buckets.get(0).add(source); + + // Process buckets in increasing order of distance + for (int d = 0; d <= maxPathWeight; d++) { + // Process all vertices in the current bucket + while (!buckets.get(d).isEmpty()) { + // Get and remove a vertex from the current bucket + int u = buckets.get(d).iterator().next(); + buckets.get(d).remove(u); + + // If we've found a shorter path already, skip + if (d > distances[u]) { + continue; + } + + // Relax all adjacent edges + for (Edge edge : graph.get(u)) { + int v = edge.getDestination(); + int weight = edge.getWeight(); + + // If a shorter path to v is found + if (distances[u] != Integer.MAX_VALUE && distances[u] + weight < distances[v]) { + // If v was already in a bucket, remove it from the old one + if (distances[v] != Integer.MAX_VALUE) { + buckets.get(distances[v]).remove(v); + } + // Update distance and move v to the new bucket + distances[v] = distances[u] + weight; + buckets.get(distances[v]).add(v); + } + } + } + } + return distances; + } +} diff --git a/src/test/java/com/thealgorithms/datastructures/graphs/DialsAlgorithmTest.java b/src/test/java/com/thealgorithms/datastructures/graphs/DialsAlgorithmTest.java new file mode 100644 index 000000000000..2350455d4329 --- /dev/null +++ b/src/test/java/com/thealgorithms/datastructures/graphs/DialsAlgorithmTest.java @@ -0,0 +1,88 @@ +package com.thealgorithms.datastructures.graphs; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +final class DialsAlgorithmTest { + + private List> graph; + private static final int NUM_VERTICES = 6; + private static final int MAX_EDGE_WEIGHT = 10; + + @BeforeEach + void setUp() { + graph = new ArrayList<>(); + for (int i = 0; i < NUM_VERTICES; i++) { + graph.add(new ArrayList<>()); + } + } + + private void addEdge(int u, int v, int weight) { + graph.get(u).add(new DialsAlgorithm.Edge(v, weight)); + } + + @Test + @DisplayName("Test with a simple connected graph") + void testSimpleGraph() { + // Build graph from a standard example + addEdge(0, 1, 2); + addEdge(0, 2, 4); + addEdge(1, 2, 1); + addEdge(1, 3, 7); + addEdge(2, 4, 3); + addEdge(3, 5, 1); + addEdge(4, 3, 2); + addEdge(4, 5, 5); + + int[] expectedDistances = {0, 2, 3, 8, 6, 9}; + int[] actualDistances = DialsAlgorithm.run(graph, 0, MAX_EDGE_WEIGHT); + assertArrayEquals(expectedDistances, actualDistances); + } + + @Test + @DisplayName("Test with a disconnected node") + void testDisconnectedNode() { + addEdge(0, 1, 5); + addEdge(1, 2, 5); + // Node 3, 4, 5 are disconnected + + int[] expectedDistances = {0, 5, 10, Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE}; + int[] actualDistances = DialsAlgorithm.run(graph, 0, MAX_EDGE_WEIGHT); + assertArrayEquals(expectedDistances, actualDistances); + } + + @Test + @DisplayName("Test with source as destination") + void testSourceIsDestination() { + addEdge(0, 1, 10); + int[] expectedDistances = {0, 10, Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE}; + // Run with source 0 + int[] actualDistances = DialsAlgorithm.run(graph, 0, MAX_EDGE_WEIGHT); + assertArrayEquals(expectedDistances, actualDistances); + } + + @Test + @DisplayName("Test graph with multiple paths to a node") + void testMultiplePaths() { + addEdge(0, 1, 10); + addEdge(0, 2, 3); + addEdge(2, 1, 2); // Shorter path to 1 is via 2 (3+2=5) + + int[] expectedDistances = {0, 5, 3, Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE}; + int[] actualDistances = DialsAlgorithm.run(graph, 0, MAX_EDGE_WEIGHT); + assertArrayEquals(expectedDistances, actualDistances); + } + + @Test + @DisplayName("Test with an invalid source vertex") + void testInvalidSource() { + assertThrows(IllegalArgumentException.class, () -> DialsAlgorithm.run(graph, -1, MAX_EDGE_WEIGHT)); + assertThrows(IllegalArgumentException.class, () -> DialsAlgorithm.run(graph, NUM_VERTICES, MAX_EDGE_WEIGHT)); + } +}