Skip to content

Commit 657f433

Browse files
authored
Hopcroft karp Algorithm implementation and tests (#6465)
* Add Hopcroft-Karp algorithm and tests * Adding wikipedia url for Algorithm * fixing test issues * remove unused field flagged by PMD
1 parent 57c6b03 commit 657f433

File tree

2 files changed

+236
-0
lines changed

2 files changed

+236
-0
lines changed
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
package com.thealgorithms.graph;
2+
3+
import java.util.ArrayDeque;
4+
import java.util.Arrays;
5+
import java.util.List;
6+
import java.util.Queue;
7+
8+
/**
9+
* Hopcroft–Karp algorithm for maximum bipartite matching.
10+
*
11+
* Left part: vertices [0,nLeft-1], Right part: [0,nRight-1].
12+
* Adjacency list: for each left vertex u, list right vertices v it connects to.
13+
*
14+
* Time complexity: O(E * sqrt(V)).
15+
*
16+
* @see <a href="https://en.wikipedia.org/wiki/Hopcroft%E2%80%93Karp_algorithm">
17+
* Wikipedia: Hopcroft–Karp algorithm</a>
18+
* @author ptzecher
19+
*/
20+
public class HopcroftKarp {
21+
22+
private final int nLeft;
23+
private final List<List<Integer>> adj;
24+
25+
private final int[] pairU;
26+
private final int[] pairV;
27+
private final int[] dist;
28+
29+
public HopcroftKarp(int nLeft, int nRight, List<List<Integer>> adj) {
30+
this.nLeft = nLeft;
31+
this.adj = adj;
32+
33+
this.pairU = new int[nLeft];
34+
this.pairV = new int[nRight];
35+
this.dist = new int[nLeft];
36+
37+
Arrays.fill(pairU, -1);
38+
Arrays.fill(pairV, -1);
39+
}
40+
41+
/** Returns the size of the maximum matching. */
42+
public int maxMatching() {
43+
int matching = 0;
44+
while (bfs()) {
45+
for (int u = 0; u < nLeft; u++) {
46+
if (pairU[u] == -1 && dfs(u)) {
47+
matching++;
48+
}
49+
}
50+
}
51+
return matching;
52+
}
53+
54+
// BFS to build layers
55+
private boolean bfs() {
56+
Queue<Integer> queue = new ArrayDeque<>();
57+
Arrays.fill(dist, -1);
58+
59+
for (int u = 0; u < nLeft; u++) {
60+
if (pairU[u] == -1) {
61+
dist[u] = 0;
62+
queue.add(u);
63+
}
64+
}
65+
66+
boolean foundAugPath = false;
67+
while (!queue.isEmpty()) {
68+
int u = queue.poll();
69+
for (int v : adj.get(u)) {
70+
int matchedLeft = pairV[v];
71+
if (matchedLeft == -1) {
72+
foundAugPath = true;
73+
} else if (dist[matchedLeft] == -1) {
74+
dist[matchedLeft] = dist[u] + 1;
75+
queue.add(matchedLeft);
76+
}
77+
}
78+
}
79+
return foundAugPath;
80+
}
81+
82+
// DFS to find augmenting paths within the BFS layering
83+
private boolean dfs(int u) {
84+
for (int v : adj.get(u)) {
85+
int matchedLeft = pairV[v];
86+
if (matchedLeft == -1 || (dist[matchedLeft] == dist[u] + 1 && dfs(matchedLeft))) {
87+
pairU[u] = v;
88+
pairV[v] = u;
89+
return true;
90+
}
91+
}
92+
dist[u] = -1;
93+
return false;
94+
}
95+
96+
public int[] getLeftMatches() {
97+
return pairU.clone();
98+
}
99+
100+
public int[] getRightMatches() {
101+
return pairV.clone();
102+
}
103+
}
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
package com.thealgorithms.graph;
2+
3+
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
import static org.junit.jupiter.api.Assertions.assertFalse;
5+
6+
import java.util.ArrayList;
7+
import java.util.Arrays;
8+
import java.util.List;
9+
import org.junit.jupiter.api.DisplayName;
10+
import org.junit.jupiter.api.Test;
11+
12+
/**
13+
* Unit tests for Hopcroft–Karp algorithm.
14+
*
15+
* @author ptzecher
16+
*/
17+
class HopcroftKarpTest {
18+
19+
private static List<List<Integer>> adj(int nLeft) {
20+
List<List<Integer>> g = new ArrayList<>(nLeft);
21+
for (int i = 0; i < nLeft; i++) { // braces required by Checkstyle
22+
g.add(new ArrayList<>());
23+
}
24+
return g;
25+
}
26+
27+
@Test
28+
@DisplayName("Empty graph has matching 0")
29+
void emptyGraph() {
30+
List<List<Integer>> g = adj(3);
31+
HopcroftKarp hk = new HopcroftKarp(3, 4, g);
32+
assertEquals(0, hk.maxMatching());
33+
}
34+
35+
@Test
36+
@DisplayName("Single edge gives matching 1")
37+
void singleEdge() {
38+
List<List<Integer>> g = adj(1);
39+
g.get(0).add(0);
40+
HopcroftKarp hk = new HopcroftKarp(1, 1, g);
41+
assertEquals(1, hk.maxMatching());
42+
43+
int[] leftMatch = hk.getLeftMatches();
44+
int[] rightMatch = hk.getRightMatches();
45+
assertEquals(0, leftMatch[0]);
46+
assertEquals(0, rightMatch[0]);
47+
}
48+
49+
@Test
50+
@DisplayName("Disjoint edges match perfectly")
51+
void disjointEdges() {
52+
// L0-R0, L1-R1, L2-R2
53+
List<List<Integer>> g = adj(3);
54+
g.get(0).add(0);
55+
g.get(1).add(1);
56+
g.get(2).add(2);
57+
58+
HopcroftKarp hk = new HopcroftKarp(3, 3, g);
59+
assertEquals(3, hk.maxMatching());
60+
61+
int[] leftMatch = hk.getLeftMatches();
62+
int[] rightMatch = hk.getRightMatches();
63+
for (int i = 0; i < 3; i++) {
64+
assertEquals(i, leftMatch[i]);
65+
assertEquals(i, rightMatch[i]);
66+
}
67+
}
68+
69+
@Test
70+
@DisplayName("Complete bipartite K(3,4) matches min(3,4)=3")
71+
void completeK34() {
72+
int nLeft = 3;
73+
int nRight = 4; // split declarations
74+
List<List<Integer>> g = adj(nLeft);
75+
for (int u = 0; u < nLeft; u++) {
76+
g.get(u).addAll(Arrays.asList(0, 1, 2, 3));
77+
}
78+
HopcroftKarp hk = new HopcroftKarp(nLeft, nRight, g);
79+
assertEquals(3, hk.maxMatching());
80+
81+
// sanity: no two left vertices share the same matched right vertex
82+
int[] leftMatch = hk.getLeftMatches();
83+
boolean[] used = new boolean[nRight];
84+
for (int u = 0; u < nLeft; u++) {
85+
int v = leftMatch[u];
86+
if (v != -1) {
87+
assertFalse(used[v]);
88+
used[v] = true;
89+
}
90+
}
91+
}
92+
93+
@Test
94+
@DisplayName("Rectangular, sparse graph")
95+
void rectangularSparse() {
96+
// Left: 5, Right: 2
97+
// edges: L0-R0, L1-R1, L2-R0, L3-R1 (max matching = 2)
98+
List<List<Integer>> g = adj(5);
99+
g.get(0).add(0);
100+
g.get(1).add(1);
101+
g.get(2).add(0);
102+
g.get(3).add(1);
103+
// L4 isolated
104+
105+
HopcroftKarp hk = new HopcroftKarp(5, 2, g);
106+
assertEquals(2, hk.maxMatching());
107+
108+
int[] leftMatch = hk.getLeftMatches();
109+
int[] rightMatch = hk.getRightMatches();
110+
111+
// Check consistency: if leftMatch[u]=v then rightMatch[v]=u
112+
for (int u = 0; u < 5; u++) {
113+
int v = leftMatch[u];
114+
if (v != -1) {
115+
assertEquals(u, rightMatch[v]);
116+
}
117+
}
118+
}
119+
120+
@Test
121+
@DisplayName("Layering advantage case (short augmenting paths)")
122+
void layeringAdvantage() {
123+
// Left 4, Right 4
124+
List<List<Integer>> g = adj(4);
125+
g.get(0).addAll(Arrays.asList(0, 1));
126+
g.get(1).addAll(Arrays.asList(1, 2));
127+
g.get(2).addAll(Arrays.asList(2, 3));
128+
g.get(3).addAll(Arrays.asList(0, 3));
129+
130+
HopcroftKarp hk = new HopcroftKarp(4, 4, g);
131+
assertEquals(4, hk.maxMatching()); // perfect matching exists
132+
}
133+
}

0 commit comments

Comments
 (0)