Skip to content

Commit

Permalink
Merge fb764d9 into 5cae3f2
Browse files Browse the repository at this point in the history
  • Loading branch information
Upabjojr committed Dec 19, 2020
2 parents 5cae3f2 + fb764d9 commit c2cf605
Show file tree
Hide file tree
Showing 5 changed files with 186 additions and 10 deletions.
1 change: 0 additions & 1 deletion environment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,5 @@ dependencies:
- pip:
- Sphinx
- graphviz
- hopcroftkarp
- multiset>=2.0,<3.0
- setuptools_scm
14 changes: 7 additions & 7 deletions matchpy/matching/bipartite.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@

from typing import (Dict, Generic, Hashable, Iterator, List, Set, Tuple, TypeVar, Union, cast, MutableMapping)

from matchpy.matching.hopcroft_karp import HopcroftKarp

try:
from graphviz import Digraph, Graph
except ImportError:
Digraph = Graph = None
from hopcroftkarp import HopcroftKarp

__all__ = ['BipartiteGraph', 'enum_maximum_matchings_iter']

Expand Down Expand Up @@ -141,8 +142,7 @@ def as_graph(self) -> Graph: # pragma: no cover
def find_matching(self) -> Dict[TLeft, TRight]:
"""Finds a matching in the bipartite graph.
This is done using the Hopcroft-Karp algorithm with an implementation from the
`hopcroftkarp` package.
This is done using the Hopcroft-Karp algorithm.
Returns:
A dictionary where each edge of the matching is represented by a key-value pair
Expand All @@ -154,17 +154,17 @@ def find_matching(self) -> Dict[TLeft, TRight]:
# In addition, the graph stores which part of the bipartite graph a node originated from
# to avoid problems when a value exists in both halfs.
# Only one direction of the undirected edge is needed for the HopcroftKarp class
directed_graph = {} # type: Dict[Tuple[int, TLeft], Set[Tuple[int, TRight]]]
directed_graph = {} # type: Dict[Tuple[int, TLeft], List[Tuple[int, TRight]]]

for (left, right) in self._edges:
tail = (LEFT, left)
head = (RIGHT, right)
if tail not in directed_graph:
directed_graph[tail] = {head}
directed_graph[tail] = [head]
else:
directed_graph[tail].add(head)
directed_graph[tail].append(head)

matching = HopcroftKarp(directed_graph).maximum_matching()
matching = HopcroftKarp(directed_graph).get_maximum_matching()

# Filter out the partitions (LEFT and RIGHT) and only return the matching edges
# that go from LEFT to RIGHT
Expand Down
131 changes: 131 additions & 0 deletions matchpy/matching/hopcroft_karp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
from collections import deque
from typing import Generic, Dict, TypeVar, Hashable, List, Tuple, Deque

THLeft = TypeVar('THLeft', bound=Hashable)
THRight = TypeVar('THRight', bound=Hashable)

FAKE_INFINITY = -1


class HopcroftKarp(Generic[THLeft, THRight]):
"""Implementation of the Hopcroft-Karp algorithm on a bipartite graph.
The two partitions of the bipartite graph may have different types,
which are here represented by THLeft and THRight.
The constructor accepts a ``dict`` mapping the left vertices to the set
of connected right vertices.
An instance of maximum matching may be returned by
``.get_maximum_matching()``, while ``.get_maximum_matching_num()``
returns both cardinality and an instance of maximum matching.
The internal algorithm does not use sets in order to keep identical
results across different Python versions.
"""

def __init__(self, graph_left: Dict[THLeft, List[THRight]]):
"""Construct the HopcroftKarp class with a bipartite graph.
Args:
graph_left: a dictionary mapping the left-nodes to a list of
right-nodes among which connections exist. The list shall not
contain duplicates.
"""
self._graph_left: Dict[THLeft, List[THRight]] = graph_left
self._reference_distance: int = FAKE_INFINITY
self._pair_left: Dict[THLeft, THRight] = {}
self._pair_right: Dict[THRight, THLeft] = {}
self._left: List[THLeft] = list(self._graph_left.keys())
self._dist_left: Dict[THLeft, int] = {}

def _run_hopcroft_karp(self) -> int:
self._pair_left.clear()
self._pair_right.clear()
self._dist_left.clear()
left: THLeft
for left in self._left:
self._dist_left[left] = FAKE_INFINITY
matchings: int = 0
while True:
if not self._bfs_hopcroft_karp():
break
for left in self._left:
if left in self._pair_left:
continue
if self._dfs_hopcroft_karp(left):
matchings += 1
return matchings

def get_maximum_matching(self) -> Dict[THLeft, THRight]:
"""Find an instance of maximum matching for the given bipartite graph.
Returns:
A dictionary representing an instance of maximum matching.
"""
matchings, maximum_matching = self.get_maximum_matching_num()
return maximum_matching

def get_maximum_matching_num(self) -> Tuple[int, Dict[THLeft, THRight]]:
"""Find an instance of maximum matching and the number of matchings
found.
Returns:
A tuple containing the number of matchings found and a dictionary
representing an instance of maximum matching on the given
bipartite graph.
"""
matchings = self._run_hopcroft_karp()
return matchings, self._pair_left

def _bfs_hopcroft_karp(self) -> bool:
vertex_queue: Deque[THLeft] = deque([])
left_vert: THLeft
for left_vert in self._left:
if left_vert not in self._pair_left:
vertex_queue.append(left_vert)
self._dist_left[left_vert] = 0
else:
self._dist_left[left_vert] = FAKE_INFINITY
self._reference_distance = FAKE_INFINITY
while True:
if len(vertex_queue) == 0:
break
left_vertex: THLeft = vertex_queue.popleft()
if self._dist_left[left_vertex] == self._reference_distance == FAKE_INFINITY:
continue
if self._dist_left[left_vertex] >= self._reference_distance != FAKE_INFINITY:
continue
right_vertex: THRight
for right_vertex in self._graph_left[left_vertex]:
if right_vertex not in self._pair_right:
if self._reference_distance == FAKE_INFINITY:
self._reference_distance = self._dist_left[left_vertex] + 1
else:
other_left: THLeft = self._pair_right[right_vertex]
if self._dist_left[other_left] == FAKE_INFINITY:
self._dist_left[other_left] = self._dist_left[left_vertex] + 1
vertex_queue.append(other_left)
return self._reference_distance != FAKE_INFINITY

def _swap_lr(self, left: THLeft, right: THRight) -> None:
self._pair_left[left] = right
self._pair_right[right] = left

def _dfs_hopcroft_karp(self, left: THLeft) -> bool:
right: THRight
for right in self._graph_left[left]:
if right not in self._pair_right:
if self._reference_distance == self._dist_left[left] + 1:
self._swap_lr(left, right)
return True
else:
other_left: THLeft = self._pair_right[right]
if self._dist_left[other_left] == self._dist_left[left] + 1:
if self._dfs_hopcroft_karp(other_left):
self._swap_lr(left, right)
return True
self._dist_left[left] = FAKE_INFINITY
return False
3 changes: 1 addition & 2 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,7 @@ python_requires = >=3.6
setup_requires = setuptools>=36.7.0
pytest-runner
tests_require = matchpy[tests]
install_requires = hopcroftkarp>=1.2,<2.0
multiset>=2.0,<3.0
install_requires = multiset>=2.0,<3.0

[options.packages.find]
exclude = tests
Expand Down
47 changes: 47 additions & 0 deletions tests/test_hopcroft_karp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
from typing import Dict, List

from matchpy.matching.hopcroft_karp import HopcroftKarp


class TestHopcroftKarp:
"""
Testing the implementation of the Hopcroft Karp algorithm.
"""

def test_hopcroft_karp(self):

graph: Dict[int, List[str]] = {
0: ["v0", "v1"],
1: ["v0", "v4"],
2: ["v2", "v3"],
3: ["v0", "v4"],
4: ["v0", "v3"],
}
expected: Dict[int, str] = {0: "v1", 1: "v4", 2: "v2", 3: "v0", 4: "v3"}
hk = HopcroftKarp[int, str](graph)
matchings, maximum_matching = hk.get_maximum_matching_num()
assert maximum_matching == expected
assert matchings == 5

graph: Dict[str, List[int]] = {'A': [1, 2], 'B': [2, 3], 'C': [2], 'D': [3, 4, 5, 6],
'E': [4, 7], 'F': [7], 'G': [7]}
expected: Dict[str, int] = {'A': 1, 'B': 3, 'C': 2, 'D': 5, 'E': 4, 'F': 7}
hk = HopcroftKarp[str, int](graph)
matchings, maximum_matching = hk.get_maximum_matching_num()
assert maximum_matching == expected
assert matchings == 6

graph: Dict[int, List[str]] = {1: ['a', 'c'], 2: ['a', 'c'], 3: ['c', 'b'], 4: ['e']}
expected: Dict[int, str] = {1: 'a', 2: 'c', 3: 'b', 4: 'e'}
hk = HopcroftKarp[int, str](graph)
matchings, maximum_matching = hk.get_maximum_matching_num()
assert maximum_matching == expected
assert matchings == 4

graph: Dict[str, List[int]] = {'A': [3, 4], 'B': [3, 4], 'C': [3], 'D': [1, 5, 7],
'E': [1, 2, 7], 'F': [2, 8], 'G': [6], 'H': [2, 4, 8]}
expected: Dict[str, int] = {'A': 3, 'B': 4, 'D': 1, 'E': 7, 'F': 8, 'G': 6, 'H': 2}
hk = HopcroftKarp[str, int](graph)
matchings, maximum_matching = hk.get_maximum_matching_num()
assert maximum_matching == expected
assert matchings == 7

0 comments on commit c2cf605

Please sign in to comment.