From c59f18e7fa0a1b9bd4d365bdb74837be154d1373 Mon Sep 17 00:00:00 2001 From: Sameer Prajapati Date: Wed, 15 Oct 2025 19:36:22 +0530 Subject: [PATCH 1/6] =?UTF-8?q?feat(graph):=20add=20Push=E2=80=93Relabel?= =?UTF-8?q?=20max=20flow=20with=20tests=20and=20index?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DIRECTORY.md | 2 + .../com/thealgorithms/graph/PushRelabel.java | 145 ++++++++++++++++++ .../thealgorithms/graph/PushRelabelTest.java | 66 ++++++++ 3 files changed, 213 insertions(+) create mode 100644 src/main/java/com/thealgorithms/graph/PushRelabel.java create mode 100644 src/test/java/com/thealgorithms/graph/PushRelabelTest.java diff --git a/DIRECTORY.md b/DIRECTORY.md index 8fbcbaaec96e..55a7c7f0a511 100644 --- a/DIRECTORY.md +++ b/DIRECTORY.md @@ -351,9 +351,11 @@ - 📄 [ConstrainedShortestPath](src/main/java/com/thealgorithms/graph/ConstrainedShortestPath.java) - 📄 [HopcroftKarp](src/main/java/com/thealgorithms/graph/HopcroftKarp.java) - 📄 [PredecessorConstrainedDfs](src/main/java/com/thealgorithms/graph/PredecessorConstrainedDfs.java) + - 📄 [PushRelabel](src/main/java/com/thealgorithms/graph/PushRelabel.java) - 📄 [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) diff --git a/src/main/java/com/thealgorithms/graph/PushRelabel.java b/src/main/java/com/thealgorithms/graph/PushRelabel.java new file mode 100644 index 000000000000..b3a2be2b1ce4 --- /dev/null +++ b/src/main/java/com/thealgorithms/graph/PushRelabel.java @@ -0,0 +1,145 @@ +package com.thealgorithms.graph; + +import java.util.ArrayDeque; +import java.util.Arrays; +import java.util.Queue; + +/** + * Push–Relabel (Relabel-to-Front variant simplified to array scanning) for maximum flow. + * + *

Input graph is a capacity matrix where {@code capacity[u][v]} is the capacity of the edge + * {@code u -> v}. Capacities must be non-negative. Vertices are indexed in {@code [0, n)}. + * + *

Time complexity: O(V^3) in the worst case for the array-based variant; typically fast in + * practice. This implementation uses a residual network over an adjacency-matrix representation. + * + *

The API mirrors {@link EdmondsKarp#maxFlow(int[][], int, int)} and {@link Dinic#maxFlow(int[][], int, int)}. + * + * @see Wikipedia: Push–Relabel maximum flow algorithm + */ +public final class PushRelabel { + + private PushRelabel() { + } + + /** + * Computes the maximum flow from {@code source} to {@code sink} using Push–Relabel. + * + * @param capacity square capacity matrix (n x n); entries must be >= 0 + * @param source source vertex index in [0, n) + * @param sink sink vertex index in [0, n) + * @return the maximum flow value + * @throws IllegalArgumentException if inputs are invalid + */ + public static int maxFlow(int[][] capacity, int source, int sink) { + validate(capacity, source, sink); + final int n = capacity.length; + if (source == sink) { + return 0; + } + + int[][] residual = new int[n][n]; + for (int i = 0; i < n; i++) { + residual[i] = Arrays.copyOf(capacity[i], n); + } + + int[] height = new int[n]; + int[] excess = new int[n]; + int[] nextNeighbor = new int[n]; + + // Preflow initialization + height[source] = n; + for (int v = 0; v < n; v++) { + int cap = residual[source][v]; + if (cap > 0) { + residual[source][v] -= cap; + residual[v][source] += cap; + excess[v] += cap; + excess[source] -= cap; + } + } + + // Active queue contains vertices (except source/sink) with positive excess + Queue active = new ArrayDeque<>(); + for (int v = 0; v < n; v++) { + if (v != source && v != sink && excess[v] > 0) { + active.add(v); + } + } + + while (!active.isEmpty()) { + int u = active.poll(); + discharge(u, residual, height, excess, nextNeighbor, source, sink, active); + if (excess[u] > 0) { + // still active after discharge; push to back + active.add(u); + } + } + + // Total flow equals excess at sink + return excess[sink]; + } + + private static boolean discharge(int u, int[][] residual, int[] height, int[] excess, int[] nextNeighbor, int source, int sink, Queue active) { + final int n = residual.length; + boolean pushedAny = false; + while (excess[u] > 0) { + if (nextNeighbor[u] >= n) { + relabel(u, residual, height); + nextNeighbor[u] = 0; + continue; + } + int v = nextNeighbor[u]; + if (residual[u][v] > 0 && height[u] == height[v] + 1) { + int delta = Math.min(excess[u], residual[u][v]); + residual[u][v] -= delta; + residual[v][u] += delta; + excess[u] -= delta; + int prevExcessV = excess[v]; + excess[v] += delta; + if (v != source && v != sink && prevExcessV == 0) { + active.add(v); + } + pushedAny = true; + } else { + nextNeighbor[u]++; + } + } + return pushedAny; + } + + private static void relabel(int u, int[][] residual, int[] height) { + final int n = residual.length; + int minHeight = Integer.MAX_VALUE; + for (int v = 0; v < n; v++) { + if (residual[u][v] > 0) { + minHeight = Math.min(minHeight, height[v]); + } + } + if (minHeight < Integer.MAX_VALUE) { + height[u] = minHeight + 1; + } else { + // No outgoing residual edges; keep height unchanged + } + } + + private static void validate(int[][] capacity, int source, int sink) { + if (capacity == null || capacity.length == 0) { + throw new IllegalArgumentException("Capacity matrix must not be null or empty"); + } + int n = capacity.length; + for (int i = 0; i < n; i++) { + if (capacity[i] == null || capacity[i].length != n) { + throw new IllegalArgumentException("Capacity matrix must be square"); + } + for (int j = 0; j < n; j++) { + if (capacity[i][j] < 0) { + throw new IllegalArgumentException("Capacities must be non-negative"); + } + } + } + if (source < 0 || sink < 0 || source >= n || sink >= n) { + throw new IllegalArgumentException("Source and sink must be valid vertex indices"); + } + } +} diff --git a/src/test/java/com/thealgorithms/graph/PushRelabelTest.java b/src/test/java/com/thealgorithms/graph/PushRelabelTest.java new file mode 100644 index 000000000000..b0021ec805b8 --- /dev/null +++ b/src/test/java/com/thealgorithms/graph/PushRelabelTest.java @@ -0,0 +1,66 @@ +package com.thealgorithms.graph; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class PushRelabelTest { + + @Test + @DisplayName("Classic CLRS network yields max flow 23 (PushRelabel)") + 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 = PushRelabel.maxFlow(capacity, 0, 5); + assertEquals(23, maxFlow); + } + + @Test + @DisplayName("Disconnected network has zero flow (PushRelabel)") + void disconnectedGraph() { + int[][] capacity = {{0, 0, 0}, {0, 0, 0}, {0, 0, 0}}; + int maxFlow = PushRelabel.maxFlow(capacity, 0, 2); + assertEquals(0, maxFlow); + } + + @Test + @DisplayName("Source equals sink returns zero (PushRelabel)") + void sourceEqualsSink() { + int[][] capacity = {{0, 5}, {0, 0}}; + int maxFlow = PushRelabel.maxFlow(capacity, 0, 0); + assertEquals(0, maxFlow); + } + + @Test + @DisplayName("PushRelabel matches Dinic and EdmondsKarp on random small graphs") + void parityWithOtherMaxFlow() { + java.util.Random rnd = new java.util.Random(42); + for (int n = 3; n <= 7; n++) { + for (int it = 0; it < 25; it++) { + int[][] cap = new int[n][n]; + for (int i = 0; i < n; i++) { + for (int j = 0; j < n; j++) { + if (i != j && rnd.nextDouble() < 0.35) { + cap[i][j] = rnd.nextInt(10); // capacities 0..9 + } + } + } + int s = 0; + int t = n - 1; + int fPushRelabel = PushRelabel.maxFlow(copyMatrix(cap), s, t); + int fDinic = Dinic.maxFlow(copyMatrix(cap), s, t); + int fEdmondsKarp = EdmondsKarp.maxFlow(cap, s, t); + assertEquals(fDinic, fPushRelabel); + assertEquals(fEdmondsKarp, fPushRelabel); + } + } + } + + private static int[][] copyMatrix(int[][] a) { + int[][] b = new int[a.length][a.length]; + for (int i = 0; i < a.length; i++) { + b[i] = java.util.Arrays.copyOf(a[i], a[i].length); + } + return b; + } +} From b2735617d6f9b831b0ea0789952a9a68c57bbf1d Mon Sep 17 00:00:00 2001 From: Sameer Prajapati Date: Wed, 15 Oct 2025 19:49:39 +0530 Subject: [PATCH 2/6] style(checkstyle): reduce discharge parameter count via State holder --- .../com/thealgorithms/graph/PushRelabel.java | 58 +++++++++++++------ 1 file changed, 40 insertions(+), 18 deletions(-) diff --git a/src/main/java/com/thealgorithms/graph/PushRelabel.java b/src/main/java/com/thealgorithms/graph/PushRelabel.java index b3a2be2b1ce4..fae53a39343f 100644 --- a/src/main/java/com/thealgorithms/graph/PushRelabel.java +++ b/src/main/java/com/thealgorithms/graph/PushRelabel.java @@ -67,9 +67,11 @@ public static int maxFlow(int[][] capacity, int source, int sink) { } } + State state = new State(residual, height, excess, nextNeighbor, source, sink, active); + while (!active.isEmpty()) { int u = active.poll(); - discharge(u, residual, height, excess, nextNeighbor, source, sink, active); + discharge(u, state); if (excess[u] > 0) { // still active after discharge; push to back active.add(u); @@ -80,34 +82,54 @@ public static int maxFlow(int[][] capacity, int source, int sink) { return excess[sink]; } - private static boolean discharge(int u, int[][] residual, int[] height, int[] excess, int[] nextNeighbor, int source, int sink, Queue active) { - final int n = residual.length; + private static boolean discharge(int u, State s) { + final int n = s.residual.length; boolean pushedAny = false; - while (excess[u] > 0) { - if (nextNeighbor[u] >= n) { - relabel(u, residual, height); - nextNeighbor[u] = 0; + while (s.excess[u] > 0) { + if (s.nextNeighbor[u] >= n) { + relabel(u, s.residual, s.height); + s.nextNeighbor[u] = 0; continue; } - int v = nextNeighbor[u]; - if (residual[u][v] > 0 && height[u] == height[v] + 1) { - int delta = Math.min(excess[u], residual[u][v]); - residual[u][v] -= delta; - residual[v][u] += delta; - excess[u] -= delta; - int prevExcessV = excess[v]; - excess[v] += delta; - if (v != source && v != sink && prevExcessV == 0) { - active.add(v); + int v = s.nextNeighbor[u]; + if (s.residual[u][v] > 0 && s.height[u] == s.height[v] + 1) { + int delta = Math.min(s.excess[u], s.residual[u][v]); + s.residual[u][v] -= delta; + s.residual[v][u] += delta; + s.excess[u] -= delta; + int prevExcessV = s.excess[v]; + s.excess[v] += delta; + if (v != s.source && v != s.sink && prevExcessV == 0) { + s.active.add(v); } pushedAny = true; } else { - nextNeighbor[u]++; + s.nextNeighbor[u]++; } } return pushedAny; } + private static final class State { + final int[][] residual; + final int[] height; + final int[] excess; + final int[] nextNeighbor; + final int source; + final int sink; + final Queue active; + + State(int[][] residual, int[] height, int[] excess, int[] nextNeighbor, int source, int sink, Queue active) { + this.residual = residual; + this.height = height; + this.excess = excess; + this.nextNeighbor = nextNeighbor; + this.source = source; + this.sink = sink; + this.active = active; + } + } + private static void relabel(int u, int[][] residual, int[] height) { final int n = residual.length; int minHeight = Integer.MAX_VALUE; From e408b8242f763f309a7dd3a639281ac478a07010 Mon Sep 17 00:00:00 2001 From: Sameer Prajapati Date: Wed, 15 Oct 2025 19:59:50 +0530 Subject: [PATCH 3/6] chore(pmd): make discharge void and remove empty else; satisfy PMD --- src/main/java/com/thealgorithms/graph/PushRelabel.java | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/main/java/com/thealgorithms/graph/PushRelabel.java b/src/main/java/com/thealgorithms/graph/PushRelabel.java index fae53a39343f..1bfb5ceacce0 100644 --- a/src/main/java/com/thealgorithms/graph/PushRelabel.java +++ b/src/main/java/com/thealgorithms/graph/PushRelabel.java @@ -82,9 +82,8 @@ public static int maxFlow(int[][] capacity, int source, int sink) { return excess[sink]; } - private static boolean discharge(int u, State s) { + private static void discharge(int u, State s) { final int n = s.residual.length; - boolean pushedAny = false; while (s.excess[u] > 0) { if (s.nextNeighbor[u] >= n) { relabel(u, s.residual, s.height); @@ -102,12 +101,10 @@ private static boolean discharge(int u, State s) { if (v != s.source && v != s.sink && prevExcessV == 0) { s.active.add(v); } - pushedAny = true; } else { s.nextNeighbor[u]++; } } - return pushedAny; } private static final class State { @@ -140,8 +137,6 @@ private static void relabel(int u, int[][] residual, int[] height) { } if (minHeight < Integer.MAX_VALUE) { height[u] = minHeight + 1; - } else { - // No outgoing residual edges; keep height unchanged } } From 1432019de8547532fd4ded60e2167120c94d8d82 Mon Sep 17 00:00:00 2001 From: Sameer Prajapati Date: Fri, 17 Oct 2025 00:09:59 +0530 Subject: [PATCH 4/6] feat(graph): add Hungarian Algorithm with tests and index --- DIRECTORY.md | 1 + .../graph/HungarianAlgorithm.java | 148 ++++++++++++++++++ .../graph/HungarianAlgorithmTest.java | 45 ++++++ 3 files changed, 194 insertions(+) create mode 100644 src/main/java/com/thealgorithms/graph/HungarianAlgorithm.java create mode 100644 src/test/java/com/thealgorithms/graph/HungarianAlgorithmTest.java diff --git a/DIRECTORY.md b/DIRECTORY.md index 55a7c7f0a511..7ee6074aace1 100644 --- a/DIRECTORY.md +++ b/DIRECTORY.md @@ -349,6 +349,7 @@ - 📄 [Point](src/main/java/com/thealgorithms/geometry/Point.java) - 📁 **graph** - 📄 [ConstrainedShortestPath](src/main/java/com/thealgorithms/graph/ConstrainedShortestPath.java) + - 📄 [HungarianAlgorithm](src/main/java/com/thealgorithms/graph/HungarianAlgorithm.java) - 📄 [HopcroftKarp](src/main/java/com/thealgorithms/graph/HopcroftKarp.java) - 📄 [PredecessorConstrainedDfs](src/main/java/com/thealgorithms/graph/PredecessorConstrainedDfs.java) - 📄 [PushRelabel](src/main/java/com/thealgorithms/graph/PushRelabel.java) diff --git a/src/main/java/com/thealgorithms/graph/HungarianAlgorithm.java b/src/main/java/com/thealgorithms/graph/HungarianAlgorithm.java new file mode 100644 index 000000000000..ec16a7ecee5d --- /dev/null +++ b/src/main/java/com/thealgorithms/graph/HungarianAlgorithm.java @@ -0,0 +1,148 @@ +package com.thealgorithms.graph; + +import java.util.Arrays; + +/** + * Hungarian algorithm (a.k.a. Kuhn–Munkres) for the Assignment Problem. + * + *

Given an n x m cost matrix (n tasks, m workers), finds a minimum-cost + * one-to-one assignment. If the matrix is rectangular, the algorithm pads to a + * square internally. Costs must be finite non-negative integers. + * + *

Time complexity: O(n^3) with n = max(rows, cols). + * + *

API returns the assignment as an array where {@code assignment[i]} is the + * column chosen for row i (or -1 if unassigned when rows != cols), and a total + * minimal cost. + * + * @see Wikipedia: Hungarian algorithm + */ +public final class HungarianAlgorithm { + + private HungarianAlgorithm() { + } + + /** Result holder for the Hungarian algorithm. */ + public static final class Result { + public final int[] assignment; // assignment[row] = col or -1 + public final int minCost; + + public Result(int[] assignment, int minCost) { + this.assignment = assignment; + this.minCost = minCost; + } + } + + /** + * Solves the assignment problem for a non-negative cost matrix. + * + * @param cost an r x c matrix of non-negative costs + * @return Result with row-to-column assignment and minimal total cost + * @throws IllegalArgumentException for null/empty or negative costs + */ + public static Result solve(int[][] cost) { + validate(cost); + int rows = cost.length; + int cols = cost[0].length; + int n = Math.max(rows, cols); + + // Build square matrix with padding 0 for missing cells + int[][] a = new int[n][n]; + for (int i = 0; i < n; i++) { + if (i < rows) { + for (int j = 0; j < n; j++) { + a[i][j] = (j < cols) ? cost[i][j] : 0; + } + } else { + Arrays.fill(a[i], 0); + } + } + + // Potentials and matching arrays + int[] u = new int[n + 1]; + int[] v = new int[n + 1]; + int[] p = new int[n + 1]; + int[] way = new int[n + 1]; + + for (int i = 1; i <= n; i++) { + p[0] = i; + int j0 = 0; + int[] minv = new int[n + 1]; + boolean[] used = new boolean[n + 1]; + Arrays.fill(minv, Integer.MAX_VALUE); + Arrays.fill(used, false); + do { + used[j0] = true; + int i0 = p[j0], delta = Integer.MAX_VALUE, j1 = 0; + for (int j = 1; j <= n; j++) { + if (!used[j]) { + int cur = a[i0 - 1][j - 1] - u[i0] - v[j]; + if (cur < minv[j]) { + minv[j] = cur; + way[j] = j0; + } + if (minv[j] < delta) { + delta = minv[j]; + j1 = j; + } + } + } + for (int j = 0; j <= n; j++) { + if (used[j]) { + u[p[j]] += delta; + v[j] -= delta; + } else { + minv[j] -= delta; + } + } + j0 = j1; + } while (p[j0] != 0); + do { + int j1 = way[j0]; + p[j0] = p[j1]; + j0 = j1; + } while (j0 != 0); + } + + int[] matchColForRow = new int[n]; + Arrays.fill(matchColForRow, -1); + for (int j = 1; j <= n; j++) { + if (p[j] != 0) { + matchColForRow[p[j] - 1] = j - 1; + } + } + + // Build assignment for original rows only, ignore padded rows + int[] assignment = new int[rows]; + Arrays.fill(assignment, -1); + int total = 0; + for (int i = 0; i < rows; i++) { + int j = matchColForRow[i]; + if (j >= 0 && j < cols) { + assignment[i] = j; + total += cost[i][j]; + } + } + return new Result(assignment, total); + } + + private static void validate(int[][] cost) { + if (cost == null || cost.length == 0) { + throw new IllegalArgumentException("Cost matrix must not be null or empty"); + } + int c = cost[0].length; + if (c == 0) { + throw new IllegalArgumentException("Cost matrix must have at least 1 column"); + } + for (int i = 0; i < cost.length; i++) { + if (cost[i] == null || cost[i].length != c) { + throw new IllegalArgumentException("Cost matrix must be rectangular with equal row lengths"); + } + for (int j = 0; j < c; j++) { + if (cost[i][j] < 0) { + throw new IllegalArgumentException("Costs must be non-negative"); + } + } + } + } +} diff --git a/src/test/java/com/thealgorithms/graph/HungarianAlgorithmTest.java b/src/test/java/com/thealgorithms/graph/HungarianAlgorithmTest.java new file mode 100644 index 000000000000..b6da233a3c67 --- /dev/null +++ b/src/test/java/com/thealgorithms/graph/HungarianAlgorithmTest.java @@ -0,0 +1,45 @@ +package com.thealgorithms.graph; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class HungarianAlgorithmTest { + + @Test + @DisplayName("Classic 3x3 example: minimal cost 5 with assignment [1,0,2]") + void classicSquareExample() { + int[][] cost = {{4, 1, 3}, {2, 0, 5}, {3, 2, 2}}; + HungarianAlgorithm.Result res = HungarianAlgorithm.solve(cost); + assertEquals(5, res.minCost); + assertArrayEquals(new int[] {1, 0, 2}, res.assignment); + } + + @Test + @DisplayName("Rectangular (more rows than cols): pads to square and returns -1 for unassigned rows") + void rectangularMoreRows() { + int[][] cost = {{7, 3}, {2, 8}, {5, 1}}; + // Optimal selects any 2 rows: choose row1->col0 (2) and row2->col1 (1) => total 3 + HungarianAlgorithm.Result res = HungarianAlgorithm.solve(cost); + assertEquals(3, res.minCost); + // Two rows assigned to 2 columns; one row remains -1. + int assigned = 0; + for (int a : res.assignment) { + if (a >= 0) { + assigned++; + } + } + assertEquals(2, assigned); + } + + @Test + @DisplayName("Zero diagonal yields zero total cost") + void zeroDiagonal() { + int[][] cost = {{0, 5, 9}, {4, 0, 7}, {3, 6, 0}}; + HungarianAlgorithm.Result res = HungarianAlgorithm.solve(cost); + assertEquals(0, res.minCost); + // Any permutation with diagonal chosen; verify costs sum to 0 already checked. + } +} From fde64bcd04d030fefe2d8e5bfa0b294364955ad3 Mon Sep 17 00:00:00 2001 From: Sameer Prajapati Date: Fri, 17 Oct 2025 00:24:19 +0530 Subject: [PATCH 5/6] Delete src/main/java/com/thealgorithms/graph/PushRelabel.java --- .../com/thealgorithms/graph/PushRelabel.java | 162 ------------------ 1 file changed, 162 deletions(-) delete mode 100644 src/main/java/com/thealgorithms/graph/PushRelabel.java diff --git a/src/main/java/com/thealgorithms/graph/PushRelabel.java b/src/main/java/com/thealgorithms/graph/PushRelabel.java deleted file mode 100644 index 1bfb5ceacce0..000000000000 --- a/src/main/java/com/thealgorithms/graph/PushRelabel.java +++ /dev/null @@ -1,162 +0,0 @@ -package com.thealgorithms.graph; - -import java.util.ArrayDeque; -import java.util.Arrays; -import java.util.Queue; - -/** - * Push–Relabel (Relabel-to-Front variant simplified to array scanning) for maximum flow. - * - *

Input graph is a capacity matrix where {@code capacity[u][v]} is the capacity of the edge - * {@code u -> v}. Capacities must be non-negative. Vertices are indexed in {@code [0, n)}. - * - *

Time complexity: O(V^3) in the worst case for the array-based variant; typically fast in - * practice. This implementation uses a residual network over an adjacency-matrix representation. - * - *

The API mirrors {@link EdmondsKarp#maxFlow(int[][], int, int)} and {@link Dinic#maxFlow(int[][], int, int)}. - * - * @see Wikipedia: Push–Relabel maximum flow algorithm - */ -public final class PushRelabel { - - private PushRelabel() { - } - - /** - * Computes the maximum flow from {@code source} to {@code sink} using Push–Relabel. - * - * @param capacity square capacity matrix (n x n); entries must be >= 0 - * @param source source vertex index in [0, n) - * @param sink sink vertex index in [0, n) - * @return the maximum flow value - * @throws IllegalArgumentException if inputs are invalid - */ - public static int maxFlow(int[][] capacity, int source, int sink) { - validate(capacity, source, sink); - final int n = capacity.length; - if (source == sink) { - return 0; - } - - int[][] residual = new int[n][n]; - for (int i = 0; i < n; i++) { - residual[i] = Arrays.copyOf(capacity[i], n); - } - - int[] height = new int[n]; - int[] excess = new int[n]; - int[] nextNeighbor = new int[n]; - - // Preflow initialization - height[source] = n; - for (int v = 0; v < n; v++) { - int cap = residual[source][v]; - if (cap > 0) { - residual[source][v] -= cap; - residual[v][source] += cap; - excess[v] += cap; - excess[source] -= cap; - } - } - - // Active queue contains vertices (except source/sink) with positive excess - Queue active = new ArrayDeque<>(); - for (int v = 0; v < n; v++) { - if (v != source && v != sink && excess[v] > 0) { - active.add(v); - } - } - - State state = new State(residual, height, excess, nextNeighbor, source, sink, active); - - while (!active.isEmpty()) { - int u = active.poll(); - discharge(u, state); - if (excess[u] > 0) { - // still active after discharge; push to back - active.add(u); - } - } - - // Total flow equals excess at sink - return excess[sink]; - } - - private static void discharge(int u, State s) { - final int n = s.residual.length; - while (s.excess[u] > 0) { - if (s.nextNeighbor[u] >= n) { - relabel(u, s.residual, s.height); - s.nextNeighbor[u] = 0; - continue; - } - int v = s.nextNeighbor[u]; - if (s.residual[u][v] > 0 && s.height[u] == s.height[v] + 1) { - int delta = Math.min(s.excess[u], s.residual[u][v]); - s.residual[u][v] -= delta; - s.residual[v][u] += delta; - s.excess[u] -= delta; - int prevExcessV = s.excess[v]; - s.excess[v] += delta; - if (v != s.source && v != s.sink && prevExcessV == 0) { - s.active.add(v); - } - } else { - s.nextNeighbor[u]++; - } - } - } - - private static final class State { - final int[][] residual; - final int[] height; - final int[] excess; - final int[] nextNeighbor; - final int source; - final int sink; - final Queue active; - - State(int[][] residual, int[] height, int[] excess, int[] nextNeighbor, int source, int sink, Queue active) { - this.residual = residual; - this.height = height; - this.excess = excess; - this.nextNeighbor = nextNeighbor; - this.source = source; - this.sink = sink; - this.active = active; - } - } - - private static void relabel(int u, int[][] residual, int[] height) { - final int n = residual.length; - int minHeight = Integer.MAX_VALUE; - for (int v = 0; v < n; v++) { - if (residual[u][v] > 0) { - minHeight = Math.min(minHeight, height[v]); - } - } - if (minHeight < Integer.MAX_VALUE) { - height[u] = minHeight + 1; - } - } - - private static void validate(int[][] capacity, int source, int sink) { - if (capacity == null || capacity.length == 0) { - throw new IllegalArgumentException("Capacity matrix must not be null or empty"); - } - int n = capacity.length; - for (int i = 0; i < n; i++) { - if (capacity[i] == null || capacity[i].length != n) { - throw new IllegalArgumentException("Capacity matrix must be square"); - } - for (int j = 0; j < n; j++) { - if (capacity[i][j] < 0) { - throw new IllegalArgumentException("Capacities must be non-negative"); - } - } - } - if (source < 0 || sink < 0 || source >= n || sink >= n) { - throw new IllegalArgumentException("Source and sink must be valid vertex indices"); - } - } -} From a9ed04f343826eb8df4c9fcda3edf7355b538833 Mon Sep 17 00:00:00 2001 From: Sameer Prajapati Date: Fri, 17 Oct 2025 00:24:32 +0530 Subject: [PATCH 6/6] Delete src/test/java/com/thealgorithms/graph/PushRelabelTest.java --- .../thealgorithms/graph/PushRelabelTest.java | 66 ------------------- 1 file changed, 66 deletions(-) delete mode 100644 src/test/java/com/thealgorithms/graph/PushRelabelTest.java diff --git a/src/test/java/com/thealgorithms/graph/PushRelabelTest.java b/src/test/java/com/thealgorithms/graph/PushRelabelTest.java deleted file mode 100644 index b0021ec805b8..000000000000 --- a/src/test/java/com/thealgorithms/graph/PushRelabelTest.java +++ /dev/null @@ -1,66 +0,0 @@ -package com.thealgorithms.graph; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - -class PushRelabelTest { - - @Test - @DisplayName("Classic CLRS network yields max flow 23 (PushRelabel)") - 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 = PushRelabel.maxFlow(capacity, 0, 5); - assertEquals(23, maxFlow); - } - - @Test - @DisplayName("Disconnected network has zero flow (PushRelabel)") - void disconnectedGraph() { - int[][] capacity = {{0, 0, 0}, {0, 0, 0}, {0, 0, 0}}; - int maxFlow = PushRelabel.maxFlow(capacity, 0, 2); - assertEquals(0, maxFlow); - } - - @Test - @DisplayName("Source equals sink returns zero (PushRelabel)") - void sourceEqualsSink() { - int[][] capacity = {{0, 5}, {0, 0}}; - int maxFlow = PushRelabel.maxFlow(capacity, 0, 0); - assertEquals(0, maxFlow); - } - - @Test - @DisplayName("PushRelabel matches Dinic and EdmondsKarp on random small graphs") - void parityWithOtherMaxFlow() { - java.util.Random rnd = new java.util.Random(42); - for (int n = 3; n <= 7; n++) { - for (int it = 0; it < 25; it++) { - int[][] cap = new int[n][n]; - for (int i = 0; i < n; i++) { - for (int j = 0; j < n; j++) { - if (i != j && rnd.nextDouble() < 0.35) { - cap[i][j] = rnd.nextInt(10); // capacities 0..9 - } - } - } - int s = 0; - int t = n - 1; - int fPushRelabel = PushRelabel.maxFlow(copyMatrix(cap), s, t); - int fDinic = Dinic.maxFlow(copyMatrix(cap), s, t); - int fEdmondsKarp = EdmondsKarp.maxFlow(cap, s, t); - assertEquals(fDinic, fPushRelabel); - assertEquals(fEdmondsKarp, fPushRelabel); - } - } - } - - private static int[][] copyMatrix(int[][] a) { - int[][] b = new int[a.length][a.length]; - for (int i = 0; i < a.length; i++) { - b[i] = java.util.Arrays.copyOf(a[i], a[i].length); - } - return b; - } -}