diff --git a/DIRECTORY.md b/DIRECTORY.md index 8fbcbaaec96e..3bfedac64b89 100644 --- a/DIRECTORY.md +++ b/DIRECTORY.md @@ -354,6 +354,7 @@ - 📄 [StronglyConnectedComponentOptimized](src/main/java/com/thealgorithms/graph/StronglyConnectedComponentOptimized.java) - 📄 [TravelingSalesman](src/main/java/com/thealgorithms/graph/TravelingSalesman.java) - 📄 [Dinic](src/main/java/com/thealgorithms/graph/Dinic.java) + - 📄 [YensKShortestPaths](src/main/java/com/thealgorithms/graph/YensKShortestPaths.java) - 📁 **greedyalgorithms** - 📄 [ActivitySelection](src/main/java/com/thealgorithms/greedyalgorithms/ActivitySelection.java) - 📄 [BandwidthAllocation](src/main/java/com/thealgorithms/greedyalgorithms/BandwidthAllocation.java) diff --git a/src/main/java/com/thealgorithms/datastructures/bloomfilter/BloomFilter.java b/src/main/java/com/thealgorithms/datastructures/bloomfilter/BloomFilter.java index d60b95110fc2..c84f25bd78e6 100644 --- a/src/main/java/com/thealgorithms/datastructures/bloomfilter/BloomFilter.java +++ b/src/main/java/com/thealgorithms/datastructures/bloomfilter/BloomFilter.java @@ -1,5 +1,6 @@ package com.thealgorithms.datastructures.bloomfilter; +import java.util.Arrays; import java.util.BitSet; /** @@ -115,7 +116,7 @@ private static class Hash { * @return the computed hash value */ public int compute(T key) { - return index * asciiString(String.valueOf(key)); + return index * contentHash(key); } /** @@ -135,5 +136,31 @@ private int asciiString(String word) { } return sum; } + + /** + * Computes a content-based hash for arrays; falls back to ASCII-sum of String value otherwise. + */ + private int contentHash(Object key) { + if (key instanceof int[]) { + return Arrays.hashCode((int[]) key); + } else if (key instanceof long[]) { + return Arrays.hashCode((long[]) key); + } else if (key instanceof byte[]) { + return Arrays.hashCode((byte[]) key); + } else if (key instanceof short[]) { + return Arrays.hashCode((short[]) key); + } else if (key instanceof char[]) { + return Arrays.hashCode((char[]) key); + } else if (key instanceof boolean[]) { + return Arrays.hashCode((boolean[]) key); + } else if (key instanceof float[]) { + return Arrays.hashCode((float[]) key); + } else if (key instanceof double[]) { + return Arrays.hashCode((double[]) key); + } else if (key instanceof Object[]) { + return Arrays.deepHashCode((Object[]) key); + } + return asciiString(String.valueOf(key)); + } } } diff --git a/src/main/java/com/thealgorithms/graph/YensKShortestPaths.java b/src/main/java/com/thealgorithms/graph/YensKShortestPaths.java new file mode 100644 index 000000000000..dfc8386de6ce --- /dev/null +++ b/src/main/java/com/thealgorithms/graph/YensKShortestPaths.java @@ -0,0 +1,263 @@ +package com.thealgorithms.graph; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.PriorityQueue; +import java.util.Set; + +/** + * Yen's algorithm for finding K loopless shortest paths in a directed graph with non-negative edge weights. + * + *

Input is an adjacency matrix of edge weights. A value of -1 indicates no edge. + * All existing edge weights must be non-negative. Zero-weight edges are allowed.

+ * + *

References: + * - Wikipedia: Yen's algorithm (https://en.wikipedia.org/wiki/Yen%27s_algorithm) + * - Dijkstra's algorithm for the base shortest path computation.

+ */ +public final class YensKShortestPaths { + + private YensKShortestPaths() { + } + + private static final int NO_EDGE = -1; + private static final long INF_COST = Long.MAX_VALUE / 4; + + /** + * Compute up to k loopless shortest paths from src to dst using Yen's algorithm. + * + * @param weights adjacency matrix; weights[u][v] = -1 means no edge; otherwise non-negative edge weight + * @param src source vertex index + * @param dst destination vertex index + * @param k maximum number of paths to return (k >= 1) + * @return list of paths, each path is a list of vertex indices in order from src to dst + * @throws IllegalArgumentException on invalid inputs (null, non-square, negatives on existing edges, bad indices, k < 1) + */ + public static List> kShortestPaths(int[][] weights, int src, int dst, int k) { + validate(weights, src, dst, k); + final int n = weights.length; + // Make a defensive copy to avoid mutating caller's matrix + int[][] weightsCopy = new int[n][n]; + for (int i = 0; i < n; i++) { + weightsCopy[i] = Arrays.copyOf(weights[i], n); + } + + List shortestPaths = new ArrayList<>(); + PriorityQueue candidates = new PriorityQueue<>(); // min-heap by cost then lexicographic nodes + Set seen = new HashSet<>(); // deduplicate candidate paths by node sequence key + + Path first = dijkstra(weightsCopy, src, dst, new boolean[n]); + if (first == null) { + return List.of(); + } + shortestPaths.add(first); + + for (int kIdx = 1; kIdx < k; kIdx++) { + Path lastPath = shortestPaths.get(kIdx - 1); + List lastNodes = lastPath.nodes; + for (int i = 0; i < lastNodes.size() - 1; i++) { + int spurNode = lastNodes.get(i); + List rootPath = lastNodes.subList(0, i + 1); + + // Build modified graph: remove edges that would recreate same root + next edge as any A path + int[][] modifiedWeights = cloneMatrix(weightsCopy); + + for (Path p : shortestPaths) { + if (startsWith(p.nodes, rootPath) && p.nodes.size() > i + 1) { + int u = p.nodes.get(i); + int v = p.nodes.get(i + 1); + modifiedWeights[u][v] = NO_EDGE; // remove edge + } + } + // Prevent revisiting nodes in rootPath (loopless constraint), except spurNode itself + boolean[] blocked = new boolean[n]; + for (int j = 0; j < rootPath.size() - 1; j++) { + blocked[rootPath.get(j)] = true; + } + + Path spurPath = dijkstra(modifiedWeights, spurNode, dst, blocked); + if (spurPath != null) { + // concatenate rootPath (excluding spurNode at end) + spurPath + List totalNodes = new ArrayList<>(rootPath); + // spurPath.nodes starts with spurNode; avoid duplication + for (int idx = 1; idx < spurPath.nodes.size(); idx++) { + totalNodes.add(spurPath.nodes.get(idx)); + } + long rootCost = pathCost(weightsCopy, rootPath); + long totalCost = rootCost + spurPath.cost; // spurPath.cost covers from spurNode to dst + Path candidate = new Path(totalNodes, totalCost); + String key = candidate.key(); + if (seen.add(key)) { + candidates.add(candidate); + } + } + } + if (candidates.isEmpty()) { + break; + } + shortestPaths.add(candidates.poll()); + } + + // Map to list of node indices for output + List> result = new ArrayList<>(shortestPaths.size()); + for (Path p : shortestPaths) { + result.add(new ArrayList<>(p.nodes)); + } + return result; + } + + private static void validate(int[][] weights, int src, int dst, int k) { + if (weights == null || weights.length == 0) { + throw new IllegalArgumentException("Weights matrix must not be null or empty"); + } + int n = weights.length; + for (int i = 0; i < n; i++) { + if (weights[i] == null || weights[i].length != n) { + throw new IllegalArgumentException("Weights matrix must be square"); + } + for (int j = 0; j < n; j++) { + int val = weights[i][j]; + if (val < NO_EDGE) { + throw new IllegalArgumentException("Weights must be -1 (no edge) or >= 0"); + } + } + } + if (src < 0 || dst < 0 || src >= n || dst >= n) { + throw new IllegalArgumentException("Invalid src/dst indices"); + } + if (k < 1) { + throw new IllegalArgumentException("k must be >= 1"); + } + } + + private static boolean startsWith(List list, List prefix) { + if (prefix.size() > list.size()) { + return false; + } + for (int i = 0; i < prefix.size(); i++) { + if (!Objects.equals(list.get(i), prefix.get(i))) { + return false; + } + } + return true; + } + + private static int[][] cloneMatrix(int[][] a) { + int n = a.length; + int[][] b = new int[n][n]; + for (int i = 0; i < n; i++) { + b[i] = Arrays.copyOf(a[i], n); + } + return b; + } + + private static long pathCost(int[][] weights, List nodes) { + long cost = 0; + for (int i = 0; i + 1 < nodes.size(); i++) { + int u = nodes.get(i); + int v = nodes.get(i + 1); + int edgeCost = weights[u][v]; + if (edgeCost < 0) { + return INF_COST; // invalid + } + cost += edgeCost; + } + return cost; + } + + private static Path dijkstra(int[][] weights, int src, int dst, boolean[] blocked) { + int n = weights.length; + final long inf = INF_COST; + long[] dist = new long[n]; + int[] parent = new int[n]; + Arrays.fill(dist, inf); + Arrays.fill(parent, -1); + PriorityQueue queue = new PriorityQueue<>(); + if (blocked[src]) { + return null; + } + dist[src] = 0; + queue.add(new Node(src, 0)); + while (!queue.isEmpty()) { + Node current = queue.poll(); + if (current.dist != dist[current.u]) { + continue; + } + if (current.u == dst) { + break; + } + for (int v = 0; v < n; v++) { + int edgeWeight = weights[current.u][v]; + if (edgeWeight >= 0 && !blocked[v]) { + long newDist = current.dist + edgeWeight; + if (newDist < dist[v]) { + dist[v] = newDist; + parent[v] = current.u; + queue.add(new Node(v, newDist)); + } + } + } + } + if (dist[dst] >= inf) { + // If src==dst and not blocked, the path is trivial with cost 0 + if (src == dst) { + List nodes = new ArrayList<>(); + nodes.add(src); + return new Path(nodes, 0); + } + return null; + } + // Reconstruct path + List nodes = new ArrayList<>(); + int cur = dst; + while (cur != -1) { + nodes.add(0, cur); + cur = parent[cur]; + } + return new Path(nodes, dist[dst]); + } + + private static final class Node implements Comparable { + final int u; + final long dist; + Node(int u, long dist) { + this.u = u; + this.dist = dist; + } + public int compareTo(Node o) { + return Long.compare(this.dist, o.dist); + } + } + + private static final class Path implements Comparable { + final List nodes; + final long cost; + Path(List nodes, long cost) { + this.nodes = nodes; + this.cost = cost; + } + String key() { + return nodes.toString(); + } + @Override + public int compareTo(Path o) { + int costCmp = Long.compare(this.cost, o.cost); + if (costCmp != 0) { + return costCmp; + } + // tie-break lexicographically on nodes + int minLength = Math.min(this.nodes.size(), o.nodes.size()); + for (int i = 0; i < minLength; i++) { + int aNode = this.nodes.get(i); + int bNode = o.nodes.get(i); + if (aNode != bNode) { + return Integer.compare(aNode, bNode); + } + } + return Integer.compare(this.nodes.size(), o.nodes.size()); + } + } +} diff --git a/src/test/java/com/thealgorithms/graph/YensKShortestPathsTest.java b/src/test/java/com/thealgorithms/graph/YensKShortestPathsTest.java new file mode 100644 index 000000000000..1de37a310939 --- /dev/null +++ b/src/test/java/com/thealgorithms/graph/YensKShortestPathsTest.java @@ -0,0 +1,76 @@ +package com.thealgorithms.graph; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class YensKShortestPathsTest { + + @Test + @DisplayName("Basic K-shortest paths on small directed graph") + void basicKPaths() { + // Graph (directed) with non-negative weights, -1 = no edge + // 0 -> 1 (1), 0 -> 2 (2), 1 -> 3 (1), 2 -> 3 (1), 0 -> 3 (5), 1 -> 2 (1) + int n = 4; + int[][] w = new int[n][n]; + for (int i = 0; i < n; i++) { + for (int j = 0; j < n; j++) { + w[i][j] = -1; + } + } + w[0][1] = 1; + w[0][2] = 2; + w[1][3] = 1; + w[2][3] = 1; + w[0][3] = 5; + w[1][2] = 1; + + List> paths = YensKShortestPaths.kShortestPaths(w, 0, 3, 3); + // Expected K=3 loopless shortest paths from 0 to 3, ordered by total cost: + // 1) 0-1-3 (cost 2) + // 2) 0-2-3 (cost 3) + // 3) 0-1-2-3 (cost 3) -> tie with 0-2-3; tie-broken lexicographically by nodes + assertEquals(3, paths.size()); + assertEquals(List.of(0, 1, 3), paths.get(0)); + assertEquals(List.of(0, 1, 2, 3), paths.get(1)); // lexicographically before [0,2,3] + assertEquals(List.of(0, 2, 3), paths.get(2)); + } + + @Test + @DisplayName("K larger than available paths returns only existing ones") + void kLargerThanAvailable() { + int[][] w = {{-1, 1, -1}, {-1, -1, 1}, {-1, -1, -1}}; + // Only one simple path 0->1->2 + List> paths = YensKShortestPaths.kShortestPaths(w, 0, 2, 5); + assertEquals(1, paths.size()); + assertEquals(List.of(0, 1, 2), paths.get(0)); + } + + @Test + @DisplayName("No path returns empty list") + void noPath() { + int[][] w = {{-1, -1}, {-1, -1}}; + List> paths = YensKShortestPaths.kShortestPaths(w, 0, 1, 3); + assertEquals(0, paths.size()); + } + + @Test + @DisplayName("Source equals destination returns trivial path") + void sourceEqualsDestination() { + int[][] w = {{-1, 1}, {-1, -1}}; + List> paths = YensKShortestPaths.kShortestPaths(w, 0, 0, 2); + // First path is [0] + assertEquals(1, paths.size()); + assertEquals(List.of(0), paths.get(0)); + } + + @Test + @DisplayName("Negative weight entries (other than -1) are rejected") + void negativeWeightsRejected() { + int[][] w = {{-1, -2}, {-1, -1}}; + assertThrows(IllegalArgumentException.class, () -> YensKShortestPaths.kShortestPaths(w, 0, 1, 2)); + } +}