Skip to content

Commit 8726d40

Browse files
authored
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()
1 parent 8b8434c commit 8726d40

File tree

4 files changed

+368
-1
lines changed

4 files changed

+368
-1
lines changed

DIRECTORY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,7 @@
354354
- 📄 [StronglyConnectedComponentOptimized](src/main/java/com/thealgorithms/graph/StronglyConnectedComponentOptimized.java)
355355
- 📄 [TravelingSalesman](src/main/java/com/thealgorithms/graph/TravelingSalesman.java)
356356
- 📄 [Dinic](src/main/java/com/thealgorithms/graph/Dinic.java)
357+
- 📄 [YensKShortestPaths](src/main/java/com/thealgorithms/graph/YensKShortestPaths.java)
357358
- 📁 **greedyalgorithms**
358359
- 📄 [ActivitySelection](src/main/java/com/thealgorithms/greedyalgorithms/ActivitySelection.java)
359360
- 📄 [BandwidthAllocation](src/main/java/com/thealgorithms/greedyalgorithms/BandwidthAllocation.java)

src/main/java/com/thealgorithms/datastructures/bloomfilter/BloomFilter.java

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.thealgorithms.datastructures.bloomfilter;
22

3+
import java.util.Arrays;
34
import java.util.BitSet;
45

56
/**
@@ -115,7 +116,7 @@ private static class Hash<T> {
115116
* @return the computed hash value
116117
*/
117118
public int compute(T key) {
118-
return index * asciiString(String.valueOf(key));
119+
return index * contentHash(key);
119120
}
120121

121122
/**
@@ -135,5 +136,31 @@ private int asciiString(String word) {
135136
}
136137
return sum;
137138
}
139+
140+
/**
141+
* Computes a content-based hash for arrays; falls back to ASCII-sum of String value otherwise.
142+
*/
143+
private int contentHash(Object key) {
144+
if (key instanceof int[]) {
145+
return Arrays.hashCode((int[]) key);
146+
} else if (key instanceof long[]) {
147+
return Arrays.hashCode((long[]) key);
148+
} else if (key instanceof byte[]) {
149+
return Arrays.hashCode((byte[]) key);
150+
} else if (key instanceof short[]) {
151+
return Arrays.hashCode((short[]) key);
152+
} else if (key instanceof char[]) {
153+
return Arrays.hashCode((char[]) key);
154+
} else if (key instanceof boolean[]) {
155+
return Arrays.hashCode((boolean[]) key);
156+
} else if (key instanceof float[]) {
157+
return Arrays.hashCode((float[]) key);
158+
} else if (key instanceof double[]) {
159+
return Arrays.hashCode((double[]) key);
160+
} else if (key instanceof Object[]) {
161+
return Arrays.deepHashCode((Object[]) key);
162+
}
163+
return asciiString(String.valueOf(key));
164+
}
138165
}
139166
}
Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
package com.thealgorithms.graph;
2+
3+
import java.util.ArrayList;
4+
import java.util.Arrays;
5+
import java.util.HashSet;
6+
import java.util.List;
7+
import java.util.Objects;
8+
import java.util.PriorityQueue;
9+
import java.util.Set;
10+
11+
/**
12+
* Yen's algorithm for finding K loopless shortest paths in a directed graph with non-negative edge weights.
13+
*
14+
* <p>Input is an adjacency matrix of edge weights. A value of -1 indicates no edge.
15+
* All existing edge weights must be non-negative. Zero-weight edges are allowed.</p>
16+
*
17+
* <p>References:
18+
* - Wikipedia: Yen's algorithm (https://en.wikipedia.org/wiki/Yen%27s_algorithm)
19+
* - Dijkstra's algorithm for the base shortest path computation.</p>
20+
*/
21+
public final class YensKShortestPaths {
22+
23+
private YensKShortestPaths() {
24+
}
25+
26+
private static final int NO_EDGE = -1;
27+
private static final long INF_COST = Long.MAX_VALUE / 4;
28+
29+
/**
30+
* Compute up to k loopless shortest paths from src to dst using Yen's algorithm.
31+
*
32+
* @param weights adjacency matrix; weights[u][v] = -1 means no edge; otherwise non-negative edge weight
33+
* @param src source vertex index
34+
* @param dst destination vertex index
35+
* @param k maximum number of paths to return (k >= 1)
36+
* @return list of paths, each path is a list of vertex indices in order from src to dst
37+
* @throws IllegalArgumentException on invalid inputs (null, non-square, negatives on existing edges, bad indices, k < 1)
38+
*/
39+
public static List<List<Integer>> kShortestPaths(int[][] weights, int src, int dst, int k) {
40+
validate(weights, src, dst, k);
41+
final int n = weights.length;
42+
// Make a defensive copy to avoid mutating caller's matrix
43+
int[][] weightsCopy = new int[n][n];
44+
for (int i = 0; i < n; i++) {
45+
weightsCopy[i] = Arrays.copyOf(weights[i], n);
46+
}
47+
48+
List<Path> shortestPaths = new ArrayList<>();
49+
PriorityQueue<Path> candidates = new PriorityQueue<>(); // min-heap by cost then lexicographic nodes
50+
Set<String> seen = new HashSet<>(); // deduplicate candidate paths by node sequence key
51+
52+
Path first = dijkstra(weightsCopy, src, dst, new boolean[n]);
53+
if (first == null) {
54+
return List.of();
55+
}
56+
shortestPaths.add(first);
57+
58+
for (int kIdx = 1; kIdx < k; kIdx++) {
59+
Path lastPath = shortestPaths.get(kIdx - 1);
60+
List<Integer> lastNodes = lastPath.nodes;
61+
for (int i = 0; i < lastNodes.size() - 1; i++) {
62+
int spurNode = lastNodes.get(i);
63+
List<Integer> rootPath = lastNodes.subList(0, i + 1);
64+
65+
// Build modified graph: remove edges that would recreate same root + next edge as any A path
66+
int[][] modifiedWeights = cloneMatrix(weightsCopy);
67+
68+
for (Path p : shortestPaths) {
69+
if (startsWith(p.nodes, rootPath) && p.nodes.size() > i + 1) {
70+
int u = p.nodes.get(i);
71+
int v = p.nodes.get(i + 1);
72+
modifiedWeights[u][v] = NO_EDGE; // remove edge
73+
}
74+
}
75+
// Prevent revisiting nodes in rootPath (loopless constraint), except spurNode itself
76+
boolean[] blocked = new boolean[n];
77+
for (int j = 0; j < rootPath.size() - 1; j++) {
78+
blocked[rootPath.get(j)] = true;
79+
}
80+
81+
Path spurPath = dijkstra(modifiedWeights, spurNode, dst, blocked);
82+
if (spurPath != null) {
83+
// concatenate rootPath (excluding spurNode at end) + spurPath
84+
List<Integer> totalNodes = new ArrayList<>(rootPath);
85+
// spurPath.nodes starts with spurNode; avoid duplication
86+
for (int idx = 1; idx < spurPath.nodes.size(); idx++) {
87+
totalNodes.add(spurPath.nodes.get(idx));
88+
}
89+
long rootCost = pathCost(weightsCopy, rootPath);
90+
long totalCost = rootCost + spurPath.cost; // spurPath.cost covers from spurNode to dst
91+
Path candidate = new Path(totalNodes, totalCost);
92+
String key = candidate.key();
93+
if (seen.add(key)) {
94+
candidates.add(candidate);
95+
}
96+
}
97+
}
98+
if (candidates.isEmpty()) {
99+
break;
100+
}
101+
shortestPaths.add(candidates.poll());
102+
}
103+
104+
// Map to list of node indices for output
105+
List<List<Integer>> result = new ArrayList<>(shortestPaths.size());
106+
for (Path p : shortestPaths) {
107+
result.add(new ArrayList<>(p.nodes));
108+
}
109+
return result;
110+
}
111+
112+
private static void validate(int[][] weights, int src, int dst, int k) {
113+
if (weights == null || weights.length == 0) {
114+
throw new IllegalArgumentException("Weights matrix must not be null or empty");
115+
}
116+
int n = weights.length;
117+
for (int i = 0; i < n; i++) {
118+
if (weights[i] == null || weights[i].length != n) {
119+
throw new IllegalArgumentException("Weights matrix must be square");
120+
}
121+
for (int j = 0; j < n; j++) {
122+
int val = weights[i][j];
123+
if (val < NO_EDGE) {
124+
throw new IllegalArgumentException("Weights must be -1 (no edge) or >= 0");
125+
}
126+
}
127+
}
128+
if (src < 0 || dst < 0 || src >= n || dst >= n) {
129+
throw new IllegalArgumentException("Invalid src/dst indices");
130+
}
131+
if (k < 1) {
132+
throw new IllegalArgumentException("k must be >= 1");
133+
}
134+
}
135+
136+
private static boolean startsWith(List<Integer> list, List<Integer> prefix) {
137+
if (prefix.size() > list.size()) {
138+
return false;
139+
}
140+
for (int i = 0; i < prefix.size(); i++) {
141+
if (!Objects.equals(list.get(i), prefix.get(i))) {
142+
return false;
143+
}
144+
}
145+
return true;
146+
}
147+
148+
private static int[][] cloneMatrix(int[][] a) {
149+
int n = a.length;
150+
int[][] b = new int[n][n];
151+
for (int i = 0; i < n; i++) {
152+
b[i] = Arrays.copyOf(a[i], n);
153+
}
154+
return b;
155+
}
156+
157+
private static long pathCost(int[][] weights, List<Integer> nodes) {
158+
long cost = 0;
159+
for (int i = 0; i + 1 < nodes.size(); i++) {
160+
int u = nodes.get(i);
161+
int v = nodes.get(i + 1);
162+
int edgeCost = weights[u][v];
163+
if (edgeCost < 0) {
164+
return INF_COST; // invalid
165+
}
166+
cost += edgeCost;
167+
}
168+
return cost;
169+
}
170+
171+
private static Path dijkstra(int[][] weights, int src, int dst, boolean[] blocked) {
172+
int n = weights.length;
173+
final long inf = INF_COST;
174+
long[] dist = new long[n];
175+
int[] parent = new int[n];
176+
Arrays.fill(dist, inf);
177+
Arrays.fill(parent, -1);
178+
PriorityQueue<Node> queue = new PriorityQueue<>();
179+
if (blocked[src]) {
180+
return null;
181+
}
182+
dist[src] = 0;
183+
queue.add(new Node(src, 0));
184+
while (!queue.isEmpty()) {
185+
Node current = queue.poll();
186+
if (current.dist != dist[current.u]) {
187+
continue;
188+
}
189+
if (current.u == dst) {
190+
break;
191+
}
192+
for (int v = 0; v < n; v++) {
193+
int edgeWeight = weights[current.u][v];
194+
if (edgeWeight >= 0 && !blocked[v]) {
195+
long newDist = current.dist + edgeWeight;
196+
if (newDist < dist[v]) {
197+
dist[v] = newDist;
198+
parent[v] = current.u;
199+
queue.add(new Node(v, newDist));
200+
}
201+
}
202+
}
203+
}
204+
if (dist[dst] >= inf) {
205+
// If src==dst and not blocked, the path is trivial with cost 0
206+
if (src == dst) {
207+
List<Integer> nodes = new ArrayList<>();
208+
nodes.add(src);
209+
return new Path(nodes, 0);
210+
}
211+
return null;
212+
}
213+
// Reconstruct path
214+
List<Integer> nodes = new ArrayList<>();
215+
int cur = dst;
216+
while (cur != -1) {
217+
nodes.add(0, cur);
218+
cur = parent[cur];
219+
}
220+
return new Path(nodes, dist[dst]);
221+
}
222+
223+
private static final class Node implements Comparable<Node> {
224+
final int u;
225+
final long dist;
226+
Node(int u, long dist) {
227+
this.u = u;
228+
this.dist = dist;
229+
}
230+
public int compareTo(Node o) {
231+
return Long.compare(this.dist, o.dist);
232+
}
233+
}
234+
235+
private static final class Path implements Comparable<Path> {
236+
final List<Integer> nodes;
237+
final long cost;
238+
Path(List<Integer> nodes, long cost) {
239+
this.nodes = nodes;
240+
this.cost = cost;
241+
}
242+
String key() {
243+
return nodes.toString();
244+
}
245+
@Override
246+
public int compareTo(Path o) {
247+
int costCmp = Long.compare(this.cost, o.cost);
248+
if (costCmp != 0) {
249+
return costCmp;
250+
}
251+
// tie-break lexicographically on nodes
252+
int minLength = Math.min(this.nodes.size(), o.nodes.size());
253+
for (int i = 0; i < minLength; i++) {
254+
int aNode = this.nodes.get(i);
255+
int bNode = o.nodes.get(i);
256+
if (aNode != bNode) {
257+
return Integer.compare(aNode, bNode);
258+
}
259+
}
260+
return Integer.compare(this.nodes.size(), o.nodes.size());
261+
}
262+
}
263+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package com.thealgorithms.graph;
2+
3+
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
import static org.junit.jupiter.api.Assertions.assertThrows;
5+
6+
import java.util.List;
7+
import org.junit.jupiter.api.DisplayName;
8+
import org.junit.jupiter.api.Test;
9+
10+
class YensKShortestPathsTest {
11+
12+
@Test
13+
@DisplayName("Basic K-shortest paths on small directed graph")
14+
void basicKPaths() {
15+
// Graph (directed) with non-negative weights, -1 = no edge
16+
// 0 -> 1 (1), 0 -> 2 (2), 1 -> 3 (1), 2 -> 3 (1), 0 -> 3 (5), 1 -> 2 (1)
17+
int n = 4;
18+
int[][] w = new int[n][n];
19+
for (int i = 0; i < n; i++) {
20+
for (int j = 0; j < n; j++) {
21+
w[i][j] = -1;
22+
}
23+
}
24+
w[0][1] = 1;
25+
w[0][2] = 2;
26+
w[1][3] = 1;
27+
w[2][3] = 1;
28+
w[0][3] = 5;
29+
w[1][2] = 1;
30+
31+
List<List<Integer>> paths = YensKShortestPaths.kShortestPaths(w, 0, 3, 3);
32+
// Expected K=3 loopless shortest paths from 0 to 3, ordered by total cost:
33+
// 1) 0-1-3 (cost 2)
34+
// 2) 0-2-3 (cost 3)
35+
// 3) 0-1-2-3 (cost 3) -> tie with 0-2-3; tie-broken lexicographically by nodes
36+
assertEquals(3, paths.size());
37+
assertEquals(List.of(0, 1, 3), paths.get(0));
38+
assertEquals(List.of(0, 1, 2, 3), paths.get(1)); // lexicographically before [0,2,3]
39+
assertEquals(List.of(0, 2, 3), paths.get(2));
40+
}
41+
42+
@Test
43+
@DisplayName("K larger than available paths returns only existing ones")
44+
void kLargerThanAvailable() {
45+
int[][] w = {{-1, 1, -1}, {-1, -1, 1}, {-1, -1, -1}};
46+
// Only one simple path 0->1->2
47+
List<List<Integer>> paths = YensKShortestPaths.kShortestPaths(w, 0, 2, 5);
48+
assertEquals(1, paths.size());
49+
assertEquals(List.of(0, 1, 2), paths.get(0));
50+
}
51+
52+
@Test
53+
@DisplayName("No path returns empty list")
54+
void noPath() {
55+
int[][] w = {{-1, -1}, {-1, -1}};
56+
List<List<Integer>> paths = YensKShortestPaths.kShortestPaths(w, 0, 1, 3);
57+
assertEquals(0, paths.size());
58+
}
59+
60+
@Test
61+
@DisplayName("Source equals destination returns trivial path")
62+
void sourceEqualsDestination() {
63+
int[][] w = {{-1, 1}, {-1, -1}};
64+
List<List<Integer>> paths = YensKShortestPaths.kShortestPaths(w, 0, 0, 2);
65+
// First path is [0]
66+
assertEquals(1, paths.size());
67+
assertEquals(List.of(0), paths.get(0));
68+
}
69+
70+
@Test
71+
@DisplayName("Negative weight entries (other than -1) are rejected")
72+
void negativeWeightsRejected() {
73+
int[][] w = {{-1, -2}, {-1, -1}};
74+
assertThrows(IllegalArgumentException.class, () -> YensKShortestPaths.kShortestPaths(w, 0, 1, 2));
75+
}
76+
}

0 commit comments

Comments
 (0)