diff --git a/src/main/java/com/williamfiset/algorithms/graphtheory/Boruvkas.java b/src/main/java/com/williamfiset/algorithms/graphtheory/Boruvkas.java index ff3dd82f2..331bcba7c 100644 --- a/src/main/java/com/williamfiset/algorithms/graphtheory/Boruvkas.java +++ b/src/main/java/com/williamfiset/algorithms/graphtheory/Boruvkas.java @@ -1,10 +1,34 @@ +/** + * Boruvka's Minimum Spanning Tree Algorithm — Edge List + * + *
Finds the MST of a weighted undirected graph by repeatedly selecting the + * cheapest outgoing edge from each connected component and merging components. + * + *
Algorithm: + *
If the graph is disconnected, no MST exists and the solver returns null. + * + *
Time: O(E log V) + *
Space: O(V + E)
+ *
+ * @author William Fiset, william.alexandre.fiset@gmail.com
+ */
package com.williamfiset.algorithms.graphtheory;
-import java.util.*;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.OptionalLong;
public class Boruvkas {
- static class Edge implements Comparable> getMst() {
solve();
- return mstExists ? mst : null;
+ return mstExists ? Optional.of(mst) : Optional.empty();
}
- public Long getMstCost() {
+ /**
+ * Returns the total cost of the MST, or empty if the graph is disconnected.
+ */
+ public OptionalLong getMstCost() {
solve();
- return mstExists ? minCostSum : null;
+ return mstExists ? OptionalLong.of(minCostSum) : OptionalLong.empty();
}
- // Given a graph represented as an edge list this method finds
- // the Minimum Spanning Tree (MST) cost if there exists
- // a MST, otherwise it returns null.
private void solve() {
- if (solved) return;
+ if (solved) {
+ return;
+ }
- mst = new ArrayList<>();
UnionFind uf = new UnionFind(n);
while (uf.components > 1) {
- boolean stop = true;
Edge[] cheapest = new Edge[n];
- // Find the cheapest edge for each component
+ // For each edge, track the cheapest crossing edge for each component.
for (Edge e : graph) {
int root1 = uf.find(e.u);
int root2 = uf.find(e.v);
- if (root1 == root2) continue;
-
+ if (root1 == root2) {
+ continue;
+ }
if (cheapest[root1] == null || e.cost < cheapest[root1].cost) {
cheapest[root1] = e;
- stop = false;
}
if (cheapest[root2] == null || e.cost < cheapest[root2].cost) {
cheapest[root2] = e;
- stop = false;
}
}
- if (stop) break;
-
- // Add the cheapest edges to the MST
- for (int i = 0; i < n; i++) {
- Edge e = cheapest[i];
- if (e == null) {
- continue;
- }
- int root1 = uf.find(e.u);
- int root2 = uf.find(e.v);
- if (root1 != root2) {
- uf.union(root1, root2);
+ // Merge components using their cheapest crossing edges.
+ int prevComponents = uf.components;
+ for (Edge e : cheapest) {
+ if (e != null && uf.find(e.u) != uf.find(e.v)) {
+ uf.union(e.u, e.v);
mst.add(e);
minCostSum += e.cost;
}
}
+
+ if (uf.components == prevComponents) {
+ break;
+ }
}
mstExists = (mst.size() == n - 1);
solved = true;
}
+ // ==================== Main ====================
+
+ //
+ // 1 7 2
+ // 0 --------------- 1 --------------- 2 --------------- 3
+ // | | | |
+ // | | | |
+ // 4 | 3 | 5 | 6 |
+ // | | | |
+ // | | | |
+ // 4 --------------- 5 --------------- 6 --------------- 7
+ // 8 2 9
+ //
+ // MST cost: 23
+ //
public static void main(String[] args) {
-
- int n = 10, m = 18, i = 0;
- Edge[] g = new Edge[m];
-
- // Edges are treated as undirected
- g[i++] = new Edge(0, 1, 5);
- g[i++] = new Edge(0, 3, 4);
- g[i++] = new Edge(0, 4, 1);
- g[i++] = new Edge(1, 2, 4);
- g[i++] = new Edge(1, 3, 2);
- g[i++] = new Edge(2, 7, 4);
- g[i++] = new Edge(2, 8, 1);
- g[i++] = new Edge(2, 9, 2);
- g[i++] = new Edge(3, 6, 11);
- g[i++] = new Edge(3, 7, 2);
- g[i++] = new Edge(4, 3, 2);
- g[i++] = new Edge(4, 5, 1);
- g[i++] = new Edge(5, 3, 5);
- g[i++] = new Edge(5, 6, 7);
- g[i++] = new Edge(6, 7, 1);
- g[i++] = new Edge(6, 8, 4);
- g[i++] = new Edge(7, 8, 6);
- g[i++] = new Edge(9, 8, 0);
-
- Boruvkas solver = new Boruvkas(n, g);
-
- Long ans = solver.getMstCost();
- if (ans != null) {
- System.out.println("MST cost: " + ans);
- for (Edge e : solver.getMst()) {
- System.out.println(e);
+ Edge[] g = {
+ new Edge(0, 1, 1),
+ new Edge(1, 2, 7),
+ new Edge(2, 3, 2),
+ new Edge(0, 4, 4),
+ new Edge(1, 5, 3),
+ new Edge(2, 6, 5),
+ new Edge(3, 7, 6),
+ new Edge(4, 5, 8),
+ new Edge(5, 6, 2),
+ new Edge(6, 7, 9),
+ };
+
+ Boruvkas solver = new Boruvkas(8, g);
+
+ OptionalLong cost = solver.getMstCost();
+ if (cost.isPresent()) {
+ System.out.println("MST cost: " + cost.getAsLong()); // 23
+ for (Edge e : solver.getMst().get()) {
+ System.out.printf("Edge %d-%d, cost: %d%n", e.u, e.v, e.cost);
}
} else {
System.out.println("No MST exists");
}
}
- // Union find data structure
+ // Union-Find with path compression and union by size.
private static class UnionFind {
int components;
int[] id, sz;
@@ -166,27 +172,17 @@ public UnionFind(int n) {
}
public int find(int p) {
- int root = p;
- while (root != id[root]) root = id[root];
- while (p != root) { // Do path compression
- int next = id[p];
- id[p] = root;
- p = next;
+ if (id[p] != p) {
+ id[p] = find(id[p]);
}
- return root;
- }
-
- public boolean connected(int p, int q) {
- return find(p) == find(q);
- }
-
- public int size(int p) {
- return sz[find(p)];
+ return id[p];
}
public void union(int p, int q) {
int root1 = find(p), root2 = find(q);
- if (root1 == root2) return;
+ if (root1 == root2) {
+ return;
+ }
if (sz[root1] < sz[root2]) {
sz[root2] += sz[root1];
id[root1] = root2;
diff --git a/src/test/java/com/williamfiset/algorithms/graphtheory/BoruvkasTest.java b/src/test/java/com/williamfiset/algorithms/graphtheory/BoruvkasTest.java
index 4d0585c65..6c084c501 100644
--- a/src/test/java/com/williamfiset/algorithms/graphtheory/BoruvkasTest.java
+++ b/src/test/java/com/williamfiset/algorithms/graphtheory/BoruvkasTest.java
@@ -18,24 +18,24 @@ public void testNullGraphThrowsException() {
public void testSingleNode() {
Edge[] graph = new Edge[0];
Boruvkas solver = new Boruvkas(1, graph);
- assertThat(solver.getMstCost()).isEqualTo(0L);
- assertThat(solver.getMst()).isEmpty();
+ assertThat(solver.getMstCost().getAsLong()).isEqualTo(0L);
+ assertThat(solver.getMst().get()).isEmpty();
}
@Test
public void testTwoNodesConnected() {
Edge[] graph = new Edge[] {new Edge(0, 1, 5)};
Boruvkas solver = new Boruvkas(2, graph);
- assertThat(solver.getMstCost()).isEqualTo(5L);
- assertThat(solver.getMst()).hasSize(1);
+ assertThat(solver.getMstCost().getAsLong()).isEqualTo(5L);
+ assertThat(solver.getMst().get()).hasSize(1);
}
@Test
public void testTwoNodesDisconnected() {
Edge[] graph = new Edge[0];
Boruvkas solver = new Boruvkas(2, graph);
- assertThat(solver.getMstCost()).isNull();
- assertThat(solver.getMst()).isNull();
+ assertThat(solver.getMstCost().isEmpty()).isTrue();
+ assertThat(solver.getMst().isEmpty()).isTrue();
}
@Test
@@ -43,8 +43,8 @@ public void testSimpleTriangle() {
Edge[] graph =
new Edge[] {new Edge(0, 1, 1), new Edge(1, 2, 2), new Edge(0, 2, 3)};
Boruvkas solver = new Boruvkas(3, graph);
- assertThat(solver.getMstCost()).isEqualTo(3L);
- assertThat(solver.getMst()).hasSize(2);
+ assertThat(solver.getMstCost().getAsLong()).isEqualTo(3L);
+ assertThat(solver.getMst().get()).hasSize(2);
}
@Test
@@ -52,38 +52,29 @@ public void testDisconnectedGraph() {
// Two separate components: {0,1} and {2,3}
Edge[] graph = new Edge[] {new Edge(0, 1, 1), new Edge(2, 3, 2)};
Boruvkas solver = new Boruvkas(4, graph);
- assertThat(solver.getMstCost()).isNull();
- assertThat(solver.getMst()).isNull();
+ assertThat(solver.getMstCost().isEmpty()).isTrue();
+ assertThat(solver.getMst().isEmpty()).isTrue();
}
@Test
public void testExampleFromMainMethod() {
- int n = 10, m = 18, i = 0;
- Edge[] g = new Edge[m];
+ Edge[] g = {
+ new Edge(0, 1, 1),
+ new Edge(1, 2, 7),
+ new Edge(2, 3, 2),
+ new Edge(0, 4, 4),
+ new Edge(1, 5, 3),
+ new Edge(2, 6, 5),
+ new Edge(3, 7, 6),
+ new Edge(4, 5, 8),
+ new Edge(5, 6, 2),
+ new Edge(6, 7, 9),
+ };
- g[i++] = new Edge(0, 1, 5);
- g[i++] = new Edge(0, 3, 4);
- g[i++] = new Edge(0, 4, 1);
- g[i++] = new Edge(1, 2, 4);
- g[i++] = new Edge(1, 3, 2);
- g[i++] = new Edge(2, 7, 4);
- g[i++] = new Edge(2, 8, 1);
- g[i++] = new Edge(2, 9, 2);
- g[i++] = new Edge(3, 6, 11);
- g[i++] = new Edge(3, 7, 2);
- g[i++] = new Edge(4, 3, 2);
- g[i++] = new Edge(4, 5, 1);
- g[i++] = new Edge(5, 3, 5);
- g[i++] = new Edge(5, 6, 7);
- g[i++] = new Edge(6, 7, 1);
- g[i++] = new Edge(6, 8, 4);
- g[i++] = new Edge(7, 8, 6);
- g[i++] = new Edge(9, 8, 0);
+ Boruvkas solver = new Boruvkas(8, g);
- Boruvkas solver = new Boruvkas(n, g);
-
- assertThat(solver.getMstCost()).isEqualTo(14L);
- assertThat(solver.getMst()).hasSize(n - 1);
+ assertThat(solver.getMstCost().getAsLong()).isEqualTo(23L);
+ assertThat(solver.getMst().get()).hasSize(7);
}
@Test
@@ -94,8 +85,8 @@ public void testLinearGraph() {
new Edge(0, 1, 1), new Edge(1, 2, 2), new Edge(2, 3, 3), new Edge(3, 4, 4)
};
Boruvkas solver = new Boruvkas(5, graph);
- assertThat(solver.getMstCost()).isEqualTo(10L);
- assertThat(solver.getMst()).hasSize(4);
+ assertThat(solver.getMstCost().getAsLong()).isEqualTo(10L);
+ assertThat(solver.getMst().get()).hasSize(4);
}
@Test
@@ -112,8 +103,8 @@ public void testCompleteGraphK4() {
};
Boruvkas solver = new Boruvkas(4, graph);
// MST should be: 0-1 (1), 1-2 (2), 0-3 (3) = 6
- assertThat(solver.getMstCost()).isEqualTo(6L);
- assertThat(solver.getMst()).hasSize(3);
+ assertThat(solver.getMstCost().getAsLong()).isEqualTo(6L);
+ assertThat(solver.getMst().get()).hasSize(3);
}
@Test
@@ -121,8 +112,8 @@ public void testGraphWithZeroWeightEdges() {
Edge[] graph =
new Edge[] {new Edge(0, 1, 0), new Edge(1, 2, 0), new Edge(2, 3, 0)};
Boruvkas solver = new Boruvkas(4, graph);
- assertThat(solver.getMstCost()).isEqualTo(0L);
- assertThat(solver.getMst()).hasSize(3);
+ assertThat(solver.getMstCost().getAsLong()).isEqualTo(0L);
+ assertThat(solver.getMst().get()).hasSize(3);
}
@Test
@@ -130,8 +121,8 @@ public void testGraphWithNegativeWeightEdges() {
Edge[] graph =
new Edge[] {new Edge(0, 1, -5), new Edge(1, 2, -3), new Edge(0, 2, 10)};
Boruvkas solver = new Boruvkas(3, graph);
- assertThat(solver.getMstCost()).isEqualTo(-8L);
- assertThat(solver.getMst()).hasSize(2);
+ assertThat(solver.getMstCost().getAsLong()).isEqualTo(-8L);
+ assertThat(solver.getMst().get()).hasSize(2);
}
@Test
@@ -146,8 +137,8 @@ public void testGraphWithEqualWeightEdges() {
new Edge(0, 2, 5)
};
Boruvkas solver = new Boruvkas(4, graph);
- assertThat(solver.getMstCost()).isEqualTo(15L);
- assertThat(solver.getMst()).hasSize(3);
+ assertThat(solver.getMstCost().getAsLong()).isEqualTo(15L);
+ assertThat(solver.getMst().get()).hasSize(3);
}
@Test
@@ -158,8 +149,8 @@ public void testStarGraph() {
new Edge(0, 1, 1), new Edge(0, 2, 2), new Edge(0, 3, 3), new Edge(0, 4, 4)
};
Boruvkas solver = new Boruvkas(5, graph);
- assertThat(solver.getMstCost()).isEqualTo(10L);
- assertThat(solver.getMst()).hasSize(4);
+ assertThat(solver.getMstCost().getAsLong()).isEqualTo(10L);
+ assertThat(solver.getMst().get()).hasSize(4);
}
@Test
@@ -169,10 +160,10 @@ public void testMstIsIdempotent() {
Boruvkas solver = new Boruvkas(3, graph);
// Call multiple times to verify idempotency
- Long cost1 = solver.getMstCost();
- Long cost2 = solver.getMstCost();
- List