diff --git a/src/main/java/com/thealgorithms/graph/BronKerbosch.java b/src/main/java/com/thealgorithms/graph/BronKerbosch.java new file mode 100644 index 000000000000..0510d9bcf494 --- /dev/null +++ b/src/main/java/com/thealgorithms/graph/BronKerbosch.java @@ -0,0 +1,114 @@ +package com.thealgorithms.graph; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Implementation of the Bron–Kerbosch algorithm with pivoting for enumerating all maximal cliques + * in an undirected graph. + * + *

The input graph is represented as an adjacency list where {@code adjacency.get(u)} returns the + * set of vertices adjacent to {@code u}. The algorithm runs in time proportional to the number of + * maximal cliques produced and is widely used for clique enumeration problems.

+ * + * @author Wikipedia: Bron–Kerbosch algorithm + */ +public final class BronKerbosch { + + private BronKerbosch() { + } + + /** + * Finds all maximal cliques of the provided graph. + * + * @param adjacency adjacency list where {@code adjacency.size()} equals the number of vertices + * @return a list containing every maximal clique, each represented as a {@link Set} of vertices + * @throws IllegalArgumentException if the adjacency list is {@code null}, contains {@code null} + * entries, or references invalid vertices + */ + public static List> findMaximalCliques(List> adjacency) { + if (adjacency == null) { + throw new IllegalArgumentException("Adjacency list must not be null"); + } + + int n = adjacency.size(); + List> graph = new ArrayList<>(n); + for (int u = 0; u < n; u++) { + Set neighbors = adjacency.get(u); + if (neighbors == null) { + throw new IllegalArgumentException("Adjacency list must not contain null sets"); + } + Set copy = new HashSet<>(); + for (int v : neighbors) { + if (v < 0 || v >= n) { + throw new IllegalArgumentException("Neighbor index out of bounds: " + v); + } + if (v != u) { + copy.add(v); + } + } + graph.add(copy); + } + + Set r = new HashSet<>(); + Set p = new HashSet<>(); + Set x = new HashSet<>(); + for (int v = 0; v < n; v++) { + p.add(v); + } + + List> cliques = new ArrayList<>(); + bronKerboschPivot(r, p, x, graph, cliques); + return cliques; + } + + private static void bronKerboschPivot(Set r, Set p, Set x, List> graph, List> cliques) { + if (p.isEmpty() && x.isEmpty()) { + cliques.add(new HashSet<>(r)); + return; + } + + int pivot = choosePivot(p, x, graph); + Set candidates = new HashSet<>(p); + if (pivot != -1) { + candidates.removeAll(graph.get(pivot)); + } + + for (Integer v : candidates) { + r.add(v); + Set newP = intersection(p, graph.get(v)); + Set newX = intersection(x, graph.get(v)); + bronKerboschPivot(r, newP, newX, graph, cliques); + r.remove(v); + p.remove(v); + x.add(v); + } + } + + private static int choosePivot(Set p, Set x, List> graph) { + int pivot = -1; + int maxDegree = -1; + Set union = new HashSet<>(p); + union.addAll(x); + for (Integer v : union) { + int degree = graph.get(v).size(); + if (degree > maxDegree) { + maxDegree = degree; + pivot = v; + } + } + return pivot; + } + + private static Set intersection(Set base, Set neighbors) { + Set result = new HashSet<>(); + for (Integer v : base) { + if (neighbors.contains(v)) { + result.add(v); + } + } + return result; + } +} diff --git a/src/main/java/com/thealgorithms/graph/EdmondsKarp.java b/src/main/java/com/thealgorithms/graph/EdmondsKarp.java new file mode 100644 index 000000000000..59e7b09cb49c --- /dev/null +++ b/src/main/java/com/thealgorithms/graph/EdmondsKarp.java @@ -0,0 +1,107 @@ +package com.thealgorithms.graph; + +import java.util.ArrayDeque; +import java.util.Arrays; +import java.util.Queue; + +/** + * Implementation of the Edmonds–Karp algorithm for computing the maximum flow of a directed graph. + *

+ * The algorithm runs in O(V * E^2) time and is a specific implementation of the Ford–Fulkerson + * method where the augmenting paths are found using breadth-first search (BFS) to ensure the + * shortest augmenting paths (in terms of the number of edges) are used. + *

+ * + *

The graph is represented with a capacity matrix where {@code capacity[u][v]} denotes the + * capacity of the edge from {@code u} to {@code v}. Negative capacities are not allowed.

+ * + * @author Wikipedia: EdmondsKarp algorithm + */ +public final class EdmondsKarp { + + private EdmondsKarp() { + } + + /** + * Computes the maximum flow from {@code source} to {@code sink} in the provided capacity matrix. + * + * @param capacity the capacity matrix representing the directed graph; must be square and non-null + * @param source the source vertex index + * @param sink the sink vertex index + * @return the value of the maximum flow between {@code source} and {@code sink} + * @throws IllegalArgumentException if the matrix is {@code null}, not square, contains negative + * capacities, or if {@code source} / {@code sink} indices are invalid + */ + public static int maxFlow(int[][] capacity, int source, int sink) { + if (capacity == null || capacity.length == 0) { + throw new IllegalArgumentException("Capacity matrix must not be null or empty"); + } + + final int n = capacity.length; + for (int row = 0; row < n; row++) { + if (capacity[row] == null || capacity[row].length != n) { + throw new IllegalArgumentException("Capacity matrix must be square"); + } + for (int col = 0; col < n; col++) { + if (capacity[row][col] < 0) { + throw new IllegalArgumentException("Capacities must be non-negative"); + } + } + } + + if (source < 0 || source >= n || sink < 0 || sink >= n) { + throw new IllegalArgumentException("Source and sink must be valid vertex indices"); + } + if (source == sink) { + return 0; + } + + final int[][] residual = new int[n][n]; + for (int i = 0; i < n; i++) { + residual[i] = Arrays.copyOf(capacity[i], n); + } + + final int[] parent = new int[n]; + int maxFlow = 0; + + while (bfs(residual, source, sink, parent)) { + int pathFlow = Integer.MAX_VALUE; + for (int v = sink; v != source; v = parent[v]) { + int u = parent[v]; + pathFlow = Math.min(pathFlow, residual[u][v]); + } + + for (int v = sink; v != source; v = parent[v]) { + int u = parent[v]; + residual[u][v] -= pathFlow; + residual[v][u] += pathFlow; + } + + maxFlow += pathFlow; + } + + return maxFlow; + } + + private static boolean bfs(int[][] residual, int source, int sink, int[] parent) { + Arrays.fill(parent, -1); + parent[source] = source; + + Queue queue = new ArrayDeque<>(); + queue.add(source); + + while (!queue.isEmpty()) { + int u = queue.poll(); + for (int v = 0; v < residual.length; v++) { + if (residual[u][v] > 0 && parent[v] == -1) { + parent[v] = u; + if (v == sink) { + return true; + } + queue.add(v); + } + } + } + return false; + } +} diff --git a/src/test/java/com/thealgorithms/graph/BronKerboschTest.java b/src/test/java/com/thealgorithms/graph/BronKerboschTest.java new file mode 100644 index 000000000000..54c91c6ac1fd --- /dev/null +++ b/src/test/java/com/thealgorithms/graph/BronKerboschTest.java @@ -0,0 +1,79 @@ +package com.thealgorithms.graph; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class BronKerboschTest { + + @Test + @DisplayName("Complete graph returns single clique") + void completeGraph() { + List> adjacency = buildGraph(4); + addUndirectedEdge(adjacency, 0, 1); + addUndirectedEdge(adjacency, 0, 2); + addUndirectedEdge(adjacency, 0, 3); + addUndirectedEdge(adjacency, 1, 2); + addUndirectedEdge(adjacency, 1, 3); + addUndirectedEdge(adjacency, 2, 3); + + List> cliques = BronKerbosch.findMaximalCliques(adjacency); + assertEquals(1, cliques.size()); + assertEquals(Set.of(0, 1, 2, 3), cliques.get(0)); + } + + @Test + @DisplayName("Path graph produces individual edges") + void pathGraph() { + List> adjacency = buildGraph(3); + addUndirectedEdge(adjacency, 0, 1); + addUndirectedEdge(adjacency, 1, 2); + + List> cliques = BronKerbosch.findMaximalCliques(adjacency); + Set> result = new HashSet<>(cliques); + Set> expected = Set.of(Set.of(0, 1), Set.of(1, 2)); + assertEquals(expected, result); + } + + @Test + @DisplayName("Disconnected graph finds cliques per component") + void disconnectedGraph() { + List> adjacency = buildGraph(5); + addUndirectedEdge(adjacency, 0, 1); + addUndirectedEdge(adjacency, 0, 2); + addUndirectedEdge(adjacency, 1, 2); + addUndirectedEdge(adjacency, 3, 4); + + List> cliques = BronKerbosch.findMaximalCliques(adjacency); + Set> result = new HashSet<>(cliques); + Set> expected = Set.of(Set.of(0, 1, 2), Set.of(3, 4)); + assertEquals(expected, result); + } + + @Test + @DisplayName("Null neighbor set triggers exception") + void nullNeighborSet() { + List> adjacency = new ArrayList<>(); + adjacency.add(null); + assertThrows(IllegalArgumentException.class, () -> BronKerbosch.findMaximalCliques(adjacency)); + } + + private static List> buildGraph(int n) { + List> graph = new ArrayList<>(n); + for (int i = 0; i < n; i++) { + graph.add(new HashSet<>()); + } + return graph; + } + + private static void addUndirectedEdge(List> graph, int u, int v) { + graph.get(u).add(v); + graph.get(v).add(u); + } +} diff --git a/src/test/java/com/thealgorithms/graph/EdmondsKarpTest.java b/src/test/java/com/thealgorithms/graph/EdmondsKarpTest.java new file mode 100644 index 000000000000..55aeda381031 --- /dev/null +++ b/src/test/java/com/thealgorithms/graph/EdmondsKarpTest.java @@ -0,0 +1,48 @@ +package com.thealgorithms.graph; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class EdmondsKarpTest { + + @Test + @DisplayName("Classic CLRS network yields max flow 23") + void clrsExample() { + int[][] capacity = {{0, 16, 13, 0, 0, 0}, {0, 0, 10, 12, 0, 0}, {0, 4, 0, 0, 14, 0}, {0, 0, 9, 0, 0, 20}, {0, 0, 0, 7, 0, 4}, {0, 0, 0, 0, 0, 0}}; + int maxFlow = EdmondsKarp.maxFlow(capacity, 0, 5); + assertEquals(23, maxFlow); + } + + @Test + @DisplayName("Disconnected network has zero flow") + void disconnectedGraph() { + int[][] capacity = {{0, 0, 0}, {0, 0, 0}, {0, 0, 0}}; + int maxFlow = EdmondsKarp.maxFlow(capacity, 0, 2); + assertEquals(0, maxFlow); + } + + @Test + @DisplayName("Source equals sink returns zero") + void sourceEqualsSink() { + int[][] capacity = {{0, 5}, {0, 0}}; + int maxFlow = EdmondsKarp.maxFlow(capacity, 0, 0); + assertEquals(0, maxFlow); + } + + @Test + @DisplayName("Invalid matrix throws exception") + void invalidMatrix() { + int[][] capacity = {{0, 1}, {1}}; + assertThrows(IllegalArgumentException.class, () -> EdmondsKarp.maxFlow(capacity, 0, 1)); + } + + @Test + @DisplayName("Negative capacity is rejected") + void negativeCapacity() { + int[][] capacity = {{0, -1}, {0, 0}}; + assertThrows(IllegalArgumentException.class, () -> EdmondsKarp.maxFlow(capacity, 0, 1)); + } +}