From 5cfd7d615789ac4f2142aabcd861395cb6502986 Mon Sep 17 00:00:00 2001 From: ptzecher Date: Wed, 13 Aug 2025 14:15:09 +0300 Subject: [PATCH 1/4] Add Hopcroft-Karp algorithm and tests --- .../dynamicprogramming/PartitionProblem.java | 11 +- .../com/thealgorithms/graph/HopcroftKarp.java | 99 ++++++++++++++ .../thealgorithms/graph/HopcroftKarpTest.java | 129 ++++++++++++++++++ 3 files changed, 235 insertions(+), 4 deletions(-) create mode 100644 src/main/java/com/thealgorithms/graph/HopcroftKarp.java create mode 100644 src/test/java/com/thealgorithms/graph/HopcroftKarpTest.java diff --git a/src/main/java/com/thealgorithms/dynamicprogramming/PartitionProblem.java b/src/main/java/com/thealgorithms/dynamicprogramming/PartitionProblem.java index 49c4a0a3a008..45fdd83080a7 100644 --- a/src/main/java/com/thealgorithms/dynamicprogramming/PartitionProblem.java +++ b/src/main/java/com/thealgorithms/dynamicprogramming/PartitionProblem.java @@ -1,3 +1,10 @@ + + +package com.thealgorithms.dynamicprogramming; + +import java.util.Arrays; + + /** * @author Md Asif Joardar * @@ -14,10 +21,6 @@ * The time complexity of the solution is O(n × sum) and requires O(n × sum) space */ -package com.thealgorithms.dynamicprogramming; - -import java.util.Arrays; - public final class PartitionProblem { private PartitionProblem() { } 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..513716d1e809 --- /dev/null +++ b/src/main/java/com/thealgorithms/graph/HopcroftKarp.java @@ -0,0 +1,99 @@ +package com.thealgorithms.graph; + +import java.util.*; + +/** + * @author Panteleimon Tzecheridis + * Implementation of the Hopcroft–Karp algorithm for finding the maximum matching in a bipartite graph. + * + * The bipartite graph is assumed to have: + * - Left part: vertices [0..nLeft-1] + * - Right part: vertices [0..nRight-1] + * + * Adjacency list format: For each left vertex, list the right vertices it is connected to. + * Example: + * adj[0] = [0, 1] // left vertex 0 connects to right vertices 0 and 1 + * + * Time complexity: O(E * sqrt(V)) + */ +public class HopcroftKarp { + + private int nLeft, nRight; + private List> adj; + private int[] pairU, pairV, dist; + + public HopcroftKarp(int nLeft, int nRight, List> adj) { + this.nLeft = nLeft; + this.nRight = nRight; + 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 u2 = pairV[v]; + if (u2 == -1) { + foundAugPath = true; + } else if (dist[u2] == -1) { + dist[u2] = dist[u] + 1; + queue.add(u2); + } + } + } + return foundAugPath; + } + + // DFS to find augmenting paths + private boolean dfs(int u) { + for (int v : adj.get(u)) { + int u2 = pairV[v]; + if (u2 == -1 || (dist[u2] == dist[u] + 1 && dfs(u2))) { + 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(); + } + +} \ No newline at end of file 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..d4b01e346e16 --- /dev/null +++ b/src/test/java/com/thealgorithms/graph/HopcroftKarpTest.java @@ -0,0 +1,129 @@ +package com.thealgorithms.graph; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class HopcroftKarpTest { + + private static List> adj(int nLeft) { + List> g = new ArrayList<>(nLeft); + for (int i = 0; i < nLeft; i++) 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[] L = hk.getLeftMatches(); + int[] R = hk.getRightMatches(); + assertEquals(0, L[0]); + assertEquals(0, R[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[] L = hk.getLeftMatches(); + int[] R = hk.getRightMatches(); + for (int i = 0; i < 3; i++) { + assertEquals(i, L[i]); + assertEquals(i, R[i]); + } + } + + @Test + @DisplayName("Complete bipartite K(3,4) matches min(3,4)=3") + void completeK34() { + int nLeft = 3, nRight = 4; + 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[] L = hk.getLeftMatches(); + boolean[] used = new boolean[nRight]; + for (int u = 0; u < nLeft; u++) { + int v = L[u]; + if (v != -1) { + assertFalse(used[v]); + used[v] = true; + } + } + } + + @Test + @DisplayName("Non-square, 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[] L = hk.getLeftMatches(); + int[] R = hk.getRightMatches(); + + // Check consistency: if L[u]=v then R[v]=u + for (int u = 0; u < 5; u++) { + int v = L[u]; + if (v != -1) { + assertEquals(u, R[v]); + } + } + } + + @Test + @DisplayName("Layering advantage case (chains of short augmenting paths)") + void layeringAdvantage() { + // Left 4, Right 4 + // Build a structure that benefits from BFS layering + // L0: R0, R1 + // L1: R1, R2 + // L2: R2, R3 + // L3: R0, R3 + 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 + } +} \ No newline at end of file From 329d47074afb1ee5741337851d1108f811f4d362 Mon Sep 17 00:00:00 2001 From: ptzecher Date: Wed, 13 Aug 2025 14:24:07 +0300 Subject: [PATCH 2/4] Adding wikipedia url for Algorithm --- src/main/java/com/thealgorithms/graph/HopcroftKarp.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/com/thealgorithms/graph/HopcroftKarp.java b/src/main/java/com/thealgorithms/graph/HopcroftKarp.java index 513716d1e809..f503016b988c 100644 --- a/src/main/java/com/thealgorithms/graph/HopcroftKarp.java +++ b/src/main/java/com/thealgorithms/graph/HopcroftKarp.java @@ -4,6 +4,7 @@ /** * @author Panteleimon Tzecheridis + * * Implementation of the Hopcroft–Karp algorithm for finding the maximum matching in a bipartite graph. * * The bipartite graph is assumed to have: @@ -15,6 +16,8 @@ * adj[0] = [0, 1] // left vertex 0 connects to right vertices 0 and 1 * * Time complexity: O(E * sqrt(V)) + * + * @see Wikipedia: Hopcroft–Karp algorithm */ public class HopcroftKarp { From bfdd3e9927324a6d9db6aa005e8cfa0d51e52a0f Mon Sep 17 00:00:00 2001 From: ptzecher Date: Wed, 13 Aug 2025 21:00:28 +0300 Subject: [PATCH 3/4] fixing test issues --- .../dynamicprogramming/PartitionProblem.java | 11 ++-- .../com/thealgorithms/graph/HopcroftKarp.java | 61 ++++++++++--------- .../thealgorithms/graph/HopcroftKarpTest.java | 58 ++++++++++-------- 3 files changed, 67 insertions(+), 63 deletions(-) diff --git a/src/main/java/com/thealgorithms/dynamicprogramming/PartitionProblem.java b/src/main/java/com/thealgorithms/dynamicprogramming/PartitionProblem.java index 45fdd83080a7..49c4a0a3a008 100644 --- a/src/main/java/com/thealgorithms/dynamicprogramming/PartitionProblem.java +++ b/src/main/java/com/thealgorithms/dynamicprogramming/PartitionProblem.java @@ -1,10 +1,3 @@ - - -package com.thealgorithms.dynamicprogramming; - -import java.util.Arrays; - - /** * @author Md Asif Joardar * @@ -21,6 +14,10 @@ * The time complexity of the solution is O(n × sum) and requires O(n × sum) space */ +package com.thealgorithms.dynamicprogramming; + +import java.util.Arrays; + public final class PartitionProblem { private PartitionProblem() { } diff --git a/src/main/java/com/thealgorithms/graph/HopcroftKarp.java b/src/main/java/com/thealgorithms/graph/HopcroftKarp.java index f503016b988c..7afaeb621c1a 100644 --- a/src/main/java/com/thealgorithms/graph/HopcroftKarp.java +++ b/src/main/java/com/thealgorithms/graph/HopcroftKarp.java @@ -1,44 +1,46 @@ package com.thealgorithms.graph; -import java.util.*; +import java.util.ArrayDeque; +import java.util.Arrays; +import java.util.List; +import java.util.Queue; /** - * @author Panteleimon Tzecheridis + * Hopcroft–Karp algorithm for maximum bipartite matching. * - * Implementation of the Hopcroft–Karp algorithm for finding the maximum matching in a bipartite graph. + * 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. * - * The bipartite graph is assumed to have: - * - Left part: vertices [0..nLeft-1] - * - Right part: vertices [0..nRight-1] + * Time complexity: O(E * sqrt(V)). * - * Adjacency list format: For each left vertex, list the right vertices it is connected to. - * Example: - * adj[0] = [0, 1] // left vertex 0 connects to right vertices 0 and 1 - * - * Time complexity: O(E * sqrt(V)) - * - * @see Wikipedia: Hopcroft–Karp algorithm + * @see + * Wikipedia: Hopcroft–Karp algorithm + * @author ptzecher */ public class HopcroftKarp { - private int nLeft, nRight; - private List> adj; - private int[] pairU, pairV, dist; + private final int nLeft; + private final int nRight; + 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.nRight = nRight; 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. - */ + /** Returns the size of the maximum matching. */ public int maxMatching() { int matching = 0; while (bfs()) { @@ -55,33 +57,35 @@ public int maxMatching() { 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 u2 = pairV[v]; - if (u2 == -1) { + int matchedLeft = pairV[v]; + if (matchedLeft == -1) { foundAugPath = true; - } else if (dist[u2] == -1) { - dist[u2] = dist[u] + 1; - queue.add(u2); + } else if (dist[matchedLeft] == -1) { + dist[matchedLeft] = dist[u] + 1; + queue.add(matchedLeft); } } } return foundAugPath; } - // DFS to find augmenting paths + // DFS to find augmenting paths within the BFS layering private boolean dfs(int u) { for (int v : adj.get(u)) { - int u2 = pairV[v]; - if (u2 == -1 || (dist[u2] == dist[u] + 1 && dfs(u2))) { + int matchedLeft = pairV[v]; + if (matchedLeft == -1 || (dist[matchedLeft] == dist[u] + 1 && dfs(matchedLeft))) { pairU[u] = v; pairV[v] = u; return true; @@ -98,5 +102,4 @@ public int[] getLeftMatches() { public int[] getRightMatches() { return pairV.clone(); } - -} \ No newline at end of file +} diff --git a/src/test/java/com/thealgorithms/graph/HopcroftKarpTest.java b/src/test/java/com/thealgorithms/graph/HopcroftKarpTest.java index d4b01e346e16..2a5b1cdac795 100644 --- a/src/test/java/com/thealgorithms/graph/HopcroftKarpTest.java +++ b/src/test/java/com/thealgorithms/graph/HopcroftKarpTest.java @@ -1,6 +1,7 @@ package com.thealgorithms.graph; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import java.util.ArrayList; import java.util.Arrays; @@ -8,11 +9,18 @@ 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++) g.add(new ArrayList<>()); + for (int i = 0; i < nLeft; i++) { // braces required by Checkstyle + g.add(new ArrayList<>()); + } return g; } @@ -32,10 +40,10 @@ void singleEdge() { HopcroftKarp hk = new HopcroftKarp(1, 1, g); assertEquals(1, hk.maxMatching()); - int[] L = hk.getLeftMatches(); - int[] R = hk.getRightMatches(); - assertEquals(0, L[0]); - assertEquals(0, R[0]); + int[] leftMatch = hk.getLeftMatches(); + int[] rightMatch = hk.getRightMatches(); + assertEquals(0, leftMatch[0]); + assertEquals(0, rightMatch[0]); } @Test @@ -50,18 +58,19 @@ void disjointEdges() { HopcroftKarp hk = new HopcroftKarp(3, 3, g); assertEquals(3, hk.maxMatching()); - int[] L = hk.getLeftMatches(); - int[] R = hk.getRightMatches(); + int[] leftMatch = hk.getLeftMatches(); + int[] rightMatch = hk.getRightMatches(); for (int i = 0; i < 3; i++) { - assertEquals(i, L[i]); - assertEquals(i, R[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, nRight = 4; + 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)); @@ -70,10 +79,10 @@ void completeK34() { assertEquals(3, hk.maxMatching()); // sanity: no two left vertices share the same matched right vertex - int[] L = hk.getLeftMatches(); + int[] leftMatch = hk.getLeftMatches(); boolean[] used = new boolean[nRight]; for (int u = 0; u < nLeft; u++) { - int v = L[u]; + int v = leftMatch[u]; if (v != -1) { assertFalse(used[v]); used[v] = true; @@ -82,10 +91,10 @@ void completeK34() { } @Test - @DisplayName("Non-square, sparse graph") + @DisplayName("Rectangular, sparse graph") void rectangularSparse() { // Left: 5, Right: 2 - // edges: L0-R0, L1-R1, L2-R0, L3-R1 (max matching = 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); @@ -96,27 +105,22 @@ void rectangularSparse() { HopcroftKarp hk = new HopcroftKarp(5, 2, g); assertEquals(2, hk.maxMatching()); - int[] L = hk.getLeftMatches(); - int[] R = hk.getRightMatches(); + int[] leftMatch = hk.getLeftMatches(); + int[] rightMatch = hk.getRightMatches(); - // Check consistency: if L[u]=v then R[v]=u + // Check consistency: if leftMatch[u]=v then rightMatch[v]=u for (int u = 0; u < 5; u++) { - int v = L[u]; + int v = leftMatch[u]; if (v != -1) { - assertEquals(u, R[v]); + assertEquals(u, rightMatch[v]); } } } @Test - @DisplayName("Layering advantage case (chains of short augmenting paths)") + @DisplayName("Layering advantage case (short augmenting paths)") void layeringAdvantage() { // Left 4, Right 4 - // Build a structure that benefits from BFS layering - // L0: R0, R1 - // L1: R1, R2 - // L2: R2, R3 - // L3: R0, R3 List> g = adj(4); g.get(0).addAll(Arrays.asList(0, 1)); g.get(1).addAll(Arrays.asList(1, 2)); @@ -126,4 +130,4 @@ void layeringAdvantage() { HopcroftKarp hk = new HopcroftKarp(4, 4, g); assertEquals(4, hk.maxMatching()); // perfect matching exists } -} \ No newline at end of file +} From be49e4b465c7581a77c54dcd39ad5d3deb97b6de Mon Sep 17 00:00:00 2001 From: ptzecher Date: Wed, 13 Aug 2025 21:40:54 +0300 Subject: [PATCH 4/4] remove unused field flagged by PMD --- src/main/java/com/thealgorithms/graph/HopcroftKarp.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main/java/com/thealgorithms/graph/HopcroftKarp.java b/src/main/java/com/thealgorithms/graph/HopcroftKarp.java index 7afaeb621c1a..76f7f3eaa3a7 100644 --- a/src/main/java/com/thealgorithms/graph/HopcroftKarp.java +++ b/src/main/java/com/thealgorithms/graph/HopcroftKarp.java @@ -8,7 +8,7 @@ /** * Hopcroft–Karp algorithm for maximum bipartite matching. * - * Left part: vertices [0..nLeft-1], Right part: [0..nRight-1]. + * 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)). @@ -20,7 +20,6 @@ public class HopcroftKarp { private final int nLeft; - private final int nRight; private final List> adj; private final int[] pairU; @@ -29,7 +28,6 @@ public class HopcroftKarp { public HopcroftKarp(int nLeft, int nRight, List> adj) { this.nLeft = nLeft; - this.nRight = nRight; this.adj = adj; this.pairU = new int[nLeft];