Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
201 changes: 201 additions & 0 deletions src/main/java/com/thealgorithms/graph/Edmonds.java
Original file line number Diff line number Diff line change
@@ -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 changes: 172 additions & 0 deletions src/test/java/com/thealgorithms/graph/EdmondsTest.java
Original file line number Diff line number Diff line change
@@ -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);
}
}