From 901a1cb342700ce50096191703bd8201d3b00be0 Mon Sep 17 00:00:00 2001
From: Microindole <1513979779@qq.com>
Date: Mon, 13 Oct 2025 19:08:51 +0800
Subject: [PATCH 1/2] feat(graph): Add Edmonds's algorithm for minimum spanning
arborescence
---
.../java/com/thealgorithms/graph/Edmonds.java | 201 ++++++++++++++++++
.../com/thealgorithms/graph/EdmondsTest.java | 121 +++++++++++
2 files changed, 322 insertions(+)
create mode 100644 src/main/java/com/thealgorithms/graph/Edmonds.java
create mode 100644 src/test/java/com/thealgorithms/graph/EdmondsTest.java
diff --git a/src/main/java/com/thealgorithms/graph/Edmonds.java b/src/main/java/com/thealgorithms/graph/Edmonds.java
new file mode 100644
index 000000000000..92ee74d92dd1
--- /dev/null
+++ b/src/main/java/com/thealgorithms/graph/Edmonds.java
@@ -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).
+ *
+ *
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.
+ *
+ *
The algorithm works recursively:
+ *
+ * - For each vertex other than the root, select the incoming edge with the minimum weight.
+ * - If the selected edges form a spanning arborescence, it is the MSA.
+ * - If cycles are formed, contract each cycle into a new "supernode".
+ * - Modify the weights of edges entering the new supernode.
+ * - Recursively call the algorithm on the contracted graph.
+ * - The final cost is the sum of the initial edge selections and the result of the recursive call.
+ *
+ *
+ * Time Complexity: O(E * V) where E is the number of edges and V is the number of vertices.
+ *
+ *
References:
+ *
+ */
+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 edges, int root) {
+ if (numVertices <= 0 || 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 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 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 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);
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/com/thealgorithms/graph/EdmondsTest.java b/src/test/java/com/thealgorithms/graph/EdmondsTest.java
new file mode 100644
index 000000000000..99484ba04d4a
--- /dev/null
+++ b/src/test/java/com/thealgorithms/graph/EdmondsTest.java
@@ -0,0 +1,121 @@
+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 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 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 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 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 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 edges = new ArrayList<>();
+
+ long result = Edmonds.findMinimumSpanningArborescence(n, edges, root);
+ assertEquals(0, result);
+ }
+
+ @Test
+ void testInvalidInputThrowsException() {
+ List 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));
+ }
+}
\ No newline at end of file
From dbc80bb2f8014b6de59e7066a581a367bb04d240 Mon Sep 17 00:00:00 2001
From: Microindole <1513979779@qq.com>
Date: Mon, 13 Oct 2025 19:47:15 +0800
Subject: [PATCH 2/2] test: Add test cases to achieve 100% coverage
---
.../java/com/thealgorithms/graph/Edmonds.java | 4 +-
.../com/thealgorithms/graph/EdmondsTest.java | 65 +++++++++++++++++--
2 files changed, 60 insertions(+), 9 deletions(-)
diff --git a/src/main/java/com/thealgorithms/graph/Edmonds.java b/src/main/java/com/thealgorithms/graph/Edmonds.java
index 92ee74d92dd1..4ddb8f9ff544 100644
--- a/src/main/java/com/thealgorithms/graph/Edmonds.java
+++ b/src/main/java/com/thealgorithms/graph/Edmonds.java
@@ -68,7 +68,7 @@ public Edge(int from, int to, long weight) {
* @throws IllegalArgumentException if {@code numVertices <= 0} or {@code root} is out of range.
*/
public static long findMinimumSpanningArborescence(int numVertices, List edges, int root) {
- if (numVertices <= 0 || root < 0 || root >= numVertices) {
+ if (root < 0 || root >= numVertices) {
throw new IllegalArgumentException("Invalid number of vertices or root");
}
if (numVertices == 1) {
@@ -198,4 +198,4 @@ private static long findMSARecursive(int n, List edges, int root) {
}
return cycleWeightSum + findMSARecursive(newN, newEdges, newRoot);
}
-}
\ No newline at end of file
+}
diff --git a/src/test/java/com/thealgorithms/graph/EdmondsTest.java b/src/test/java/com/thealgorithms/graph/EdmondsTest.java
index 99484ba04d4a..ab5740c94217 100644
--- a/src/test/java/com/thealgorithms/graph/EdmondsTest.java
+++ b/src/test/java/com/thealgorithms/graph/EdmondsTest.java
@@ -111,11 +111,62 @@ void testSingleNode() {
void testInvalidInputThrowsException() {
List 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));
+ assertThrows(IllegalArgumentException.class, () -> Edmonds.findMinimumSpanningArborescence(0, edges, 0));
+ assertThrows(IllegalArgumentException.class, () -> Edmonds.findMinimumSpanningArborescence(5, edges, -1));
+ assertThrows(IllegalArgumentException.class, () -> Edmonds.findMinimumSpanningArborescence(5, edges, 5));
}
-}
\ No newline at end of file
+
+ @Test
+ void testCoverageForEdgeSelectionLogic() {
+ int n = 3;
+ int root = 0;
+ List 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 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);
+ }
+}