Skip to content

Commit 6ada459

Browse files
committed
Add Yen’s K-shortest loopless paths with tests and index update
1 parent 8b8434c commit 6ada459

File tree

3 files changed

+321
-0
lines changed

3 files changed

+321
-0
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)
Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
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+
/**
27+
* Compute up to k loopless shortest paths from src to dst using Yen's algorithm.
28+
*
29+
* @param weights adjacency matrix; weights[u][v] = -1 means no edge; otherwise non-negative edge weight
30+
* @param src source vertex index
31+
* @param dst destination vertex index
32+
* @param k maximum number of paths to return (k >= 1)
33+
* @return list of paths, each path is a list of vertex indices in order from src to dst
34+
* @throws IllegalArgumentException on invalid inputs (null, non-square, negatives on existing edges, bad indices, k < 1)
35+
*/
36+
public static List<List<Integer>> kShortestPaths(int[][] weights, int src, int dst, int k) {
37+
validate(weights, src, dst, k);
38+
final int n = weights.length;
39+
// Make a defensive copy to avoid mutating caller's matrix
40+
int[][] w = new int[n][n];
41+
for (int i = 0; i < n; i++) {
42+
w[i] = Arrays.copyOf(weights[i], n);
43+
}
44+
45+
List<Path> A = new ArrayList<>();
46+
PriorityQueue<Path> B = new PriorityQueue<>(); // min-heap by cost then lexicographic nodes
47+
Set<String> seen = new HashSet<>(); // deduplicate candidate paths by node sequence key
48+
49+
Path first = dijkstra(w, src, dst, new boolean[n]);
50+
if (first == null) {
51+
return List.of();
52+
}
53+
A.add(first);
54+
55+
for (int kIdx = 1; kIdx < k; kIdx++) {
56+
Path lastPath = A.get(kIdx - 1);
57+
List<Integer> lastNodes = lastPath.nodes;
58+
for (int i = 0; i < lastNodes.size() - 1; i++) {
59+
int spurNode = lastNodes.get(i);
60+
List<Integer> rootPath = lastNodes.subList(0, i + 1);
61+
62+
// Build modified graph: remove edges that would recreate same root + next edge as any A path
63+
int[][] wMod = cloneMatrix(w);
64+
65+
for (Path p : A) {
66+
if (startsWith(p.nodes, rootPath) && p.nodes.size() > i + 1) {
67+
int u = p.nodes.get(i);
68+
int v = p.nodes.get(i + 1);
69+
wMod[u][v] = -1; // remove edge
70+
}
71+
}
72+
// Prevent revisiting nodes in rootPath (loopless constraint), except spurNode itself
73+
boolean[] blocked = new boolean[n];
74+
for (int j = 0; j < rootPath.size() - 1; j++) {
75+
blocked[rootPath.get(j)] = true;
76+
}
77+
78+
Path spurPath = dijkstra(wMod, spurNode, dst, blocked);
79+
if (spurPath != null) {
80+
// concatenate rootPath (excluding spurNode at end) + spurPath
81+
List<Integer> totalNodes = new ArrayList<>(rootPath);
82+
// spurPath.nodes starts with spurNode; avoid duplication
83+
for (int idx = 1; idx < spurPath.nodes.size(); idx++) {
84+
totalNodes.add(spurPath.nodes.get(idx));
85+
}
86+
long rootCost = pathCost(w, rootPath);
87+
long totalCost = rootCost + spurPath.cost; // spurPath.cost covers from spurNode to dst
88+
Path candidate = new Path(totalNodes, totalCost);
89+
String key = candidate.key();
90+
if (!seen.contains(key)) {
91+
B.add(candidate);
92+
seen.add(key);
93+
}
94+
}
95+
}
96+
if (B.isEmpty()) {
97+
break;
98+
}
99+
A.add(B.poll());
100+
}
101+
102+
// Map to list of node indices for output
103+
List<List<Integer>> result = new ArrayList<>(A.size());
104+
for (Path p : A) {
105+
result.add(new ArrayList<>(p.nodes));
106+
}
107+
return result;
108+
}
109+
110+
private static void validate(int[][] weights, int src, int dst, int k) {
111+
if (weights == null || weights.length == 0) {
112+
throw new IllegalArgumentException("Weights matrix must not be null or empty");
113+
}
114+
int n = weights.length;
115+
for (int i = 0; i < n; i++) {
116+
if (weights[i] == null || weights[i].length != n) {
117+
throw new IllegalArgumentException("Weights matrix must be square");
118+
}
119+
for (int j = 0; j < n; j++) {
120+
int val = weights[i][j];
121+
if (val < -1) {
122+
throw new IllegalArgumentException("Weights must be -1 (no edge) or >= 0");
123+
}
124+
}
125+
}
126+
if (src < 0 || dst < 0 || src >= n || dst >= n) {
127+
throw new IllegalArgumentException("Invalid src/dst indices");
128+
}
129+
if (k < 1) {
130+
throw new IllegalArgumentException("k must be >= 1");
131+
}
132+
if (src == dst) {
133+
// allowed: path is [src] with cost 0 (handled by dijkstra)
134+
}
135+
}
136+
137+
private static boolean startsWith(List<Integer> list, List<Integer> prefix) {
138+
if (prefix.size() > list.size()) return false;
139+
for (int i = 0; i < prefix.size(); i++) {
140+
if (!Objects.equals(list.get(i), prefix.get(i))) return false;
141+
}
142+
return true;
143+
}
144+
145+
private static int[][] cloneMatrix(int[][] a) {
146+
int n = a.length;
147+
int[][] b = new int[n][n];
148+
for (int i = 0; i < n; i++) b[i] = Arrays.copyOf(a[i], n);
149+
return b;
150+
}
151+
152+
private static long pathCost(int[][] w, List<Integer> nodes) {
153+
long cost = 0;
154+
for (int i = 0; i + 1 < nodes.size(); i++) {
155+
int u = nodes.get(i), v = nodes.get(i + 1);
156+
int c = w[u][v];
157+
if (c < 0) return Long.MAX_VALUE / 4; // invalid
158+
cost += c;
159+
}
160+
return cost;
161+
}
162+
163+
private static Path dijkstra(int[][] w, int src, int dst, boolean[] blocked) {
164+
int n = w.length;
165+
final long INF = Long.MAX_VALUE / 4;
166+
long[] dist = new long[n];
167+
int[] parent = new int[n];
168+
Arrays.fill(dist, INF);
169+
Arrays.fill(parent, -1);
170+
PriorityQueue<Node> pq = new PriorityQueue<>();
171+
if (blocked[src]) return null;
172+
dist[src] = 0;
173+
pq.add(new Node(src, 0));
174+
while (!pq.isEmpty()) {
175+
Node cur = pq.poll();
176+
if (cur.dist != dist[cur.u]) continue;
177+
if (cur.u == dst) break;
178+
for (int v = 0; v < n; v++) {
179+
int wuv = w[cur.u][v];
180+
if (wuv >= 0 && !blocked[v]) {
181+
long nd = cur.dist + wuv;
182+
if (nd < dist[v]) {
183+
dist[v] = nd;
184+
parent[v] = cur.u;
185+
pq.add(new Node(v, nd));
186+
}
187+
}
188+
}
189+
}
190+
if (dist[dst] >= INF) {
191+
// If src==dst and not blocked, the path is trivial with cost 0
192+
if (src == dst) {
193+
List<Integer> nodes = new ArrayList<>();
194+
nodes.add(src);
195+
return new Path(nodes, 0);
196+
}
197+
return null;
198+
}
199+
// Reconstruct path
200+
List<Integer> nodes = new ArrayList<>();
201+
int cur = dst;
202+
while (cur != -1) {
203+
nodes.add(0, cur);
204+
cur = parent[cur];
205+
}
206+
return new Path(nodes, dist[dst]);
207+
}
208+
209+
private static final class Node implements Comparable<Node> {
210+
final int u;
211+
final long dist;
212+
Node(int u, long dist) {
213+
this.u = u;
214+
this.dist = dist;
215+
}
216+
public int compareTo(Node o) {
217+
return Long.compare(this.dist, o.dist);
218+
}
219+
}
220+
221+
private static final class Path implements Comparable<Path> {
222+
final List<Integer> nodes;
223+
final long cost;
224+
Path(List<Integer> nodes, long cost) {
225+
this.nodes = nodes;
226+
this.cost = cost;
227+
}
228+
String key() {
229+
return nodes.toString();
230+
}
231+
@Override
232+
public int compareTo(Path o) {
233+
int c = Long.compare(this.cost, o.cost);
234+
if (c != 0) return c;
235+
// tie-break lexicographically on nodes
236+
int m = Math.min(this.nodes.size(), o.nodes.size());
237+
for (int i = 0; i < m; i++) {
238+
int a = this.nodes.get(i), b = o.nodes.get(i);
239+
if (a != b) return Integer.compare(a, b);
240+
}
241+
return Integer.compare(this.nodes.size(), o.nodes.size());
242+
}
243+
}
244+
}
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)