Skip to content

Commit c59f18e

Browse files
committed
feat(graph): add Push–Relabel max flow with tests and index
1 parent 8b8434c commit c59f18e

File tree

3 files changed

+213
-0
lines changed

3 files changed

+213
-0
lines changed

DIRECTORY.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,9 +351,11 @@
351351
- 📄 [ConstrainedShortestPath](src/main/java/com/thealgorithms/graph/ConstrainedShortestPath.java)
352352
- 📄 [HopcroftKarp](src/main/java/com/thealgorithms/graph/HopcroftKarp.java)
353353
- 📄 [PredecessorConstrainedDfs](src/main/java/com/thealgorithms/graph/PredecessorConstrainedDfs.java)
354+
- 📄 [PushRelabel](src/main/java/com/thealgorithms/graph/PushRelabel.java)
354355
- 📄 [StronglyConnectedComponentOptimized](src/main/java/com/thealgorithms/graph/StronglyConnectedComponentOptimized.java)
355356
- 📄 [TravelingSalesman](src/main/java/com/thealgorithms/graph/TravelingSalesman.java)
356357
- 📄 [Dinic](src/main/java/com/thealgorithms/graph/Dinic.java)
358+
- 📄 [YensKShortestPaths](src/main/java/com/thealgorithms/graph/YensKShortestPaths.java)
357359
- 📁 **greedyalgorithms**
358360
- 📄 [ActivitySelection](src/main/java/com/thealgorithms/greedyalgorithms/ActivitySelection.java)
359361
- 📄 [BandwidthAllocation](src/main/java/com/thealgorithms/greedyalgorithms/BandwidthAllocation.java)
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
package com.thealgorithms.graph;
2+
3+
import java.util.ArrayDeque;
4+
import java.util.Arrays;
5+
import java.util.Queue;
6+
7+
/**
8+
* Push–Relabel (Relabel-to-Front variant simplified to array scanning) for maximum flow.
9+
*
10+
* <p>Input graph is a capacity matrix where {@code capacity[u][v]} is the capacity of the edge
11+
* {@code u -> v}. Capacities must be non-negative. Vertices are indexed in {@code [0, n)}.
12+
*
13+
* <p>Time complexity: O(V^3) in the worst case for the array-based variant; typically fast in
14+
* practice. This implementation uses a residual network over an adjacency-matrix representation.
15+
*
16+
* <p>The API mirrors {@link EdmondsKarp#maxFlow(int[][], int, int)} and {@link Dinic#maxFlow(int[][], int, int)}.
17+
*
18+
* @see <a href="https://en.wikipedia.org/wiki/Push%E2%80%93relabel_maximum_flow_algorithm">Wikipedia: Push–Relabel maximum flow algorithm</a>
19+
*/
20+
public final class PushRelabel {
21+
22+
private PushRelabel() {
23+
}
24+
25+
/**
26+
* Computes the maximum flow from {@code source} to {@code sink} using Push–Relabel.
27+
*
28+
* @param capacity square capacity matrix (n x n); entries must be >= 0
29+
* @param source source vertex index in [0, n)
30+
* @param sink sink vertex index in [0, n)
31+
* @return the maximum flow value
32+
* @throws IllegalArgumentException if inputs are invalid
33+
*/
34+
public static int maxFlow(int[][] capacity, int source, int sink) {
35+
validate(capacity, source, sink);
36+
final int n = capacity.length;
37+
if (source == sink) {
38+
return 0;
39+
}
40+
41+
int[][] residual = new int[n][n];
42+
for (int i = 0; i < n; i++) {
43+
residual[i] = Arrays.copyOf(capacity[i], n);
44+
}
45+
46+
int[] height = new int[n];
47+
int[] excess = new int[n];
48+
int[] nextNeighbor = new int[n];
49+
50+
// Preflow initialization
51+
height[source] = n;
52+
for (int v = 0; v < n; v++) {
53+
int cap = residual[source][v];
54+
if (cap > 0) {
55+
residual[source][v] -= cap;
56+
residual[v][source] += cap;
57+
excess[v] += cap;
58+
excess[source] -= cap;
59+
}
60+
}
61+
62+
// Active queue contains vertices (except source/sink) with positive excess
63+
Queue<Integer> active = new ArrayDeque<>();
64+
for (int v = 0; v < n; v++) {
65+
if (v != source && v != sink && excess[v] > 0) {
66+
active.add(v);
67+
}
68+
}
69+
70+
while (!active.isEmpty()) {
71+
int u = active.poll();
72+
discharge(u, residual, height, excess, nextNeighbor, source, sink, active);
73+
if (excess[u] > 0) {
74+
// still active after discharge; push to back
75+
active.add(u);
76+
}
77+
}
78+
79+
// Total flow equals excess at sink
80+
return excess[sink];
81+
}
82+
83+
private static boolean discharge(int u, int[][] residual, int[] height, int[] excess, int[] nextNeighbor, int source, int sink, Queue<Integer> active) {
84+
final int n = residual.length;
85+
boolean pushedAny = false;
86+
while (excess[u] > 0) {
87+
if (nextNeighbor[u] >= n) {
88+
relabel(u, residual, height);
89+
nextNeighbor[u] = 0;
90+
continue;
91+
}
92+
int v = nextNeighbor[u];
93+
if (residual[u][v] > 0 && height[u] == height[v] + 1) {
94+
int delta = Math.min(excess[u], residual[u][v]);
95+
residual[u][v] -= delta;
96+
residual[v][u] += delta;
97+
excess[u] -= delta;
98+
int prevExcessV = excess[v];
99+
excess[v] += delta;
100+
if (v != source && v != sink && prevExcessV == 0) {
101+
active.add(v);
102+
}
103+
pushedAny = true;
104+
} else {
105+
nextNeighbor[u]++;
106+
}
107+
}
108+
return pushedAny;
109+
}
110+
111+
private static void relabel(int u, int[][] residual, int[] height) {
112+
final int n = residual.length;
113+
int minHeight = Integer.MAX_VALUE;
114+
for (int v = 0; v < n; v++) {
115+
if (residual[u][v] > 0) {
116+
minHeight = Math.min(minHeight, height[v]);
117+
}
118+
}
119+
if (minHeight < Integer.MAX_VALUE) {
120+
height[u] = minHeight + 1;
121+
} else {
122+
// No outgoing residual edges; keep height unchanged
123+
}
124+
}
125+
126+
private static void validate(int[][] capacity, int source, int sink) {
127+
if (capacity == null || capacity.length == 0) {
128+
throw new IllegalArgumentException("Capacity matrix must not be null or empty");
129+
}
130+
int n = capacity.length;
131+
for (int i = 0; i < n; i++) {
132+
if (capacity[i] == null || capacity[i].length != n) {
133+
throw new IllegalArgumentException("Capacity matrix must be square");
134+
}
135+
for (int j = 0; j < n; j++) {
136+
if (capacity[i][j] < 0) {
137+
throw new IllegalArgumentException("Capacities must be non-negative");
138+
}
139+
}
140+
}
141+
if (source < 0 || sink < 0 || source >= n || sink >= n) {
142+
throw new IllegalArgumentException("Source and sink must be valid vertex indices");
143+
}
144+
}
145+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package com.thealgorithms.graph;
2+
3+
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
5+
import org.junit.jupiter.api.DisplayName;
6+
import org.junit.jupiter.api.Test;
7+
8+
class PushRelabelTest {
9+
10+
@Test
11+
@DisplayName("Classic CLRS network yields max flow 23 (PushRelabel)")
12+
void clrsExample() {
13+
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}};
14+
int maxFlow = PushRelabel.maxFlow(capacity, 0, 5);
15+
assertEquals(23, maxFlow);
16+
}
17+
18+
@Test
19+
@DisplayName("Disconnected network has zero flow (PushRelabel)")
20+
void disconnectedGraph() {
21+
int[][] capacity = {{0, 0, 0}, {0, 0, 0}, {0, 0, 0}};
22+
int maxFlow = PushRelabel.maxFlow(capacity, 0, 2);
23+
assertEquals(0, maxFlow);
24+
}
25+
26+
@Test
27+
@DisplayName("Source equals sink returns zero (PushRelabel)")
28+
void sourceEqualsSink() {
29+
int[][] capacity = {{0, 5}, {0, 0}};
30+
int maxFlow = PushRelabel.maxFlow(capacity, 0, 0);
31+
assertEquals(0, maxFlow);
32+
}
33+
34+
@Test
35+
@DisplayName("PushRelabel matches Dinic and EdmondsKarp on random small graphs")
36+
void parityWithOtherMaxFlow() {
37+
java.util.Random rnd = new java.util.Random(42);
38+
for (int n = 3; n <= 7; n++) {
39+
for (int it = 0; it < 25; it++) {
40+
int[][] cap = new int[n][n];
41+
for (int i = 0; i < n; i++) {
42+
for (int j = 0; j < n; j++) {
43+
if (i != j && rnd.nextDouble() < 0.35) {
44+
cap[i][j] = rnd.nextInt(10); // capacities 0..9
45+
}
46+
}
47+
}
48+
int s = 0;
49+
int t = n - 1;
50+
int fPushRelabel = PushRelabel.maxFlow(copyMatrix(cap), s, t);
51+
int fDinic = Dinic.maxFlow(copyMatrix(cap), s, t);
52+
int fEdmondsKarp = EdmondsKarp.maxFlow(cap, s, t);
53+
assertEquals(fDinic, fPushRelabel);
54+
assertEquals(fEdmondsKarp, fPushRelabel);
55+
}
56+
}
57+
}
58+
59+
private static int[][] copyMatrix(int[][] a) {
60+
int[][] b = new int[a.length][a.length];
61+
for (int i = 0; i < a.length; i++) {
62+
b[i] = java.util.Arrays.copyOf(a[i], a[i].length);
63+
}
64+
return b;
65+
}
66+
}

0 commit comments

Comments
 (0)