Skip to content

Commit 8ea4165

Browse files
committed
Implement Hopcroft-karp algorithm to find the maximum matching of a undirected bipartite graph
1 parent f34b7d6 commit 8ea4165

File tree

4 files changed

+526
-3
lines changed

4 files changed

+526
-3
lines changed

pydatastructs/graphs/__init__.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,10 @@
2121
all_pair_shortest_paths,
2222
topological_sort,
2323
topological_sort_parallel,
24-
max_flow
24+
max_flow,
25+
maximum_matching,
26+
maximum_matching_parallel,
27+
bipartite_coloring
2528
)
2629

2730
__all__.extend(algorithms.__all__)

pydatastructs/graphs/algorithms.py

+323-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,10 @@
2323
'all_pair_shortest_paths',
2424
'topological_sort',
2525
'topological_sort_parallel',
26-
'max_flow'
26+
'max_flow',
27+
'maximum_matching',
28+
'maximum_matching_parallel',
29+
'bipartite_coloring'
2730
]
2831

2932
Stack = Queue = deque
@@ -1216,3 +1219,322 @@ def max_flow(graph, source, sink, algorithm='edmonds_karp', **kwargs):
12161219
f"Currently {algorithm} algorithm isn't implemented for "
12171220
"performing max flow on graphs.")
12181221
return getattr(algorithms, func)(graph, source, sink)
1222+
1223+
def bipartite_coloring(graph: Graph, **kwargs) -> tuple[bool, dict]:
1224+
"""
1225+
Finds a 2-coloring of the given graph if it is bipartite.
1226+
1227+
Parameters
1228+
==========
1229+
1230+
graph: Graph
1231+
The graph under consideration.
1232+
invert: bool
1233+
If True, the colors are inverted.
1234+
make_undirected: bool
1235+
If False, the input graph should be undirected else it can be made undirected by setting this to True
1236+
backend: pydatastructs.Backend
1237+
The backend to be used.
1238+
Optional, by default, the best available
1239+
backend is used.
1240+
1241+
Returns
1242+
=======
1243+
1244+
tuple
1245+
A tuple containing a boolean value and a dictionary.
1246+
The boolean value is True if the graph is bipartite
1247+
and False otherwise. The dictionary contains the
1248+
color assigned to each vertex.
1249+
1250+
Examples
1251+
========
1252+
1253+
>>> from pydatastructs import Graph, AdjacencyListGraphNode, bipartite_coloring
1254+
>>> v_1 = AdjacencyListGraphNode('v_1')
1255+
>>> v_2 = AdjacencyListGraphNode('v_2')
1256+
>>> v_3 = AdjacencyListGraphNode('v_3')
1257+
>>> v_4 = AdjacencyListGraphNode('v_4')
1258+
>>> graph = Graph(v_1, v_2, v_3, v_4)
1259+
>>> graph.add_edge('v_1', 'v_2')
1260+
>>> graph.add_edge('v_2', 'v_3')
1261+
>>> graph.add_edge('v_4', 'v_1')
1262+
>>> bipartite_coloring(graph)
1263+
>>> (True, {'v_1': 0, 'v_2': 1, 'v_4': 1, 'v_3': 0})
1264+
1265+
References
1266+
==========
1267+
1268+
.. [1] https://en.wikipedia.org/wiki/Bipartite_graph
1269+
"""
1270+
1271+
color = {}
1272+
queue = Queue()
1273+
invert = kwargs.get('invert', False)
1274+
make_unidirected = kwargs.get('make_undirected', False)
1275+
1276+
if make_unidirected:
1277+
graph = graph.to_undirected_adjacency_list()
1278+
1279+
for start in graph.vertices:
1280+
if start not in color:
1281+
queue.append(start)
1282+
color[start] = 1 if invert else 0
1283+
1284+
while queue:
1285+
u = queue.popleft()
1286+
for v in graph.neighbors(u):
1287+
v_name = v.name
1288+
if v_name not in color:
1289+
color[v_name] = 1 - color[u]
1290+
queue.append(v_name)
1291+
elif color[v_name] == color[u]:
1292+
return (False, {})
1293+
1294+
return (True, color)
1295+
1296+
1297+
def _maximum_matching_hopcroft_karp_(graph: Graph):
1298+
U = set()
1299+
V = set()
1300+
bipartiteness, coloring = bipartite_coloring(graph)
1301+
1302+
if not bipartiteness:
1303+
raise ValueError("Graph is not bipartite.")
1304+
1305+
for node, c in coloring.items():
1306+
if c == 0:
1307+
U.add(node)
1308+
else:
1309+
V.add(node)
1310+
1311+
1312+
pair_U = {u: None for u in U}
1313+
pair_V = {v: None for v in V}
1314+
dist = {}
1315+
1316+
def bfs():
1317+
queue = Queue()
1318+
for u in U:
1319+
if pair_U[u] is None:
1320+
dist[u] = 0
1321+
queue.append(u)
1322+
else:
1323+
dist[u] = float('inf')
1324+
dist[None] = float('inf')
1325+
while queue:
1326+
u = queue.popleft()
1327+
if dist[u] < dist[None]:
1328+
for v in graph.neighbors(u):
1329+
if dist[pair_V[v.name]] == float('inf'):
1330+
dist[pair_V[v.name]] = dist[u] + 1
1331+
queue.append(pair_V[v.name])
1332+
return dist[None] != float('inf')
1333+
1334+
def dfs(u):
1335+
if u is not None:
1336+
for v in graph.neighbors(u):
1337+
if dist[pair_V[v.name]] == dist[u] + 1:
1338+
if dfs(pair_V[v.name]):
1339+
pair_V[v.name] = u
1340+
pair_U[u] = v.name
1341+
return True
1342+
dist[u] = float('inf')
1343+
return False
1344+
return True
1345+
1346+
matching = set()
1347+
while bfs():
1348+
for u in U:
1349+
if pair_U[u] is None:
1350+
if dfs(u):
1351+
matching.add((u, pair_U[u]))
1352+
1353+
return matching
1354+
1355+
def maximum_matching(graph: Graph, algorithm: str, **kwargs) -> set:
1356+
"""
1357+
Finds the maximum matching in the given undirected using the given algorithm.
1358+
1359+
Parameters
1360+
==========
1361+
1362+
graph: Graph
1363+
The graph under consideration.
1364+
algorithm: str
1365+
The algorithm to be used.
1366+
Currently, following are supported,
1367+
1368+
'hopcroft_karp' -> Hopcroft-Karp algorithm for Bipartite Graphs as given in [1].
1369+
make_undirected: bool
1370+
If False, the graph should be undirected or unwanted results may be obtained. The graph can be made undirected by setting this to true.
1371+
backend: pydatastructs.Backend
1372+
The backend to be used.
1373+
Optional, by default, the best available
1374+
backend is used.
1375+
1376+
Returns
1377+
=======
1378+
1379+
set
1380+
The set of edges which form the maximum matching.
1381+
1382+
Examples
1383+
========
1384+
1385+
>>> from pydatastructs import Graph, AdjacencyListGraphNode, maximum_matching
1386+
>>> v_1 = AdjacencyListGraphNode('v_1')
1387+
>>> v_2 = AdjacencyListGraphNode('v_2')
1388+
>>> v_3 = AdjacencyListGraphNode('v_3')
1389+
>>> v_4 = AdjacencyListGraphNode('v_4')
1390+
>>> graph = Graph(v_1, v_2, v_3, v_4)
1391+
>>> graph.add_edge('v_1', 'v_2')
1392+
>>> graph.add_edge('v_2', 'v_3')
1393+
>>> graph.add_edge('v_4', 'v_1')
1394+
>>> maximum_matching(graph, 'hopcroft_karp')
1395+
>>> {('v_1', 'v_4'), ('v_3', 'v_2')}
1396+
1397+
References
1398+
==========
1399+
1400+
.. [1] https://en.wikipedia.org/wiki/Hopcroft%E2%80%93Karp_algorithm
1401+
"""
1402+
1403+
1404+
raise_if_backend_is_not_python(
1405+
maximum_matching, kwargs.get('backend', Backend.PYTHON))
1406+
make_undirected = kwargs.get('make_undirected', False)
1407+
if make_undirected:
1408+
graph = graph.to_undirected_adjacency_list()
1409+
1410+
import pydatastructs.graphs.algorithms as algorithms
1411+
func = "_maximum_matching_" + algorithm + "_"
1412+
if not hasattr(algorithms, func):
1413+
raise NotImplementedError(
1414+
f"Currently {algorithm} algorithm isn't implemented for "
1415+
"finding maximum matching in graphs.")
1416+
return getattr(algorithms, func)(graph)
1417+
1418+
def _maximum_matching_hopcroft_karp_parallel(graph: Graph, num_threads: int) -> set:
1419+
U = set()
1420+
V = set()
1421+
bipartiteness, coloring = bipartite_coloring(graph)
1422+
1423+
if not bipartiteness:
1424+
raise ValueError("Graph is not bipartite.")
1425+
1426+
for node, c in coloring.items():
1427+
if c == 0:
1428+
U.add(node)
1429+
else:
1430+
V.add(node)
1431+
1432+
1433+
pair_U = {u: None for u in U}
1434+
pair_V = {v: None for v in V}
1435+
dist = {}
1436+
1437+
def bfs():
1438+
queue = Queue()
1439+
for u in U:
1440+
if pair_U[u] is None:
1441+
dist[u] = 0
1442+
queue.append(u)
1443+
else:
1444+
dist[u] = float('inf')
1445+
dist[None] = float('inf')
1446+
while queue:
1447+
u = queue.popleft()
1448+
if dist[u] < dist[None]:
1449+
for v in graph.neighbors(u):
1450+
if dist[pair_V[v.name]] == float('inf'):
1451+
dist[pair_V[v.name]] = dist[u] + 1
1452+
queue.append(pair_V[v.name])
1453+
return dist[None] != float('inf')
1454+
1455+
def dfs(u):
1456+
if u is not None:
1457+
for v in graph.neighbors(u):
1458+
if dist[pair_V[v.name]] == dist[u] + 1:
1459+
if dfs(pair_V[v.name]):
1460+
pair_V[v.name] = u
1461+
pair_U[u] = v.name
1462+
return True
1463+
dist[u] = float('inf')
1464+
return False
1465+
return True
1466+
1467+
matching = set()
1468+
1469+
while bfs():
1470+
unmatched_nodes = [u for u in U if pair_U[u] is None]
1471+
1472+
with ThreadPoolExecutor(max_workers=num_threads) as Executor:
1473+
results = Executor.map(dfs, unmatched_nodes)
1474+
1475+
for u, success in zip(unmatched_nodes, results):
1476+
if success:
1477+
matching.add((u, pair_U[u]))
1478+
1479+
return matching
1480+
1481+
1482+
def maximum_matching_parallel(graph: Graph, algorithm: str, num_threads: int, **kwargs):
1483+
"""
1484+
Finds the maximum matching in the given graph using the given algorithm using
1485+
the given number of threads.
1486+
1487+
Parameters
1488+
==========
1489+
1490+
graph: Graph
1491+
The graph under consideration.
1492+
algorithm: str
1493+
The algorithm to be used.
1494+
Currently, following are supported,
1495+
1496+
'hopcroft_karp' -> Hopcroft-Karp algorithm for Bipartite Graphs as given in [1].
1497+
num_threads: int
1498+
The maximum number of threads to be used.
1499+
backend: pydatastructs.Backend
1500+
The backend to be used.
1501+
Optional, by default, the best available
1502+
backend is used.
1503+
1504+
Returns
1505+
=======
1506+
1507+
set
1508+
The set of edges which form the maximum matching.
1509+
1510+
Examples
1511+
========
1512+
1513+
>>> from pydatastructs import Graph, AdjacencyListGraphNode, maximum_matching_parallel
1514+
>>> v_1 = AdjacencyListGraphNode('v_1')
1515+
>>> v_2 = AdjacencyListGraphNode('v_2')
1516+
>>> v_3 = AdjacencyListGraphNode('v_3')
1517+
>>> v_4 = AdjacencyListGraphNode('v_4')
1518+
>>> graph = Graph(v_1, v_2, v_3, v_4)
1519+
>>> graph.add_bidirectional_edge('v_1', 'v_2')
1520+
>>> graph.add_bidirectional_edge('v_2', 'v_3')
1521+
>>> graph.add_bidirectional_edge('v_4', 'v_1')
1522+
>>> maximum_matching_parallel(graph, 'hopcroft_karp', 1)
1523+
>>> {('v_1', 'v_4'), ('v_3', 'v_2')}
1524+
1525+
References
1526+
==========
1527+
1528+
.. [1] https://en.wikipedia.org/wiki/Hopcroft%E2%80%93Karp_algorithm
1529+
"""
1530+
1531+
raise_if_backend_is_not_python(
1532+
maximum_matching_parallel, kwargs.get('backend', Backend.PYTHON))
1533+
1534+
import pydatastructs.graphs.algorithms as algorithms
1535+
func = "_maximum_matching_" + algorithm + "_parallel"
1536+
if not hasattr(algorithms, func):
1537+
raise NotImplementedError(
1538+
f"Currently {algorithm} algorithm isn't implemented for "
1539+
"finding maximum matching in graphs.")
1540+
return getattr(algorithms, func)(graph, num_threads)

pydatastructs/graphs/graph.py

+25
Original file line numberDiff line numberDiff line change
@@ -161,3 +161,28 @@ def num_edges(self):
161161
"""
162162
raise NotImplementedError(
163163
"This is an abstract method.")
164+
165+
def add_bidirectional_edge(self, node1, node2, cost=None):
166+
"""
167+
Adds edges between node1 and node2 in both directions.
168+
"""
169+
self.add_edge(node1, node2, cost)
170+
self.add_edge(node2, node1, cost)
171+
172+
def to_undirected_adjacency_list(self):
173+
"""
174+
Converts the graph to undirected graph.
175+
"""
176+
vertexes = []
177+
undirected_graph = Graph(implementation='adjacency_list')
178+
for vertex in self.vertices:
179+
undirected_graph.add_vertex(
180+
self.__getattribute__(vertex)
181+
)
182+
183+
for vertex in self.vertices:
184+
for v in self.neighbors(vertex):
185+
edge = self.get_edge(vertex, v.name)
186+
undirected_graph.add_bidirectional_edge(vertex, v.name, edge)
187+
188+
return undirected_graph

0 commit comments

Comments
 (0)