Skip to content

Commit c2d682a

Browse files
committed
feat: implement Hopcroft-Karp algorithm
1 parent ac193ea commit c2d682a

6 files changed

Lines changed: 472 additions & 0 deletions

File tree

Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
/***********************************************************/
2+
/*** ______ ____ ______ _ ***/
3+
/*** / ___\ \/ /\ \/ / ___|_ __ __ _ _ __ | |__ ***/
4+
/*** | | \ / \ / | _| '__/ _` | '_ \| '_ \ ***/
5+
/*** | |___ / \ / \ |_| | | | (_| | |_) | | | | ***/
6+
/*** \____/_/\_\/_/\_\____|_| \__,_| .__/|_| |_| ***/
7+
/*** |_| ***/
8+
/***********************************************************/
9+
/*** Header-Only C++ Library for Graph ***/
10+
/*** Representation and Algorithms ***/
11+
/***********************************************************/
12+
/*** Author: ZigRazor ***/
13+
/*** E-Mail: zigrazor@gmail.com ***/
14+
/***********************************************************/
15+
/*** Collaboration: ----------- ***/
16+
/***********************************************************/
17+
/*** License: MPL v2.0 ***/
18+
/***********************************************************/
19+
20+
#ifndef __CXXGRAPH_HOPCROFTKARP_IMPL_H__
21+
#define __CXXGRAPH_HOPCROFTKARP_IMPL_H__
22+
23+
#pragma once
24+
25+
#include "CXXGraph/Graph/Graph_decl.h"
26+
#include <queue>
27+
#include <iostream>
28+
29+
namespace CXXGraph {
30+
template <typename T>
31+
const HopcroftKarpResult_struct Graph<T>::hopcroftKarp() const {
32+
HopcroftKarpResult_struct result;
33+
result.success = false;
34+
result.errorMessage = "";
35+
result.maxMatching = 0;
36+
37+
auto nodeSet = getNodeSet();
38+
39+
// need at least 2 nodes for matching
40+
if(nodeSet.size() < 2) {
41+
result.errorMessage = "Graph must have at least 2 nodes for bipartite matching.";
42+
return result;
43+
}
44+
45+
// algorithm requires undirected graph
46+
if(isDirectedGraph()) {
47+
result.errorMessage = ERR_DIR_GRAPH;
48+
return result;
49+
}
50+
51+
// convert nodes to vector for easier iteration
52+
std::vector<shared<const Node<T>>> nodes(nodeSet.begin(), nodeSet.end());
53+
54+
// verify graph is bipartite using BFS 2-coloring
55+
std::unordered_map<std::string, int> color; // use node IDs to avoid pointer issues
56+
bool isBipartite = true;
57+
58+
// process each connected component separately
59+
for(const auto& startNode : nodes) {
60+
if(color.find(startNode->getUserId()) == color.end() && isBipartite) {
61+
std::queue<shared<const Node<T>>> queue;
62+
queue.push(startNode);
63+
color[startNode->getUserId()] = 0;
64+
65+
while(!queue.empty() && isBipartite) {
66+
auto current = queue.front();
67+
queue.pop();
68+
69+
auto neighbors = this->inOutNeighbors(current);
70+
for(const auto& neighbor : neighbors) {
71+
if(color.find(neighbor->getUserId()) == color.end()) {
72+
// assign opposite color
73+
color[neighbor->getUserId()] = 1 - color[current->getUserId()];
74+
queue.push(neighbor);
75+
}
76+
else if(color[neighbor->getUserId()] == color[current->getUserId()]) {
77+
// same color means odd cycle - not bipartite
78+
isBipartite = false;
79+
break;
80+
}
81+
}
82+
}
83+
}
84+
}
85+
86+
if(!isBipartite) {
87+
result.errorMessage = "Graph is not bipartite.";
88+
return result;
89+
}
90+
91+
// partition nodes into left (U) and right (V) sets
92+
std::vector<shared<const Node<T>>> U, V;
93+
for(const auto& node : nodes) {
94+
auto colorIt = color.find(node->getUserId());
95+
if(colorIt != color.end()) {
96+
if(colorIt->second == 0) {
97+
U.push_back(node);
98+
}
99+
else {
100+
V.push_back(node);
101+
}
102+
}
103+
}
104+
105+
// assign isolated vertices to left partition
106+
for(const auto& node : nodes) {
107+
if(color.find(node->getUserId()) == color.end()) {
108+
U.push_back(node);
109+
}
110+
}
111+
112+
// no matching possible if either partition is empty
113+
if(U.empty() || V.empty()) {
114+
result.success = true;
115+
result.maxMatching = 0;
116+
return result;
117+
}
118+
119+
// main Hopcroft-Karp algorithm
120+
std::unordered_map<shared<const Node<T>>, shared<const Node<T>>, nodeHash<T>> match;
121+
std::unordered_map<shared<const Node<T>>, int, nodeHash<T>> dist;
122+
int matchingSize = 0;
123+
int path_len = -1;
124+
125+
// BFS: builds layered graph and finds shortest augmenting path length
126+
auto bfs = [&]() {
127+
dist.clear();
128+
path_len = -1;
129+
std::queue<shared<const Node<T>>> q;
130+
131+
// start from unmatched nodes in left partition
132+
for (const auto& u : U) {
133+
if (match.find(u) == match.end()) {
134+
dist[u] = 0;
135+
q.push(u);
136+
}
137+
}
138+
139+
while (!q.empty()) {
140+
auto u = q.front();
141+
q.pop();
142+
143+
// stop if shortest path already found
144+
if (path_len != -1 && dist.at(u) >= path_len) continue;
145+
146+
auto neighbors = this->inOutNeighbors(u);
147+
for (const auto& v : neighbors) {
148+
if (match.find(v) == match.end()) {
149+
// found unmatched node - augmenting path exists
150+
if (path_len == -1) path_len = dist.at(u) + 1;
151+
} else {
152+
// add matched partner to next layer
153+
auto matched_u = match.at(v);
154+
if (dist.find(matched_u) == dist.end()) {
155+
dist[matched_u] = dist.at(u) + 1;
156+
q.push(matched_u);
157+
}
158+
}
159+
}
160+
}
161+
return path_len != -1;
162+
};
163+
164+
// DFS: finds vertex-disjoint augmenting paths
165+
auto dfs = [&](auto&& self, shared<const Node<T>> u) -> bool {
166+
if (dist.find(u) == dist.end()) return false;
167+
168+
auto neighbors = this->inOutNeighbors(u);
169+
for (const auto& v : neighbors) {
170+
if (match.find(v) == match.end()) {
171+
// found unmatched node at correct distance
172+
if (dist.at(u) + 1 == path_len) {
173+
match[v] = u;
174+
match[u] = v;
175+
return true;
176+
}
177+
} else {
178+
// try extending through matched partner
179+
auto matched_u = match.at(v);
180+
if (dist.count(matched_u) && dist.at(matched_u) == dist.at(u) + 1) {
181+
if (self(self, matched_u)) {
182+
match[v] = u;
183+
match[u] = v;
184+
return true;
185+
}
186+
}
187+
}
188+
}
189+
dist.erase(u); // remove processed node
190+
return false;
191+
};
192+
193+
// main loop: alternate BFS and DFS phases
194+
while (bfs()) {
195+
for (const auto& u : U) {
196+
if (match.find(u) == match.end()) {
197+
if (dfs(dfs, u)) {
198+
matchingSize++;
199+
}
200+
}
201+
}
202+
}
203+
204+
// prepare result
205+
result.success = true;
206+
result.maxMatching = matchingSize;
207+
208+
// build matching pairs (only from U to avoid duplicates)
209+
for (const auto& u : U) {
210+
if (match.count(u)) {
211+
result.matching.push_back({u->getUserId(), match.at(u)->getUserId()});
212+
}
213+
}
214+
215+
return result;
216+
}
217+
218+
} // namespace CXXGraph
219+
220+
#endif // __CXXGRAPH_HOPCROFTKARP_IMPL_H__

include/CXXGraph/Graph/Graph.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
#include "CXXGraph/Graph/Algorithm/Dijkstra_impl.hpp"
3838
#include "CXXGraph/Graph/Algorithm/FloydWarshall_impl.hpp"
3939
#include "CXXGraph/Graph/Algorithm/FordFulkerson_impl.hpp"
40+
#include "CXXGraph/Graph/Algorithm/HopcroftKarp_impl.hpp"
4041
#include "CXXGraph/Graph/Algorithm/Kahn_impl.hpp"
4142
#include "CXXGraph/Graph/Algorithm/Kosaraju_impl.hpp"
4243
#include "CXXGraph/Graph/Algorithm/Kruskal_impl.hpp"

include/CXXGraph/Graph/Graph_decl.h

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -962,6 +962,22 @@ class Graph {
962962
virtual double fordFulkersonMaxFlow(const Node<T> &source,
963963
const Node<T> &target) const;
964964

965+
/**
966+
* @brief This function performs the Hopcroft-Karp algorithm to find the
967+
* maximum matching in a bipartite graph.
968+
*
969+
* @return HopcroftKarpResult containing success status, error message (if
970+
* any), the size of maximum matching, and the actual matching pairs
971+
*
972+
* Note: The function requires an undirected graph. If the graph is not
973+
* bipartite, the algorithm will return a matching of size 0 with success =
974+
* true.
975+
*
976+
* Complexity: O(E√V) where E is the number of edges and V is the number of
977+
* vertices
978+
*/
979+
virtual const HopcroftKarpResult_struct hopcroftKarp() const;
980+
965981
/**
966982
* @brief Welsh-Powell Coloring algorithm
967983
* @return a std::map of keys being the nodes and the values being the color

include/CXXGraph/Utility/Typedef.hpp

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,15 @@ struct BronKerboschResult_struct {
248248
template <typename T>
249249
using BronKerboschResult = BronKerboschResult_struct<T>;
250250

251+
/// Struct that contains the information about Hopcroft-Karp Algorithm results
252+
struct HopcroftKarpResult_struct {
253+
bool success = false; // TRUE if the function does not return error, FALSE otherwise
254+
std::string errorMessage = ""; // message of error
255+
int maxMatching = 0; // Size of maximum bipartite matching
256+
std::vector<std::pair<std::string, std::string>> matching = {}; // The matching pairs (node userIds)
257+
};
258+
using HopcroftKarpResult = HopcroftKarpResult_struct;
259+
251260
///////////////////////////////////////////////////////////////////////////////////
252261
// Using Definition
253262
// ///////////////////////////////////////////////////////////////

test/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ if(TEST)
9999
add_test(test_fordfulkerson test_exe --gtest_filter=FordFulkerson*)
100100
add_test(test_fw test_exe --gtest_filter=FW*)
101101
add_test(test_graph_slicing test_exe --gtest_filter=GraphSlicing*)
102+
add_test(test_hopcroft_karp test_exe --gtest_filter=HopcroftKarp*)
102103
add_test(test_kruskal test_exe --gtest_filter=Kruskal*)
103104
add_test(test_mtx test_exe --gtest_filter=MTX*)
104105
add_test(test_prim test_exe --gtest_filter=Prim*)

0 commit comments

Comments
 (0)