mirror of
https://github.com/TheAlgorithms/Java.git
synced 2025-12-19 07:00:35 +08:00
Add Yen’s K-shortest loopless paths with tests [GRAPHS] (#6773)
* Add Yen’s K-shortest loopless paths with tests and index update * style: fix Checkstyle in Yens algorithm and tests * fix: resolve SpotBugs in Yens algorithm * fix (PMD): rename short variables in the code * (pmd): code fixes * fix(bloomfilter): hash arrays by content to satisfy array membership tests * style(pmd): fix EmptyControlStatement in validate() by returning early when src==dst * style(pmd): remove unnecessary return in validate()
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.thealgorithms.datastructures.bloomfilter;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.BitSet;
|
||||
|
||||
/**
|
||||
@@ -115,7 +116,7 @@ public class BloomFilter<T> {
|
||||
* @return the computed hash value
|
||||
*/
|
||||
public int compute(T key) {
|
||||
return index * asciiString(String.valueOf(key));
|
||||
return index * contentHash(key);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -135,5 +136,31 @@ public class BloomFilter<T> {
|
||||
}
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
263
src/main/java/com/thealgorithms/graph/YensKShortestPaths.java
Normal file
263
src/main/java/com/thealgorithms/graph/YensKShortestPaths.java
Normal file
@@ -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.
|
||||
*
|
||||
* <p>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.</p>
|
||||
*
|
||||
* <p>References:
|
||||
* - Wikipedia: Yen's algorithm (https://en.wikipedia.org/wiki/Yen%27s_algorithm)
|
||||
* - Dijkstra's algorithm for the base shortest path computation.</p>
|
||||
*/
|
||||
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<List<Integer>> 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<Path> shortestPaths = new ArrayList<>();
|
||||
PriorityQueue<Path> candidates = new PriorityQueue<>(); // min-heap by cost then lexicographic nodes
|
||||
Set<String> 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<Integer> lastNodes = lastPath.nodes;
|
||||
for (int i = 0; i < lastNodes.size() - 1; i++) {
|
||||
int spurNode = lastNodes.get(i);
|
||||
List<Integer> 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<Integer> 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<List<Integer>> 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<Integer> list, List<Integer> 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<Integer> 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<Node> 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<Integer> nodes = new ArrayList<>();
|
||||
nodes.add(src);
|
||||
return new Path(nodes, 0);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
// Reconstruct path
|
||||
List<Integer> 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<Node> {
|
||||
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<Path> {
|
||||
final List<Integer> nodes;
|
||||
final long cost;
|
||||
Path(List<Integer> 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<List<Integer>> 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<List<Integer>> 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<List<Integer>> 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<List<Integer>> 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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user