Skip to content

Commit b367916

Browse files
committed
fix(hopcroft-karp): Improve test coverage and fix edge cases
Added new unit tests for the Hopcroft-Karp algorithm to cover more scenarios and edge cases. This led to a fix for a bug where graphs with no possible matching were handled incorrectly. Also updated the README documentation.
1 parent c2d682a commit b367916

3 files changed

Lines changed: 132 additions & 10 deletions

File tree

README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ If you are interested, please contact us at zigrazor@gmail.com or contribute to
9191
- [Borůvka's Algorithm](#borůvkas-algorithm)
9292
- [Graph Slicing based on connectivity](#graph-slicing-based-on-connectivity)
9393
- [Ford-Fulkerson Algorithm](#ford-fulkerson-algorithm)
94+
- [Hopcroft-Karp Algorithm](#hopcroft-karp-algorithm)
9495
- [Kosaraju's Algorithm](#kosarajus-algorithm)
9596
- [Kahn's Algorithm](#kahns-algorithm)
9697
- [Partition Algorithm Explanation](#partition-algorithm-explanation)
@@ -461,6 +462,18 @@ This algorithm is used in garbage collection systems to decide which other objec
461462
[Ford-Fulkerson Algorithm](https://en.wikipedia.org/wiki/Ford%E2%80%93Fulkerson_algorithm) is a greedy algorithm for finding a maximum flow in a flow network.
462463
The idea behind the algorithm is as follows: as long as there is a path from the source (start node) to the sink (end node), with available capacity on all edges in the path, we send flow along one of the paths. Then we find another path, and so on. A path with available capacity is called an augmenting path.
463464

465+
### Hopcroft-Karp Algorithm
466+
467+
[Hopcroft-Karp Algorithm](https://en.wikipedia.org/wiki/Hopcroft%E2%80%93Karp_algorithm) is an algorithm that finds the maximum cardinality matching in a bipartite graph in O(E√V) time. It repeatedly finds augmenting paths of shortest length using BFS, then uses DFS to find a maximal set of vertex-disjoint augmenting paths of that length.
468+
469+
The algorithm operates in phases:
470+
471+
1. **BFS Phase**: Find the shortest augmenting path length from unmatched left vertices to unmatched right vertices. If no augmenting path exists, the current matching is maximum.
472+
2. **DFS Phase**: Use DFS to find a maximal set of vertex-disjoint augmenting paths of the shortest length found in the BFS phase.
473+
3. **Augmentation**: Add all found augmenting paths to the matching simultaneously.
474+
475+
This process repeats until no more augmenting paths exist. Each iteration increases the matching size by at least one, and there are at most O(√V) iterations, giving the overall O(E√V) time complexity.
476+
464477
### Kosaraju's Algorithm
465478
[Kosaraju's Algorithm](https://en.wikipedia.org/wiki/Kosaraju%27s_algorithm) is a linear time algorithm to find the strongly connected components of a directed graph. It is based on the idea that if one is able to reach a vertex v starting from vertex u, then one should be able to reach vertex u starting from vertex v and if such is the case, one can say that vertices u and v are strongly connected - they are in a strongly connected sub-graph. Following is an example:
466479

include/CXXGraph/Graph/Algorithm/HopcroftKarp_impl.hpp

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,14 @@ namespace CXXGraph {
3434
result.errorMessage = "";
3535
result.maxMatching = 0;
3636

37+
// If the graph is empty or has no edges, the maximum matching is 0.
38+
// The graph is still bipartite.
39+
if (this->getNodeSet().empty() || this->getEdgeSet().empty()) {
40+
result.success = true;
41+
result.maxMatching = 0;
42+
return result;
43+
}
44+
3745
auto nodeSet = getNodeSet();
3846

3947
// need at least 2 nodes for matching
@@ -116,7 +124,6 @@ namespace CXXGraph {
116124
return result;
117125
}
118126

119-
// main Hopcroft-Karp algorithm
120127
std::unordered_map<shared<const Node<T>>, shared<const Node<T>>, nodeHash<T>> match;
121128
std::unordered_map<shared<const Node<T>>, int, nodeHash<T>> dist;
122129
int matchingSize = 0;

test/HopcroftKarpTest.cpp

Lines changed: 111 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -84,29 +84,27 @@ TEST(HopcroftKarpTest, test_single_edge) {
8484
}
8585

8686
TEST(HopcroftKarpTest, test_empty_graph) {
87-
// empty graph should fail due to insufficient nodes
87+
// empty graph should be considered bipartite with 0 matching
8888
CXXGraph::T_EdgeSet<int> edgeSet;
8989

9090
CXXGraph::Graph<int> graph(edgeSet);
9191
CXXGraph::HopcroftKarpResult result = graph.hopcroftKarp();
9292

93-
ASSERT_FALSE(result.success);
93+
ASSERT_TRUE(result.success);
9494
ASSERT_EQ(result.maxMatching, 0);
95-
ASSERT_FALSE(result.errorMessage.empty());
96-
ASSERT_EQ(result.errorMessage, "Graph must have at least 2 nodes for bipartite matching.");
95+
ASSERT_TRUE(result.errorMessage.empty());
9796
}
9897

9998
TEST(HopcroftKarpTest, test_single_node_graph) {
100-
// single node should fail due to insufficient nodes
99+
// single node graph is also bipartite with 0 matching
101100
CXXGraph::Graph<int> graph;
102101
graph.addNode(std::make_shared<CXXGraph::Node<int>>("1", 1));
103-
102+
104103
CXXGraph::HopcroftKarpResult result = graph.hopcroftKarp();
105104

106-
ASSERT_FALSE(result.success);
105+
ASSERT_TRUE(result.success);
107106
ASSERT_EQ(result.maxMatching, 0);
108-
ASSERT_FALSE(result.errorMessage.empty());
109-
ASSERT_EQ(result.errorMessage, "Graph must have at least 2 nodes for bipartite matching.");
107+
ASSERT_TRUE(result.errorMessage.empty());
110108
}
111109

112110
TEST(HopcroftKarpTest, test_non_bipartite_graph) {
@@ -223,3 +221,107 @@ TEST(HopcroftKarpTest, test_incremental_matching) {
223221
std::set<std::string> possibleMatches = { "v1", "v2", "v3" };
224222
ASSERT_TRUE(possibleMatches.count(result.matching[0].second) > 0);
225223
}
224+
225+
TEST(HopcroftKarpTest, test_disconnected_graph) {
226+
// tests matching on a graph with multiple disconnected components
227+
// component 1: a single edge (matching size 1)
228+
CXXGraph::Node<int> u1("u1", 1), v1("v1", 2);
229+
CXXGraph::UndirectedEdge<int> edge1(1, u1, v1);
230+
231+
// component 2: a 4-cycle (matching size 2)
232+
CXXGraph::Node<int> u2("u2", 3), v2("v2", 4);
233+
CXXGraph::Node<int> u3("u3", 5), v3("v3", 6);
234+
CXXGraph::UndirectedEdge<int> edge2(2, u2, v2);
235+
CXXGraph::UndirectedEdge<int> edge3(3, v2, u3);
236+
CXXGraph::UndirectedEdge<int> edge4(4, u3, v3);
237+
CXXGraph::UndirectedEdge<int> edge5(5, v3, u2);
238+
239+
CXXGraph::T_EdgeSet<int> edgeSet;
240+
edgeSet.insert(make_shared<CXXGraph::UndirectedEdge<int>>(edge1));
241+
edgeSet.insert(make_shared<CXXGraph::UndirectedEdge<int>>(edge2));
242+
edgeSet.insert(make_shared<CXXGraph::UndirectedEdge<int>>(edge3));
243+
edgeSet.insert(make_shared<CXXGraph::UndirectedEdge<int>>(edge4));
244+
edgeSet.insert(make_shared<CXXGraph::UndirectedEdge<int>>(edge5));
245+
246+
CXXGraph::Graph<int> graph(edgeSet);
247+
CXXGraph::HopcroftKarpResult result = graph.hopcroftKarp();
248+
249+
// expected matching is sum of matchings from each component (1 + 2 = 3)
250+
ASSERT_TRUE(result.success);
251+
ASSERT_EQ(result.maxMatching, 3);
252+
ASSERT_EQ(result.matching.size(), 3);
253+
ASSERT_TRUE(result.errorMessage.empty());
254+
}
255+
256+
TEST(HopcroftKarpTest, test_graph_with_isolated_vertices) {
257+
// tests matching on a graph with isolated vertices
258+
CXXGraph::Node<int> u1("u1", 1), v1("v1", 2);
259+
CXXGraph::UndirectedEdge<int> edge1(1, u1, v1);
260+
261+
// isolated nodes
262+
CXXGraph::Node<int> iso1("iso1", 3), iso2("iso2", 4);
263+
264+
CXXGraph::T_EdgeSet<int> edgeSet;
265+
edgeSet.insert(make_shared<CXXGraph::UndirectedEdge<int>>(edge1));
266+
267+
CXXGraph::Graph<int> graph(edgeSet);
268+
graph.addNode(make_shared<CXXGraph::Node<int>>(iso1));
269+
graph.addNode(make_shared<CXXGraph::Node<int>>(iso2));
270+
271+
CXXGraph::HopcroftKarpResult result = graph.hopcroftKarp();
272+
273+
// isolated vertices should not affect the matching
274+
ASSERT_TRUE(result.success);
275+
ASSERT_EQ(result.maxMatching, 1);
276+
ASSERT_EQ(result.matching.size(), 1);
277+
ASSERT_TRUE(result.errorMessage.empty());
278+
ASSERT_EQ(result.matching[0].first, "u1");
279+
ASSERT_EQ(result.matching[0].second, "v1");
280+
}
281+
282+
TEST(HopcroftKarpTest, test_no_matching_possible) {
283+
// tests a bipartite graph with no edges between partitions
284+
CXXGraph::Node<int> u1("u1", 1), u2("u2", 2);
285+
CXXGraph::Node<int> v1("v1", 3), v2("v2", 4);
286+
287+
// no edges connect U and V partitions
288+
CXXGraph::T_EdgeSet<int> edgeSet;
289+
290+
CXXGraph::Graph<int> graph(edgeSet);
291+
graph.addNode(make_shared<CXXGraph::Node<int>>(u1));
292+
graph.addNode(make_shared<CXXGraph::Node<int>>(u2));
293+
graph.addNode(make_shared<CXXGraph::Node<int>>(v1));
294+
graph.addNode(make_shared<CXXGraph::Node<int>>(v2));
295+
296+
CXXGraph::HopcroftKarpResult result = graph.hopcroftKarp();
297+
298+
// graph is bipartite, but matching should be 0
299+
ASSERT_TRUE(result.success);
300+
ASSERT_EQ(result.maxMatching, 0);
301+
ASSERT_EQ(result.matching.size(), 0);
302+
ASSERT_TRUE(result.errorMessage.empty());
303+
}
304+
305+
TEST(HopcroftKarpTest, test_unbalanced_bipartite_graph) {
306+
// tests an unbalanced bipartite graph (U=3, V=2)
307+
CXXGraph::Node<int> u1("u1", 1), u2("u2", 2), u3("u3", 3);
308+
CXXGraph::Node<int> v1("v1", 4), v2("v2", 5);
309+
310+
CXXGraph::UndirectedEdge<int> edge1(1, u1, v1);
311+
CXXGraph::UndirectedEdge<int> edge2(2, u2, v1);
312+
CXXGraph::UndirectedEdge<int> edge3(3, u3, v2);
313+
314+
CXXGraph::T_EdgeSet<int> edgeSet;
315+
edgeSet.insert(make_shared<CXXGraph::UndirectedEdge<int>>(edge1));
316+
edgeSet.insert(make_shared<CXXGraph::UndirectedEdge<int>>(edge2));
317+
edgeSet.insert(make_shared<CXXGraph::UndirectedEdge<int>>(edge3));
318+
319+
CXXGraph::Graph<int> graph(edgeSet);
320+
CXXGraph::HopcroftKarpResult result = graph.hopcroftKarp();
321+
322+
// max matching cannot exceed the size of the smaller partition (V)
323+
ASSERT_TRUE(result.success);
324+
ASSERT_EQ(result.maxMatching, 2);
325+
ASSERT_EQ(result.matching.size(), 2);
326+
ASSERT_TRUE(result.errorMessage.empty());
327+
}

0 commit comments

Comments
 (0)