Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions DIRECTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.thealgorithms.datastructures.bloomfilter;

import java.util.Arrays;
import java.util.BitSet;

/**
Expand Down Expand Up @@ -115,7 +116,7 @@ private static class Hash<T> {
* @return the computed hash value
*/
public int compute(T key) {
return index * asciiString(String.valueOf(key));
return index * contentHash(key);
}

/**
Expand All @@ -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));
}
}
}
263 changes: 263 additions & 0 deletions src/main/java/com/thealgorithms/graph/YensKShortestPaths.java
Original file line number Diff line number Diff line change
@@ -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());
}
}
}
Original file line number Diff line number Diff line change
@@ -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));
}
}