Skip to content

Commit 4c5525a

Browse files
feat: Added Gomory–Hu Tree (all-pairs min-cuts via n1 max-flows) (#6818)
* Add Gomory–Hu Tree (all-pairs min-cuts via n1 max-flows) * Stabilize Monte Carlo integration with antithetic variates to reduce variance * Fix Checkstyle in GomoryHuTreeTest: remove inner assignments, add braces, split declarations * SpotBugs: use RandomGenerator interface in test helper --------- Co-authored-by: Deniz Altunkapan <[email protected]>
1 parent f352f81 commit 4c5525a

File tree

4 files changed

+289
-4
lines changed

4 files changed

+289
-4
lines changed

DIRECTORY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,7 @@
385385
- 📄 [Dinic](src/main/java/com/thealgorithms/graph/Dinic.java)
386386
- 📄 [Edmonds](src/main/java/com/thealgorithms/graph/Edmonds.java)
387387
- 📄 [EdmondsKarp](src/main/java/com/thealgorithms/graph/EdmondsKarp.java)
388+
- 📄 [GomoryHuTree](src/main/java/com/thealgorithms/graph/GomoryHuTree.java)
388389
- 📄 [HierholzerAlgorithm](src/main/java/com/thealgorithms/graph/HierholzerAlgorithm.java)
389390
- 📄 [HierholzerEulerianPath](src/main/java/com/thealgorithms/graph/HierholzerEulerianPath.java)
390391
- 📄 [HopcroftKarp](src/main/java/com/thealgorithms/graph/HopcroftKarp.java)
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
package com.thealgorithms.graph;
2+
3+
import java.util.ArrayDeque;
4+
import java.util.Arrays;
5+
import java.util.Queue;
6+
7+
/**
8+
* Gomory–Hu tree construction for undirected graphs via n−1 max-flow computations.
9+
*
10+
* <p>API: {@code buildTree(int[][])} returns {@code {parent, weight}} arrays for the tree.
11+
*
12+
* @see <a href="https://en.wikipedia.org/wiki/Gomory%E2%80%93Hu_tree">Wikipedia: Gomory–Hu tree</a>
13+
*/
14+
15+
public final class GomoryHuTree {
16+
private GomoryHuTree() {
17+
}
18+
19+
public static int[][] buildTree(int[][] cap) {
20+
validateCapacityMatrix(cap);
21+
final int n = cap.length;
22+
if (n == 1) {
23+
return new int[][] {new int[] {-1}, new int[] {0}};
24+
}
25+
26+
int[] parent = new int[n];
27+
int[] weight = new int[n];
28+
Arrays.fill(parent, 0);
29+
parent[0] = -1;
30+
weight[0] = 0;
31+
32+
for (int s = 1; s < n; s++) {
33+
int t = parent[s];
34+
MaxFlowResult res = edmondsKarpWithMinCut(cap, s, t);
35+
int f = res.flow;
36+
weight[s] = f;
37+
38+
for (int v = 0; v < n; v++) {
39+
if (v != s && parent[v] == t && res.reachable[v]) {
40+
parent[v] = s;
41+
}
42+
}
43+
44+
if (t != 0 && res.reachable[parent[t]]) {
45+
parent[s] = parent[t];
46+
parent[t] = s;
47+
weight[s] = weight[t];
48+
weight[t] = f;
49+
}
50+
}
51+
return new int[][] {parent, weight};
52+
}
53+
54+
private static void validateCapacityMatrix(int[][] cap) {
55+
if (cap == null || cap.length == 0) {
56+
throw new IllegalArgumentException("Capacity matrix must not be null or empty");
57+
}
58+
final int n = cap.length;
59+
for (int i = 0; i < n; i++) {
60+
if (cap[i] == null || cap[i].length != n) {
61+
throw new IllegalArgumentException("Capacity matrix must be square");
62+
}
63+
for (int j = 0; j < n; j++) {
64+
if (cap[i][j] < 0) {
65+
throw new IllegalArgumentException("Capacities must be non-negative");
66+
}
67+
}
68+
}
69+
}
70+
71+
private static final class MaxFlowResult {
72+
final int flow;
73+
final boolean[] reachable;
74+
MaxFlowResult(int flow, boolean[] reachable) {
75+
this.flow = flow;
76+
this.reachable = reachable;
77+
}
78+
}
79+
80+
private static MaxFlowResult edmondsKarpWithMinCut(int[][] capacity, int source, int sink) {
81+
final int n = capacity.length;
82+
int[][] residual = new int[n][n];
83+
for (int i = 0; i < n; i++) {
84+
residual[i] = Arrays.copyOf(capacity[i], n);
85+
}
86+
87+
int[] parent = new int[n];
88+
int maxFlow = 0;
89+
90+
while (bfs(residual, source, sink, parent)) {
91+
int pathFlow = Integer.MAX_VALUE;
92+
for (int v = sink; v != source; v = parent[v]) {
93+
int u = parent[v];
94+
pathFlow = Math.min(pathFlow, residual[u][v]);
95+
}
96+
for (int v = sink; v != source; v = parent[v]) {
97+
int u = parent[v];
98+
residual[u][v] -= pathFlow;
99+
residual[v][u] += pathFlow;
100+
}
101+
maxFlow += pathFlow;
102+
}
103+
104+
boolean[] reachable = new boolean[n];
105+
markReachable(residual, source, reachable);
106+
return new MaxFlowResult(maxFlow, reachable);
107+
}
108+
109+
private static boolean bfs(int[][] residual, int source, int sink, int[] parent) {
110+
Arrays.fill(parent, -1);
111+
parent[source] = source;
112+
Queue<Integer> q = new ArrayDeque<>();
113+
q.add(source);
114+
while (!q.isEmpty()) {
115+
int u = q.poll();
116+
for (int v = 0; v < residual.length; v++) {
117+
if (residual[u][v] > 0 && parent[v] == -1) {
118+
parent[v] = u;
119+
if (v == sink) {
120+
return true;
121+
}
122+
q.add(v);
123+
}
124+
}
125+
}
126+
return false;
127+
}
128+
129+
private static void markReachable(int[][] residual, int source, boolean[] vis) {
130+
Arrays.fill(vis, false);
131+
Queue<Integer> q = new ArrayDeque<>();
132+
vis[source] = true;
133+
q.add(source);
134+
while (!q.isEmpty()) {
135+
int u = q.poll();
136+
for (int v = 0; v < residual.length; v++) {
137+
if (!vis[v] && residual[u][v] > 0) {
138+
vis[v] = true;
139+
q.add(v);
140+
}
141+
}
142+
}
143+
}
144+
}

src/main/java/com/thealgorithms/randomized/MonteCarloIntegration.java

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,13 +64,21 @@ private static double doApproximate(Function<Double, Double> fx, double a, doubl
6464
if (!validate(fx, a, b, n)) {
6565
throw new IllegalArgumentException("Invalid input parameters");
6666
}
67-
double totalArea = 0.0;
67+
double total = 0.0;
6868
double interval = b - a;
69-
for (int i = 0; i < n; i++) {
69+
int pairs = n / 2;
70+
for (int i = 0; i < pairs; i++) {
71+
double u = generator.nextDouble();
72+
double x1 = a + u * interval;
73+
double x2 = a + (1.0 - u) * interval;
74+
total += fx.apply(x1);
75+
total += fx.apply(x2);
76+
}
77+
if ((n & 1) == 1) {
7078
double x = a + generator.nextDouble() * interval;
71-
totalArea += fx.apply(x);
79+
total += fx.apply(x);
7280
}
73-
return interval * totalArea / n;
81+
return interval * total / n;
7482
}
7583

7684
private static boolean validate(Function<Double, Double> fx, double a, double b, int n) {
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
package com.thealgorithms.graph;
2+
3+
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
5+
import java.util.ArrayDeque;
6+
import java.util.ArrayList;
7+
import java.util.Arrays;
8+
import java.util.List;
9+
import java.util.Queue;
10+
import java.util.Random;
11+
import java.util.random.RandomGenerator;
12+
import org.junit.jupiter.api.DisplayName;
13+
import org.junit.jupiter.api.Test;
14+
15+
class GomoryHuTreeTest {
16+
17+
@Test
18+
@DisplayName("Single node graph")
19+
void singleNode() {
20+
int[][] cap = {{0}};
21+
int[][] res = GomoryHuTree.buildTree(cap);
22+
int[] parent = res[0];
23+
int[] weight = res[1];
24+
assertEquals(-1, parent[0]);
25+
assertEquals(0, weight[0]);
26+
}
27+
28+
@Test
29+
@DisplayName("Triangle undirected graph with known min-cuts")
30+
void triangleGraph() {
31+
// 0-1:3, 1-2:2, 0-2:4
32+
int[][] cap = new int[3][3];
33+
cap[0][1] = 3;
34+
cap[1][0] = 3;
35+
cap[1][2] = 2;
36+
cap[2][1] = 2;
37+
cap[0][2] = 4;
38+
cap[2][0] = 4;
39+
40+
int[][] tree = GomoryHuTree.buildTree(cap);
41+
// validate all pairs via path-min-edge equals maxflow
42+
validateAllPairs(cap, tree);
43+
}
44+
45+
@Test
46+
@DisplayName("Random small undirected graphs compare to EdmondsKarp")
47+
void randomSmallGraphs() {
48+
Random rng = new Random(42);
49+
for (int n = 2; n <= 6; n++) {
50+
for (int iter = 0; iter < 10; iter++) {
51+
int[][] cap = randSymmetricMatrix(n, 0, 5, rng);
52+
int[][] tree = GomoryHuTree.buildTree(cap);
53+
validateAllPairs(cap, tree);
54+
}
55+
}
56+
}
57+
58+
private static int[][] randSymmetricMatrix(int n, int lo, int hi, RandomGenerator rng) {
59+
int[][] a = new int[n][n];
60+
for (int i = 0; i < n; i++) {
61+
for (int j = i + 1; j < n; j++) {
62+
int w = rng.nextInt(hi - lo + 1) + lo;
63+
a[i][j] = w;
64+
a[j][i] = w;
65+
}
66+
}
67+
// zero diagonal
68+
for (int i = 0; i < n; i++) {
69+
a[i][i] = 0;
70+
}
71+
return a;
72+
}
73+
74+
private static void validateAllPairs(int[][] cap, int[][] tree) {
75+
int n = cap.length;
76+
int[] parent = tree[0];
77+
int[] weight = tree[1];
78+
79+
// build adjacency list of tree without generic array creation
80+
List<List<int[]>> g = new ArrayList<>();
81+
for (int i = 0; i < n; i++) {
82+
g.add(new ArrayList<>());
83+
}
84+
for (int v = 1; v < n; v++) {
85+
int u = parent[v];
86+
int w = weight[v];
87+
g.get(u).add(new int[] {v, w});
88+
g.get(v).add(new int[] {u, w});
89+
}
90+
91+
for (int s = 0; s < n; s++) {
92+
for (int t = s + 1; t < n; t++) {
93+
int treeVal = minEdgeOnPath(g, s, t);
94+
int flowVal = EdmondsKarp.maxFlow(cap, s, t);
95+
assertEquals(flowVal, treeVal, "pair (" + s + "," + t + ")");
96+
}
97+
}
98+
}
99+
100+
private static int minEdgeOnPath(List<List<int[]>> g, int s, int t) {
101+
// BFS to record parent and edge weight along the path, since it's a tree, unique path exists
102+
int n = g.size();
103+
int[] parent = new int[n];
104+
int[] edgeW = new int[n];
105+
Arrays.fill(parent, -1);
106+
Queue<Integer> q = new ArrayDeque<>();
107+
q.add(s);
108+
parent[s] = s;
109+
while (!q.isEmpty()) {
110+
int u = q.poll();
111+
if (u == t) {
112+
break;
113+
}
114+
for (int[] e : g.get(u)) {
115+
int v = e[0];
116+
int w = e[1];
117+
if (parent[v] == -1) {
118+
parent[v] = u;
119+
edgeW[v] = w;
120+
q.add(v);
121+
}
122+
}
123+
}
124+
int cur = t;
125+
int ans = Integer.MAX_VALUE;
126+
while (cur != s) {
127+
ans = Math.min(ans, edgeW[cur]);
128+
cur = parent[cur];
129+
}
130+
return ans == Integer.MAX_VALUE ? 0 : ans;
131+
}
132+
}

0 commit comments

Comments
 (0)