Skip to content

Commit

Permalink
Merge 3e9b69f into 5cae3f2
Browse files Browse the repository at this point in the history
  • Loading branch information
Upabjojr committed Dec 15, 2020
2 parents 5cae3f2 + 3e9b69f commit 03917d7
Show file tree
Hide file tree
Showing 5 changed files with 159 additions and 7 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
8 changes: 4 additions & 4 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 @@ -164,7 +164,7 @@ def find_matching(self) -> Dict[TLeft, TRight]:
else:
directed_graph[tail].add(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
107 changes: 107 additions & 0 deletions matchpy/matching/hopcroft_karp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
from typing import Generic, Dict, Set, TypeVar, Hashable, List, Tuple

TLeft = TypeVar('TLeft', bound=Hashable)
TRight = TypeVar('TRight', bound=Hashable)

INT_MAX = 10000000000000


class HopcroftKarp(Generic[TLeft, TRight]):
"""
Implementation of the Hopcroft-Karp algorithm on a bipartite graph.
The bipartite graph has types TLeft and TRight on the two partitions.
The constructor accepts a `map` mapping the left vertices to the set of
connected right vertices.
The method `.hopcroft_karp()` finds the maximum cardinality matching,
returning its cardinality. The matching will be stored in the file
`pair_left`
and `pair_right` after the matching is found.
"""

def __init__(self, _graph_left: Dict[TLeft, Set[TRight]]):
self._graph_left: Dict[TLeft, Set[TRight]] = _graph_left
self._reference_distance = INT_MAX
self.pair_left: Dict[TLeft, TRight] = {}
self.pair_right: Dict[TRight, TLeft] = {}
self._left: List[TLeft] = []
self._dist_left: Dict[TLeft, int] = {}
self._get_left_indices_vector(_graph_left)

def hopcroft_karp(self) -> int:
self.pair_left.clear()
self.pair_right.clear()
self._dist_left.clear()
left: TLeft
for left in self._left:
self._dist_left[left] = INT_MAX
matchings: int = 0
while True:
if not self._bfs_hopcroft_karp():
break
left: TLeft
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[TLeft, TRight]:
self.hopcroft_karp()
return self.pair_left

def _get_left_indices_vector(self, m: Dict[TLeft, Set[TRight]]) -> None:
p: Tuple[TLeft, Set[TRight]]
for p in m.items():
self._left.append(p[0])

def _bfs_hopcroft_karp(self) -> bool:
vertex_queue: List[TLeft] = []
left_vert: TLeft
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] = INT_MAX
self._reference_distance = INT_MAX
while True:
if len(vertex_queue) == 0:
break
left_vertex: TLeft = vertex_queue.pop(0)
if self._dist_left[left_vertex] >= self._reference_distance:
continue
right_vertex: TRight
for right_vertex in self._graph_left[left_vertex]:
if right_vertex not in self.pair_right:
if self._reference_distance == INT_MAX:
self._reference_distance = self._dist_left[left_vertex] + 1
else:
other_left: TLeft = self.pair_right[right_vertex]
if self._dist_left[other_left] == INT_MAX:
self._dist_left[other_left] = self._dist_left[left_vertex] + 1
vertex_queue.append(other_left)
return self._reference_distance < INT_MAX

def _swap_lr(self, left: TLeft, right: TRight) -> None:
self.pair_left[left] = right
self.pair_right[right] = left

def _dfs_hopcroft_karp(self, left: TLeft) -> bool:
right: TRight
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: TLeft = 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] = INT_MAX
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, Set

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, Set[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 = hk.hopcroft_karp()
assert hk.pair_left == expected
assert matchings == 5

graph: Dict[str, Set[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 = hk.hopcroft_karp()
assert hk.pair_left == expected
assert matchings == 6

graph: Dict[int, Set[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 = hk.hopcroft_karp()
assert hk.pair_left == expected
assert matchings == 4

graph: Dict[str, Set[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 = hk.hopcroft_karp()
assert hk.pair_left == expected
assert matchings == 7

0 comments on commit 03917d7

Please sign in to comment.