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: + *

    + *
  1. Start with each node as its own component (using Union-Find).
  2. + *
  3. For each component, find the minimum-weight edge crossing to another component.
  4. + *
  5. Add all such cheapest edges to the MST and merge the components.
  6. + *
  7. Repeat until only one component remains, or no more merges are possible.
  8. + *
+ * + *

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 { + static class Edge { int u, v, cost; public Edge(int u, int v, int cost) { @@ -12,145 +36,127 @@ public Edge(int u, int v, int cost) { this.v = v; this.cost = cost; } - - @Override - public String toString() { - return String.format("%d %d, cost: %d", u, v, cost); - } - - @Override - public int compareTo(Edge other) { - int cmp = cost - other.cost; - // Break ties by picking lexicographically smallest edge pair. - if (cmp == 0) { - cmp = u - other.u; - if (cmp == 0) return v - other.v; - return cmp; - } - return cmp; - } } - // Inputs - private final int n; // Number of nodes - private final Edge[] graph; // Edge list - - // Internal + private final int n; + private final Edge[] graph; private boolean solved; private boolean mstExists; - - // Outputs private long minCostSum; private List mst; public Boruvkas(int n, Edge[] graph) { - if (graph == null) throw new IllegalArgumentException(); + if (graph == null) { + throw new IllegalArgumentException(); + } this.graph = graph; this.n = n; + this.mst = new ArrayList<>(); } - // Returns the edges used in finding the minimum spanning tree, or returns - // null if no MST exists. - public List getMst() { + /** + * Returns the edges in the MST, or empty if the graph is disconnected. + */ + public Optional> 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 mst1 = solver.getMst(); - List mst2 = solver.getMst(); + long cost1 = solver.getMstCost().getAsLong(); + long cost2 = solver.getMstCost().getAsLong(); + List mst1 = solver.getMst().get(); + List mst2 = solver.getMst().get(); assertThat(cost1).isEqualTo(cost2); assertThat(mst1).isEqualTo(mst2); @@ -200,8 +191,8 @@ public void testLargerGraph() { }; Boruvkas solver = new Boruvkas(9, graph); // Known MST cost for this classic graph - assertThat(solver.getMstCost()).isEqualTo(37L); - assertThat(solver.getMst()).hasSize(8); + assertThat(solver.getMstCost().getAsLong()).isEqualTo(37L); + assertThat(solver.getMst().get()).hasSize(8); } @Test @@ -215,9 +206,8 @@ public void testMstEdgesFormSpanningTree() { new Edge(0, 2, 5) }; Boruvkas solver = new Boruvkas(4, graph); - List mst = solver.getMst(); + List mst = solver.getMst().get(); - assertThat(mst).isNotNull(); assertThat(mst).hasSize(3); // Verify MST connects all nodes (using simple connectivity check) @@ -253,7 +243,7 @@ public void testParallelEdges() { }; Boruvkas solver = new Boruvkas(3, graph); // Should pick the minimum weight edge between 0 and 1 - assertThat(solver.getMstCost()).isEqualTo(5L); - assertThat(solver.getMst()).hasSize(2); + assertThat(solver.getMstCost().getAsLong()).isEqualTo(5L); + assertThat(solver.getMst().get()).hasSize(2); } }