Skip to content

Hopcroft karp Algorithm implementation and tests #6465

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Aug 14, 2025
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
103 changes: 103 additions & 0 deletions src/main/java/com/thealgorithms/graph/HopcroftKarp.java
Original file line number Diff line number Diff line change
@@ -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 <a href="https://en.wikipedia.org/wiki/Hopcroft%E2%80%93Karp_algorithm">
* Wikipedia: Hopcroft–Karp algorithm</a>
* @author ptzecher
*/
public class HopcroftKarp {

private final int nLeft;
private final List<List<Integer>> adj;

private final int[] pairU;
private final int[] pairV;
private final int[] dist;

public HopcroftKarp(int nLeft, int nRight, List<List<Integer>> 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<Integer> 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();
}
}
133 changes: 133 additions & 0 deletions src/test/java/com/thealgorithms/graph/HopcroftKarpTest.java
Original file line number Diff line number Diff line change
@@ -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<List<Integer>> adj(int nLeft) {
List<List<Integer>> 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<List<Integer>> g = adj(3);
HopcroftKarp hk = new HopcroftKarp(3, 4, g);
assertEquals(0, hk.maxMatching());
}

@Test
@DisplayName("Single edge gives matching 1")
void singleEdge() {
List<List<Integer>> 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<List<Integer>> 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<List<Integer>> 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<List<Integer>> 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<List<Integer>> 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
}
}