Add Boruvka's algorithm to find Minimum Spanning Tree (#4964)

This commit is contained in:
Niklas Hoefflin
2023-12-02 18:53:17 +01:00
committed by GitHub
parent 9bebcee5c7
commit e759544c33
3 changed files with 428 additions and 0 deletions

View File

@ -0,0 +1,217 @@
package com.thealgorithms.datastructures.graphs;
import java.util.ArrayList;
import java.util.List;
/**
* Boruvka's algorithm to find Minimum Spanning Tree
* (https://en.wikipedia.org/wiki/Bor%C5%AFvka%27s_algorithm)
*
* @author itakurah (https://github.com/itakurah)
*/
final class BoruvkaAlgorithm {
private BoruvkaAlgorithm() {
}
/**
* Represents an edge in the graph
*/
static class Edge {
final int src;
final int dest;
final int weight;
Edge(final int src, final int dest, final int weight) {
this.src = src;
this.dest = dest;
this.weight = weight;
}
}
/**
* Represents the graph
*/
static class Graph {
final int vertex;
final List<Edge> edges;
/**
* Constructor for the graph
*
* @param vertex number of vertices
* @param edges list of edges
*/
Graph(final int vertex, final List<Edge> edges) {
if (vertex < 0) {
throw new IllegalArgumentException("Number of vertices must be positive");
}
if (edges == null || edges.isEmpty()) {
throw new IllegalArgumentException("Edges list must not be null or empty");
}
for (final var edge : edges) {
checkEdgeVertices(edge.src, vertex);
checkEdgeVertices(edge.dest, vertex);
}
this.vertex = vertex;
this.edges = edges;
}
}
/**
* Represents a subset for Union-Find operations
*/
private static class Component {
int parent;
int rank;
Component(final int parent, final int rank) {
this.parent = parent;
this.rank = rank;
}
}
/**
* Represents the state of Union-Find components and the result list
*/
private static class BoruvkaState {
List<Edge> result;
Component[] components;
final Graph graph;
BoruvkaState(final Graph graph) {
this.result = new ArrayList<>();
this.components = initializeComponents(graph);
this.graph = graph;
}
/**
* Adds the cheapest edges to the result list and performs Union operation on the subsets.
*
* @param cheapest Array containing the cheapest edge for each subset.
*/
void merge(final Edge[] cheapest) {
for (int i = 0; i < graph.vertex; ++i) {
if (cheapest[i] != null) {
final var component1 = find(components, cheapest[i].src);
final var component2 = find(components, cheapest[i].dest);
if (component1 != component2) {
result.add(cheapest[i]);
union(components, component1, component2);
}
}
}
}
/**
* Checks if there are more edges to add to the result list
*
* @return true if there are more edges to add, false otherwise
*/
boolean hasMoreEdgesToAdd() {
return result.size() < graph.vertex - 1;
}
/**
* Computes the cheapest edges for each subset in the Union-Find structure.
*
* @return an array containing the cheapest edge for each subset.
*/
private Edge[] computeCheapestEdges() {
Edge[] cheapest = new Edge[graph.vertex];
for (final var edge : graph.edges) {
final var set1 = find(components, edge.src);
final var set2 = find(components, edge.dest);
if (set1 != set2) {
if (cheapest[set1] == null || edge.weight < cheapest[set1].weight) {
cheapest[set1] = edge;
}
if (cheapest[set2] == null || edge.weight < cheapest[set2].weight) {
cheapest[set2] = edge;
}
}
}
return cheapest;
}
/**
* Initializes subsets for Union-Find
*
* @param graph the graph
* @return the initialized subsets
*/
private static Component[] initializeComponents(final Graph graph) {
Component[] components = new Component[graph.vertex];
for (int v = 0; v < graph.vertex; ++v) {
components[v] = new Component(v, 0);
}
return components;
}
}
/**
* Finds the parent of the subset using path compression
*
* @param components array of subsets
* @param i index of the subset
* @return the parent of the subset
*/
static int find(final Component[] components, final int i) {
if (components[i].parent != i) {
components[i].parent = find(components, components[i].parent);
}
return components[i].parent;
}
/**
* Performs the Union operation for Union-Find
*
* @param components array of subsets
* @param x index of the first subset
* @param y index of the second subset
*/
static void union(Component[] components, final int x, final int y) {
final int xroot = find(components, x);
final int yroot = find(components, y);
if (components[xroot].rank < components[yroot].rank) {
components[xroot].parent = yroot;
} else if (components[xroot].rank > components[yroot].rank) {
components[yroot].parent = xroot;
} else {
components[yroot].parent = xroot;
components[xroot].rank++;
}
}
/**
* Boruvka's algorithm to find the Minimum Spanning Tree
*
* @param graph the graph
* @return list of edges in the Minimum Spanning Tree
*/
static List<Edge> boruvkaMST(final Graph graph) {
var boruvkaState = new BoruvkaState(graph);
while (boruvkaState.hasMoreEdgesToAdd()) {
final var cheapest = boruvkaState.computeCheapestEdges();
boruvkaState.merge(cheapest);
}
return boruvkaState.result;
}
/**
* Checks if the edge vertices are in a valid range
*
* @param vertex the vertex to check
* @param upperBound the upper bound for the vertex range
*/
private static void checkEdgeVertices(final int vertex, final int upperBound) {
if (vertex < 0 || vertex >= upperBound) {
throw new IllegalArgumentException("Edge vertex out of range");
}
}
}

View File

@ -0,0 +1,191 @@
package com.thealgorithms.datastructures.graphs;
import static org.junit.jupiter.api.Assertions.*;
import com.thealgorithms.datastructures.graphs.BoruvkaAlgorithm.Graph;
import java.util.ArrayList;
import java.util.List;
import org.junit.jupiter.api.Test;
public class BoruvkaAlgorithmTest {
@Test
public void testBoruvkaMSTV9E14() {
List<BoruvkaAlgorithm.Edge> edges = new ArrayList<>();
edges.add(new BoruvkaAlgorithm.Edge(0, 1, 10));
edges.add(new BoruvkaAlgorithm.Edge(0, 2, 12));
edges.add(new BoruvkaAlgorithm.Edge(1, 2, 9));
edges.add(new BoruvkaAlgorithm.Edge(1, 3, 8));
edges.add(new BoruvkaAlgorithm.Edge(2, 4, 3));
edges.add(new BoruvkaAlgorithm.Edge(2, 5, 1));
edges.add(new BoruvkaAlgorithm.Edge(4, 5, 3));
edges.add(new BoruvkaAlgorithm.Edge(4, 3, 7));
edges.add(new BoruvkaAlgorithm.Edge(3, 6, 8));
edges.add(new BoruvkaAlgorithm.Edge(3, 7, 5));
edges.add(new BoruvkaAlgorithm.Edge(5, 7, 6));
edges.add(new BoruvkaAlgorithm.Edge(6, 7, 9));
edges.add(new BoruvkaAlgorithm.Edge(6, 8, 2));
edges.add(new BoruvkaAlgorithm.Edge(7, 8, 11));
final var graph = new Graph(9, edges);
/**
* Adjacency matrix
* 0 1 2 3 4 5 6 7 8
* 0 0 10 12 0 0 0 0 0 0
* 1 10 0 9 8 0 0 0 0 0
* 2 12 9 0 0 3 1 0 0 0
* 3 0 8 0 0 7 0 8 5 0
* 4 0 0 3 7 0 3 0 0 0
* 5 0 0 1 0 3 0 0 6 0
* 6 0 0 0 8 0 0 0 9 2
* 7 0 0 0 5 0 6 9 0 11
* 8 0 0 0 0 0 0 2 11 0
*/
final var result = BoruvkaAlgorithm.boruvkaMST(graph);
assertEquals(8, result.size());
assertEquals(43, computeTotalWeight(result));
}
@Test
void testBoruvkaMSTV2E1() {
List<BoruvkaAlgorithm.Edge> edges = new ArrayList<>();
edges.add(new BoruvkaAlgorithm.Edge(0, 1, 10));
final var graph = new Graph(2, edges);
/**
* Adjacency matrix
* 0 1
* 0 0 10
* 1 10 0
*/
final var result = BoruvkaAlgorithm.boruvkaMST(graph);
assertEquals(1, result.size());
assertEquals(10, computeTotalWeight(result));
}
@Test
void testCompleteGraphK4() {
List<BoruvkaAlgorithm.Edge> edges = new ArrayList<>();
edges.add(new BoruvkaAlgorithm.Edge(0, 1, 7));
edges.add(new BoruvkaAlgorithm.Edge(0, 2, 2));
edges.add(new BoruvkaAlgorithm.Edge(0, 3, 5));
edges.add(new BoruvkaAlgorithm.Edge(1, 2, 3));
edges.add(new BoruvkaAlgorithm.Edge(1, 3, 4));
edges.add(new BoruvkaAlgorithm.Edge(2, 3, 1));
final var graph = new Graph(4, edges);
/**
* Adjacency matrix
* 0 1 2 3
* 0 0 7 2 5
* 1 7 0 3 4
* 2 2 3 0 1
* 3 5 4 1 0
*/
final var result = BoruvkaAlgorithm.boruvkaMST(graph);
assertEquals(3, result.size());
assertEquals(6, computeTotalWeight(result));
}
@Test
void testNegativeVertices() {
Exception exception1 = assertThrows(IllegalArgumentException.class, () -> new Graph(-1, null));
String expectedMessage = "Number of vertices must be positive";
String actualMessage = exception1.getMessage();
assertTrue(actualMessage.contains(expectedMessage));
}
@Test
void testEdgesNull() {
Exception exception = assertThrows(IllegalArgumentException.class, () -> new Graph(0, null));
String expectedMessage = "Edges list must not be null or empty";
String actualMessage = exception.getMessage();
assertTrue(actualMessage.contains(expectedMessage));
}
@Test
void testEdgesEmpty() {
Exception exception = assertThrows(IllegalArgumentException.class, () -> new Graph(0, new ArrayList<>()));
String expectedMessage = "Edges list must not be null or empty";
String actualMessage = exception.getMessage();
assertTrue(actualMessage.contains(expectedMessage));
}
@Test
void testEdgesRange() {
// Valid input
List<BoruvkaAlgorithm.Edge> validEdges = new ArrayList<>();
validEdges.add(new BoruvkaAlgorithm.Edge(0, 1, 2));
validEdges.add(new BoruvkaAlgorithm.Edge(1, 2, 3));
final var validGraph = new BoruvkaAlgorithm.Graph(3, validEdges);
assertEquals(validEdges, validGraph.edges);
// Edge source out of range
Exception exception1 = assertThrows(IllegalArgumentException.class, () -> {
List<BoruvkaAlgorithm.Edge> invalidEdges = new ArrayList<>();
invalidEdges.add(new BoruvkaAlgorithm.Edge(-1, 1, 2));
final var invalidGraph = new BoruvkaAlgorithm.Graph(1, invalidEdges);
assertEquals(invalidEdges, invalidGraph.edges);
});
String expectedMessage1 = "Edge vertex out of range";
String actualMessage1 = exception1.getMessage();
assertTrue(actualMessage1.contains(expectedMessage1));
// Edge source out of range
Exception exception2 = assertThrows(IllegalArgumentException.class, () -> {
List<BoruvkaAlgorithm.Edge> invalidEdges = new ArrayList<>();
invalidEdges.add(new BoruvkaAlgorithm.Edge(1, 0, 2));
final var invalidGraph = new BoruvkaAlgorithm.Graph(1, invalidEdges);
assertEquals(invalidEdges, invalidGraph.edges);
});
String expectedMessage2 = "Edge vertex out of range";
String actualMessage2 = exception2.getMessage();
assertTrue(actualMessage2.contains(expectedMessage2));
// Edge destination out of range
Exception exception3 = assertThrows(IllegalArgumentException.class, () -> {
List<BoruvkaAlgorithm.Edge> invalidEdges = new ArrayList<>();
invalidEdges.add(new BoruvkaAlgorithm.Edge(0, -1, 2));
final var invalidGraph = new BoruvkaAlgorithm.Graph(1, invalidEdges);
assertEquals(invalidEdges, invalidGraph.edges);
});
String expectedMessage3 = "Edge vertex out of range";
String actualMessage3 = exception3.getMessage();
assertTrue(actualMessage3.contains(expectedMessage3));
// Edge destination out of range
Exception exception4 = assertThrows(IllegalArgumentException.class, () -> {
List<BoruvkaAlgorithm.Edge> invalidEdges = new ArrayList<>();
invalidEdges.add(new BoruvkaAlgorithm.Edge(0, 1, 2));
final var invalidGraph = new BoruvkaAlgorithm.Graph(1, invalidEdges);
assertEquals(invalidEdges, invalidGraph.edges);
});
String expectedMessage4 = "Edge vertex out of range";
String actualMessage4 = exception4.getMessage();
assertTrue(actualMessage4.contains(expectedMessage4));
}
/**
* Computes the total weight of the Minimum Spanning Tree
*
* @param result list of edges in the Minimum Spanning Tree
* @return the total weight of the Minimum Spanning Tree
*/
int computeTotalWeight(final List<BoruvkaAlgorithm.Edge> result) {
int totalWeight = 0;
for (final var edge : result) {
totalWeight += edge.weight;
}
return totalWeight;
}
}