mirror of
https://github.com/TheAlgorithms/Java.git
synced 2026-03-13 08:40:43 +08:00
feat(graph): Add Edmonds's algorithm for minimum spanning arborescence (#6771)
* feat(graph): Add Edmonds's algorithm for minimum spanning arborescence * test: Add test cases to achieve 100% coverage
This commit is contained in:
201
src/main/java/com/thealgorithms/graph/Edmonds.java
Normal file
201
src/main/java/com/thealgorithms/graph/Edmonds.java
Normal file
@@ -0,0 +1,201 @@
|
||||
package com.thealgorithms.graph;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* An implementation of Edmonds's algorithm (also known as the Chu–Liu/Edmonds algorithm)
|
||||
* for finding a Minimum Spanning Arborescence (MSA).
|
||||
*
|
||||
* <p>An MSA is a directed graph equivalent of a Minimum Spanning Tree. It is a tree rooted
|
||||
* at a specific vertex 'r' that reaches all other vertices, such that the sum of the
|
||||
* weights of its edges is minimized.
|
||||
*
|
||||
* <p>The algorithm works recursively:
|
||||
* <ol>
|
||||
* <li>For each vertex other than the root, select the incoming edge with the minimum weight.</li>
|
||||
* <li>If the selected edges form a spanning arborescence, it is the MSA.</li>
|
||||
* <li>If cycles are formed, contract each cycle into a new "supernode".</li>
|
||||
* <li>Modify the weights of edges entering the new supernode.</li>
|
||||
* <li>Recursively call the algorithm on the contracted graph.</li>
|
||||
* <li>The final cost is the sum of the initial edge selections and the result of the recursive call.</li>
|
||||
* </ol>
|
||||
*
|
||||
* <p>Time Complexity: O(E * V) where E is the number of edges and V is the number of vertices.
|
||||
*
|
||||
* <p>References:
|
||||
* <ul>
|
||||
* <li><a href="https://en.wikipedia.org/wiki/Edmonds%27_algorithm">Wikipedia: Edmonds's algorithm</a></li>
|
||||
* </ul>
|
||||
*/
|
||||
public final class Edmonds {
|
||||
|
||||
private Edmonds() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a directed weighted edge in the graph.
|
||||
*/
|
||||
public static class Edge {
|
||||
final int from;
|
||||
final int to;
|
||||
final long weight;
|
||||
|
||||
/**
|
||||
* Constructs a directed edge.
|
||||
*
|
||||
* @param from source vertex
|
||||
* @param to destination vertex
|
||||
* @param weight edge weight
|
||||
*/
|
||||
public Edge(int from, int to, long weight) {
|
||||
this.from = from;
|
||||
this.to = to;
|
||||
this.weight = weight;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the total weight of the Minimum Spanning Arborescence of a directed,
|
||||
* weighted graph from a given root.
|
||||
*
|
||||
* @param numVertices the number of vertices, labeled {@code 0..numVertices-1}
|
||||
* @param edges list of directed edges in the graph
|
||||
* @param root the root vertex
|
||||
* @return the total weight of the MSA. Returns -1 if not all vertices are reachable
|
||||
* from the root or if a valid arborescence cannot be formed.
|
||||
* @throws IllegalArgumentException if {@code numVertices <= 0} or {@code root} is out of range.
|
||||
*/
|
||||
public static long findMinimumSpanningArborescence(int numVertices, List<Edge> edges, int root) {
|
||||
if (root < 0 || root >= numVertices) {
|
||||
throw new IllegalArgumentException("Invalid number of vertices or root");
|
||||
}
|
||||
if (numVertices == 1) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return findMSARecursive(numVertices, edges, root);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursive helper method for finding MSA.
|
||||
*/
|
||||
private static long findMSARecursive(int n, List<Edge> edges, int root) {
|
||||
long[] minWeightEdge = new long[n];
|
||||
int[] predecessor = new int[n];
|
||||
Arrays.fill(minWeightEdge, Long.MAX_VALUE);
|
||||
Arrays.fill(predecessor, -1);
|
||||
|
||||
for (Edge edge : edges) {
|
||||
if (edge.to != root && edge.weight < minWeightEdge[edge.to]) {
|
||||
minWeightEdge[edge.to] = edge.weight;
|
||||
predecessor[edge.to] = edge.from;
|
||||
}
|
||||
}
|
||||
// Check if all non-root nodes are reachable
|
||||
for (int i = 0; i < n; i++) {
|
||||
if (i != root && minWeightEdge[i] == Long.MAX_VALUE) {
|
||||
return -1; // No spanning arborescence exists
|
||||
}
|
||||
}
|
||||
int[] cycleId = new int[n];
|
||||
Arrays.fill(cycleId, -1);
|
||||
boolean[] visited = new boolean[n];
|
||||
int cycleCount = 0;
|
||||
|
||||
for (int i = 0; i < n; i++) {
|
||||
if (visited[i]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
List<Integer> path = new ArrayList<>();
|
||||
int curr = i;
|
||||
|
||||
// Follow predecessor chain
|
||||
while (curr != -1 && !visited[curr]) {
|
||||
visited[curr] = true;
|
||||
path.add(curr);
|
||||
curr = predecessor[curr];
|
||||
}
|
||||
|
||||
// If we hit a visited node, check if it forms a cycle
|
||||
if (curr != -1) {
|
||||
boolean inCycle = false;
|
||||
for (int node : path) {
|
||||
if (node == curr) {
|
||||
inCycle = true;
|
||||
}
|
||||
if (inCycle) {
|
||||
cycleId[node] = cycleCount;
|
||||
}
|
||||
}
|
||||
if (inCycle) {
|
||||
cycleCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (cycleCount == 0) {
|
||||
long totalWeight = 0;
|
||||
for (int i = 0; i < n; i++) {
|
||||
if (i != root) {
|
||||
totalWeight += minWeightEdge[i];
|
||||
}
|
||||
}
|
||||
return totalWeight;
|
||||
}
|
||||
long cycleWeightSum = 0;
|
||||
for (int i = 0; i < n; i++) {
|
||||
if (cycleId[i] >= 0) {
|
||||
cycleWeightSum += minWeightEdge[i];
|
||||
}
|
||||
}
|
||||
|
||||
// Map old nodes to new nodes (cycles become supernodes)
|
||||
int[] newNodeMap = new int[n];
|
||||
int[] cycleToNewNode = new int[cycleCount];
|
||||
int newN = 0;
|
||||
|
||||
// Assign new node IDs to cycles first
|
||||
for (int i = 0; i < cycleCount; i++) {
|
||||
cycleToNewNode[i] = newN++;
|
||||
}
|
||||
|
||||
// Assign new node IDs to non-cycle nodes
|
||||
for (int i = 0; i < n; i++) {
|
||||
if (cycleId[i] == -1) {
|
||||
newNodeMap[i] = newN++;
|
||||
} else {
|
||||
newNodeMap[i] = cycleToNewNode[cycleId[i]];
|
||||
}
|
||||
}
|
||||
|
||||
int newRoot = newNodeMap[root];
|
||||
|
||||
// Build contracted graph
|
||||
List<Edge> newEdges = new ArrayList<>();
|
||||
for (Edge edge : edges) {
|
||||
int uCycleId = cycleId[edge.from];
|
||||
int vCycleId = cycleId[edge.to];
|
||||
|
||||
// Skip edges internal to a cycle
|
||||
if (uCycleId >= 0 && uCycleId == vCycleId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int newU = newNodeMap[edge.from];
|
||||
int newV = newNodeMap[edge.to];
|
||||
|
||||
long newWeight = edge.weight;
|
||||
// Adjust weight for edges entering a cycle
|
||||
if (vCycleId >= 0) {
|
||||
newWeight -= minWeightEdge[edge.to];
|
||||
}
|
||||
|
||||
if (newU != newV) {
|
||||
newEdges.add(new Edge(newU, newV, newWeight));
|
||||
}
|
||||
}
|
||||
return cycleWeightSum + findMSARecursive(newN, newEdges, newRoot);
|
||||
}
|
||||
}
|
||||
172
src/test/java/com/thealgorithms/graph/EdmondsTest.java
Normal file
172
src/test/java/com/thealgorithms/graph/EdmondsTest.java
Normal file
@@ -0,0 +1,172 @@
|
||||
package com.thealgorithms.graph;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class EdmondsTest {
|
||||
|
||||
@Test
|
||||
void testSimpleGraphNoCycle() {
|
||||
int n = 4;
|
||||
int root = 0;
|
||||
List<Edmonds.Edge> edges = new ArrayList<>();
|
||||
edges.add(new Edmonds.Edge(0, 1, 10));
|
||||
edges.add(new Edmonds.Edge(0, 2, 1));
|
||||
edges.add(new Edmonds.Edge(2, 1, 2));
|
||||
edges.add(new Edmonds.Edge(2, 3, 5));
|
||||
|
||||
// Expected arborescence edges: (0,2), (2,1), (2,3)
|
||||
// Weights: 1 + 2 + 5 = 8
|
||||
long result = Edmonds.findMinimumSpanningArborescence(n, edges, root);
|
||||
assertEquals(8, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGraphWithOneCycle() {
|
||||
int n = 4;
|
||||
int root = 0;
|
||||
List<Edmonds.Edge> edges = new ArrayList<>();
|
||||
edges.add(new Edmonds.Edge(0, 1, 10));
|
||||
edges.add(new Edmonds.Edge(2, 1, 4));
|
||||
edges.add(new Edmonds.Edge(1, 2, 5));
|
||||
edges.add(new Edmonds.Edge(2, 3, 6));
|
||||
|
||||
// Min edges: (2,1, w=4), (1,2, w=5), (2,3, w=6)
|
||||
// Cycle: 1 -> 2 -> 1, cost = 4 + 5 = 9
|
||||
// Contract {1,2} to C.
|
||||
// New edge (0,C) with w = 10 - min_in(1) = 10 - 4 = 6
|
||||
// New edge (C,3) with w = 6
|
||||
// Contracted MSA cost = 6 + 6 = 12
|
||||
// Total cost = cycle_cost + contracted_msa_cost = 9 + 12 = 21
|
||||
long result = Edmonds.findMinimumSpanningArborescence(n, edges, root);
|
||||
assertEquals(21, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testComplexGraphWithCycle() {
|
||||
int n = 6;
|
||||
int root = 0;
|
||||
List<Edmonds.Edge> edges = new ArrayList<>();
|
||||
edges.add(new Edmonds.Edge(0, 1, 10));
|
||||
edges.add(new Edmonds.Edge(0, 2, 20));
|
||||
edges.add(new Edmonds.Edge(1, 2, 5));
|
||||
edges.add(new Edmonds.Edge(2, 3, 10));
|
||||
edges.add(new Edmonds.Edge(3, 1, 3));
|
||||
edges.add(new Edmonds.Edge(1, 4, 7));
|
||||
edges.add(new Edmonds.Edge(3, 4, 2));
|
||||
edges.add(new Edmonds.Edge(4, 5, 5));
|
||||
|
||||
// Min edges: (3,1,3), (1,2,5), (2,3,10), (3,4,2), (4,5,5)
|
||||
// Cycle: 1->2->3->1, cost = 5+10+3=18
|
||||
// Contract {1,2,3} to C.
|
||||
// Edge (0,1,10) -> (0,C), w = 10-3=7
|
||||
// Edge (0,2,20) -> (0,C), w = 20-5=15. Min is 7.
|
||||
// Edge (1,4,7) -> (C,4,7)
|
||||
// Edge (3,4,2) -> (C,4,2). Min is 2.
|
||||
// Edge (4,5,5) -> (4,5,5)
|
||||
// Contracted MSA: (0,C,7), (C,4,2), (4,5,5). Cost = 7+2+5=14
|
||||
// Total cost = 18 + 14 = 32
|
||||
long result = Edmonds.findMinimumSpanningArborescence(n, edges, root);
|
||||
assertEquals(32, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testUnreachableNode() {
|
||||
int n = 4;
|
||||
int root = 0;
|
||||
List<Edmonds.Edge> edges = new ArrayList<>();
|
||||
edges.add(new Edmonds.Edge(0, 1, 10));
|
||||
edges.add(new Edmonds.Edge(2, 3, 5)); // Node 2 and 3 are unreachable from root 0
|
||||
|
||||
long result = Edmonds.findMinimumSpanningArborescence(n, edges, root);
|
||||
assertEquals(-1, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testNoEdgesToNonRootNodes() {
|
||||
int n = 3;
|
||||
int root = 0;
|
||||
List<Edmonds.Edge> edges = new ArrayList<>();
|
||||
edges.add(new Edmonds.Edge(0, 1, 10)); // Node 2 is unreachable
|
||||
|
||||
long result = Edmonds.findMinimumSpanningArborescence(n, edges, root);
|
||||
assertEquals(-1, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSingleNode() {
|
||||
int n = 1;
|
||||
int root = 0;
|
||||
List<Edmonds.Edge> edges = new ArrayList<>();
|
||||
|
||||
long result = Edmonds.findMinimumSpanningArborescence(n, edges, root);
|
||||
assertEquals(0, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testInvalidInputThrowsException() {
|
||||
List<Edmonds.Edge> edges = new ArrayList<>();
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () -> Edmonds.findMinimumSpanningArborescence(0, edges, 0));
|
||||
assertThrows(IllegalArgumentException.class, () -> Edmonds.findMinimumSpanningArborescence(5, edges, -1));
|
||||
assertThrows(IllegalArgumentException.class, () -> Edmonds.findMinimumSpanningArborescence(5, edges, 5));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCoverageForEdgeSelectionLogic() {
|
||||
int n = 3;
|
||||
int root = 0;
|
||||
List<Edmonds.Edge> edges = new ArrayList<>();
|
||||
|
||||
// This will cover the `edge.weight < minWeightEdge[edge.to]` being false.
|
||||
edges.add(new Edmonds.Edge(0, 1, 10));
|
||||
edges.add(new Edmonds.Edge(2, 1, 20));
|
||||
|
||||
// This will cover the `edge.to != root` being false.
|
||||
edges.add(new Edmonds.Edge(1, 0, 100));
|
||||
|
||||
// A regular edge to make the graph complete
|
||||
edges.add(new Edmonds.Edge(0, 2, 5));
|
||||
|
||||
// Expected MSA: (0,1, w=10) and (0,2, w=5). Total weight = 15.
|
||||
long result = Edmonds.findMinimumSpanningArborescence(n, edges, root);
|
||||
assertEquals(15, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCoverageForContractedSelfLoop() {
|
||||
int n = 4;
|
||||
int root = 0;
|
||||
List<Edmonds.Edge> edges = new ArrayList<>();
|
||||
|
||||
// Connect root to the cycle components
|
||||
edges.add(new Edmonds.Edge(0, 1, 20));
|
||||
|
||||
// Create a cycle 1 -> 2 -> 1
|
||||
edges.add(new Edmonds.Edge(1, 2, 5));
|
||||
edges.add(new Edmonds.Edge(2, 1, 5));
|
||||
|
||||
// This is the CRITICAL edge for coverage:
|
||||
// It connects two nodes (1 and 2) that are part of the SAME cycle.
|
||||
// After contracting cycle {1, 2} into a supernode C, this edge becomes (C, C),
|
||||
// which means newU == newV. This will trigger the `false` branch of the `if`.
|
||||
edges.add(new Edmonds.Edge(1, 1, 100)); // Also a self-loop on a cycle node.
|
||||
|
||||
// Add another edge to ensure node 3 is reachable
|
||||
edges.add(new Edmonds.Edge(1, 3, 10));
|
||||
|
||||
// Cycle {1,2} has cost 5+5=10.
|
||||
// Contract {1,2} to supernode C.
|
||||
// Edge (0,1,20) becomes (0,C, w=20-5=15).
|
||||
// Edge (1,3,10) becomes (C,3, w=10).
|
||||
// Edge (1,1,100) is discarded because newU == newV.
|
||||
// Cost of contracted graph = 15 + 10 = 25.
|
||||
// Total cost = cycle cost + contracted cost = 10 + 25 = 35.
|
||||
long result = Edmonds.findMinimumSpanningArborescence(n, edges, root);
|
||||
assertEquals(35, result);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user