From c59f18e7fa0a1b9bd4d365bdb74837be154d1373 Mon Sep 17 00:00:00 2001 From: Sameer Prajapati Date: Wed, 15 Oct 2025 19:36:22 +0530 Subject: [PATCH 1/3] =?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/3] 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/3] 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 } }