diff --git a/src/main/java/com/thealgorithms/graph/HopcroftKarp.java b/src/main/java/com/thealgorithms/graph/HopcroftKarp.java new file mode 100644 index 000000000000..76f7f3eaa3a7 --- /dev/null +++ b/src/main/java/com/thealgorithms/graph/HopcroftKarp.java @@ -0,0 +1,103 @@ +package com.thealgorithms.graph; + +import java.util.ArrayDeque; +import java.util.Arrays; +import java.util.List; +import java.util.Queue; + +/** + * Hopcroft–Karp algorithm for maximum bipartite matching. + * + * Left part: vertices [0,nLeft-1], Right part: [0,nRight-1]. + * Adjacency list: for each left vertex u, list right vertices v it connects to. + * + * Time complexity: O(E * sqrt(V)). + * + * @see + * Wikipedia: Hopcroft–Karp algorithm + * @author ptzecher + */ +public class HopcroftKarp { + + private final int nLeft; + private final List> adj; + + private final int[] pairU; + private final int[] pairV; + private final int[] dist; + + public HopcroftKarp(int nLeft, int nRight, List> adj) { + this.nLeft = nLeft; + this.adj = adj; + + this.pairU = new int[nLeft]; + this.pairV = new int[nRight]; + this.dist = new int[nLeft]; + + Arrays.fill(pairU, -1); + Arrays.fill(pairV, -1); + } + + /** Returns the size of the maximum matching. */ + public int maxMatching() { + int matching = 0; + while (bfs()) { + for (int u = 0; u < nLeft; u++) { + if (pairU[u] == -1 && dfs(u)) { + matching++; + } + } + } + return matching; + } + + // BFS to build layers + private boolean bfs() { + Queue queue = new ArrayDeque<>(); + Arrays.fill(dist, -1); + + for (int u = 0; u < nLeft; u++) { + if (pairU[u] == -1) { + dist[u] = 0; + queue.add(u); + } + } + + boolean foundAugPath = false; + while (!queue.isEmpty()) { + int u = queue.poll(); + for (int v : adj.get(u)) { + int matchedLeft = pairV[v]; + if (matchedLeft == -1) { + foundAugPath = true; + } else if (dist[matchedLeft] == -1) { + dist[matchedLeft] = dist[u] + 1; + queue.add(matchedLeft); + } + } + } + return foundAugPath; + } + + // DFS to find augmenting paths within the BFS layering + private boolean dfs(int u) { + for (int v : adj.get(u)) { + int matchedLeft = pairV[v]; + if (matchedLeft == -1 || (dist[matchedLeft] == dist[u] + 1 && dfs(matchedLeft))) { + pairU[u] = v; + pairV[v] = u; + return true; + } + } + dist[u] = -1; + return false; + } + + public int[] getLeftMatches() { + return pairU.clone(); + } + + public int[] getRightMatches() { + return pairV.clone(); + } +} diff --git a/src/test/java/com/thealgorithms/graph/HopcroftKarpTest.java b/src/test/java/com/thealgorithms/graph/HopcroftKarpTest.java new file mode 100644 index 000000000000..2a5b1cdac795 --- /dev/null +++ b/src/test/java/com/thealgorithms/graph/HopcroftKarpTest.java @@ -0,0 +1,133 @@ +package com.thealgorithms.graph; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +/** + * Unit tests for Hopcroft–Karp algorithm. + * + * @author ptzecher + */ +class HopcroftKarpTest { + + private static List> adj(int nLeft) { + List> g = new ArrayList<>(nLeft); + for (int i = 0; i < nLeft; i++) { // braces required by Checkstyle + g.add(new ArrayList<>()); + } + return g; + } + + @Test + @DisplayName("Empty graph has matching 0") + void emptyGraph() { + List> g = adj(3); + HopcroftKarp hk = new HopcroftKarp(3, 4, g); + assertEquals(0, hk.maxMatching()); + } + + @Test + @DisplayName("Single edge gives matching 1") + void singleEdge() { + List> g = adj(1); + g.get(0).add(0); + HopcroftKarp hk = new HopcroftKarp(1, 1, g); + assertEquals(1, hk.maxMatching()); + + int[] leftMatch = hk.getLeftMatches(); + int[] rightMatch = hk.getRightMatches(); + assertEquals(0, leftMatch[0]); + assertEquals(0, rightMatch[0]); + } + + @Test + @DisplayName("Disjoint edges match perfectly") + void disjointEdges() { + // L0-R0, L1-R1, L2-R2 + List> g = adj(3); + g.get(0).add(0); + g.get(1).add(1); + g.get(2).add(2); + + HopcroftKarp hk = new HopcroftKarp(3, 3, g); + assertEquals(3, hk.maxMatching()); + + int[] leftMatch = hk.getLeftMatches(); + int[] rightMatch = hk.getRightMatches(); + for (int i = 0; i < 3; i++) { + assertEquals(i, leftMatch[i]); + assertEquals(i, rightMatch[i]); + } + } + + @Test + @DisplayName("Complete bipartite K(3,4) matches min(3,4)=3") + void completeK34() { + int nLeft = 3; + int nRight = 4; // split declarations + List> g = adj(nLeft); + for (int u = 0; u < nLeft; u++) { + g.get(u).addAll(Arrays.asList(0, 1, 2, 3)); + } + HopcroftKarp hk = new HopcroftKarp(nLeft, nRight, g); + assertEquals(3, hk.maxMatching()); + + // sanity: no two left vertices share the same matched right vertex + int[] leftMatch = hk.getLeftMatches(); + boolean[] used = new boolean[nRight]; + for (int u = 0; u < nLeft; u++) { + int v = leftMatch[u]; + if (v != -1) { + assertFalse(used[v]); + used[v] = true; + } + } + } + + @Test + @DisplayName("Rectangular, sparse graph") + void rectangularSparse() { + // Left: 5, Right: 2 + // edges: L0-R0, L1-R1, L2-R0, L3-R1 (max matching = 2) + List> g = adj(5); + g.get(0).add(0); + g.get(1).add(1); + g.get(2).add(0); + g.get(3).add(1); + // L4 isolated + + HopcroftKarp hk = new HopcroftKarp(5, 2, g); + assertEquals(2, hk.maxMatching()); + + int[] leftMatch = hk.getLeftMatches(); + int[] rightMatch = hk.getRightMatches(); + + // Check consistency: if leftMatch[u]=v then rightMatch[v]=u + for (int u = 0; u < 5; u++) { + int v = leftMatch[u]; + if (v != -1) { + assertEquals(u, rightMatch[v]); + } + } + } + + @Test + @DisplayName("Layering advantage case (short augmenting paths)") + void layeringAdvantage() { + // Left 4, Right 4 + List> g = adj(4); + g.get(0).addAll(Arrays.asList(0, 1)); + g.get(1).addAll(Arrays.asList(1, 2)); + g.get(2).addAll(Arrays.asList(2, 3)); + g.get(3).addAll(Arrays.asList(0, 3)); + + HopcroftKarp hk = new HopcroftKarp(4, 4, g); + assertEquals(4, hk.maxMatching()); // perfect matching exists + } +}