test_matching.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326
  1. """Unit tests for the :mod:`networkx.algorithms.bipartite.matching` module."""
  2. import itertools
  3. import pytest
  4. import networkx as nx
  5. from networkx.algorithms.bipartite.matching import (
  6. eppstein_matching,
  7. hopcroft_karp_matching,
  8. maximum_matching,
  9. minimum_weight_full_matching,
  10. to_vertex_cover,
  11. )
  12. class TestMatching:
  13. """Tests for bipartite matching algorithms."""
  14. def setup_method(self):
  15. """Creates a bipartite graph for use in testing matching algorithms.
  16. The bipartite graph has a maximum cardinality matching that leaves
  17. vertex 1 and vertex 10 unmatched. The first six numbers are the left
  18. vertices and the next six numbers are the right vertices.
  19. """
  20. self.simple_graph = nx.complete_bipartite_graph(2, 3)
  21. self.simple_solution = {0: 2, 1: 3, 2: 0, 3: 1}
  22. edges = [(0, 7), (0, 8), (2, 6), (2, 9), (3, 8), (4, 8), (4, 9), (5, 11)]
  23. self.top_nodes = set(range(6))
  24. self.graph = nx.Graph()
  25. self.graph.add_nodes_from(range(12))
  26. self.graph.add_edges_from(edges)
  27. # Example bipartite graph from issue 2127
  28. G = nx.Graph()
  29. G.add_nodes_from(
  30. [
  31. (1, "C"),
  32. (1, "B"),
  33. (0, "G"),
  34. (1, "F"),
  35. (1, "E"),
  36. (0, "C"),
  37. (1, "D"),
  38. (1, "I"),
  39. (0, "A"),
  40. (0, "D"),
  41. (0, "F"),
  42. (0, "E"),
  43. (0, "H"),
  44. (1, "G"),
  45. (1, "A"),
  46. (0, "I"),
  47. (0, "B"),
  48. (1, "H"),
  49. ]
  50. )
  51. G.add_edge((1, "C"), (0, "A"))
  52. G.add_edge((1, "B"), (0, "A"))
  53. G.add_edge((0, "G"), (1, "I"))
  54. G.add_edge((0, "G"), (1, "H"))
  55. G.add_edge((1, "F"), (0, "A"))
  56. G.add_edge((1, "F"), (0, "C"))
  57. G.add_edge((1, "F"), (0, "E"))
  58. G.add_edge((1, "E"), (0, "A"))
  59. G.add_edge((1, "E"), (0, "C"))
  60. G.add_edge((0, "C"), (1, "D"))
  61. G.add_edge((0, "C"), (1, "I"))
  62. G.add_edge((0, "C"), (1, "G"))
  63. G.add_edge((0, "C"), (1, "H"))
  64. G.add_edge((1, "D"), (0, "A"))
  65. G.add_edge((1, "I"), (0, "A"))
  66. G.add_edge((1, "I"), (0, "E"))
  67. G.add_edge((0, "A"), (1, "G"))
  68. G.add_edge((0, "A"), (1, "H"))
  69. G.add_edge((0, "E"), (1, "G"))
  70. G.add_edge((0, "E"), (1, "H"))
  71. self.disconnected_graph = G
  72. def check_match(self, matching):
  73. """Asserts that the matching is what we expect from the bipartite graph
  74. constructed in the :meth:`setup` fixture.
  75. """
  76. # For the sake of brevity, rename `matching` to `M`.
  77. M = matching
  78. matched_vertices = frozenset(itertools.chain(*M.items()))
  79. # Assert that the maximum number of vertices (10) is matched.
  80. assert matched_vertices == frozenset(range(12)) - {1, 10}
  81. # Assert that no vertex appears in two edges, or in other words, that
  82. # the matching (u, v) and (v, u) both appear in the matching
  83. # dictionary.
  84. assert all(u == M[M[u]] for u in range(12) if u in M)
  85. def check_vertex_cover(self, vertices):
  86. """Asserts that the given set of vertices is the vertex cover we
  87. expected from the bipartite graph constructed in the :meth:`setup`
  88. fixture.
  89. """
  90. # By Konig's theorem, the number of edges in a maximum matching equals
  91. # the number of vertices in a minimum vertex cover.
  92. assert len(vertices) == 5
  93. # Assert that the set is truly a vertex cover.
  94. for u, v in self.graph.edges():
  95. assert u in vertices or v in vertices
  96. # TODO Assert that the vertices are the correct ones.
  97. def test_eppstein_matching(self):
  98. """Tests that David Eppstein's implementation of the Hopcroft--Karp
  99. algorithm produces a maximum cardinality matching.
  100. """
  101. self.check_match(eppstein_matching(self.graph, self.top_nodes))
  102. def test_hopcroft_karp_matching(self):
  103. """Tests that the Hopcroft--Karp algorithm produces a maximum
  104. cardinality matching in a bipartite graph.
  105. """
  106. self.check_match(hopcroft_karp_matching(self.graph, self.top_nodes))
  107. def test_to_vertex_cover(self):
  108. """Test for converting a maximum matching to a minimum vertex cover."""
  109. matching = maximum_matching(self.graph, self.top_nodes)
  110. vertex_cover = to_vertex_cover(self.graph, matching, self.top_nodes)
  111. self.check_vertex_cover(vertex_cover)
  112. def test_eppstein_matching_simple(self):
  113. match = eppstein_matching(self.simple_graph)
  114. assert match == self.simple_solution
  115. def test_hopcroft_karp_matching_simple(self):
  116. match = hopcroft_karp_matching(self.simple_graph)
  117. assert match == self.simple_solution
  118. def test_eppstein_matching_disconnected(self):
  119. with pytest.raises(nx.AmbiguousSolution):
  120. match = eppstein_matching(self.disconnected_graph)
  121. def test_hopcroft_karp_matching_disconnected(self):
  122. with pytest.raises(nx.AmbiguousSolution):
  123. match = hopcroft_karp_matching(self.disconnected_graph)
  124. def test_issue_2127(self):
  125. """Test from issue 2127"""
  126. # Build the example DAG
  127. G = nx.DiGraph()
  128. G.add_edge("A", "C")
  129. G.add_edge("A", "B")
  130. G.add_edge("C", "E")
  131. G.add_edge("C", "D")
  132. G.add_edge("E", "G")
  133. G.add_edge("E", "F")
  134. G.add_edge("G", "I")
  135. G.add_edge("G", "H")
  136. tc = nx.transitive_closure(G)
  137. btc = nx.Graph()
  138. # Create a bipartite graph based on the transitive closure of G
  139. for v in tc.nodes():
  140. btc.add_node((0, v))
  141. btc.add_node((1, v))
  142. for u, v in tc.edges():
  143. btc.add_edge((0, u), (1, v))
  144. top_nodes = {n for n in btc if n[0] == 0}
  145. matching = hopcroft_karp_matching(btc, top_nodes)
  146. vertex_cover = to_vertex_cover(btc, matching, top_nodes)
  147. independent_set = set(G) - {v for _, v in vertex_cover}
  148. assert {"B", "D", "F", "I", "H"} == independent_set
  149. def test_vertex_cover_issue_2384(self):
  150. G = nx.Graph([(0, 3), (1, 3), (1, 4), (2, 3)])
  151. matching = maximum_matching(G)
  152. vertex_cover = to_vertex_cover(G, matching)
  153. for u, v in G.edges():
  154. assert u in vertex_cover or v in vertex_cover
  155. def test_vertex_cover_issue_3306(self):
  156. G = nx.Graph()
  157. edges = [(0, 2), (1, 0), (1, 1), (1, 2), (2, 2)]
  158. G.add_edges_from([((i, "L"), (j, "R")) for i, j in edges])
  159. matching = maximum_matching(G)
  160. vertex_cover = to_vertex_cover(G, matching)
  161. for u, v in G.edges():
  162. assert u in vertex_cover or v in vertex_cover
  163. def test_unorderable_nodes(self):
  164. a = object()
  165. b = object()
  166. c = object()
  167. d = object()
  168. e = object()
  169. G = nx.Graph([(a, d), (b, d), (b, e), (c, d)])
  170. matching = maximum_matching(G)
  171. vertex_cover = to_vertex_cover(G, matching)
  172. for u, v in G.edges():
  173. assert u in vertex_cover or v in vertex_cover
  174. def test_eppstein_matching():
  175. """Test in accordance to issue #1927"""
  176. G = nx.Graph()
  177. G.add_nodes_from(["a", 2, 3, 4], bipartite=0)
  178. G.add_nodes_from([1, "b", "c"], bipartite=1)
  179. G.add_edges_from([("a", 1), ("a", "b"), (2, "b"), (2, "c"), (3, "c"), (4, 1)])
  180. matching = eppstein_matching(G)
  181. assert len(matching) == len(maximum_matching(G))
  182. assert all(x in set(matching.keys()) for x in set(matching.values()))
  183. class TestMinimumWeightFullMatching:
  184. @classmethod
  185. def setup_class(cls):
  186. pytest.importorskip("scipy")
  187. def test_minimum_weight_full_matching_incomplete_graph(self):
  188. B = nx.Graph()
  189. B.add_nodes_from([1, 2], bipartite=0)
  190. B.add_nodes_from([3, 4], bipartite=1)
  191. B.add_edge(1, 4, weight=100)
  192. B.add_edge(2, 3, weight=100)
  193. B.add_edge(2, 4, weight=50)
  194. matching = minimum_weight_full_matching(B)
  195. assert matching == {1: 4, 2: 3, 4: 1, 3: 2}
  196. def test_minimum_weight_full_matching_with_no_full_matching(self):
  197. B = nx.Graph()
  198. B.add_nodes_from([1, 2, 3], bipartite=0)
  199. B.add_nodes_from([4, 5, 6], bipartite=1)
  200. B.add_edge(1, 4, weight=100)
  201. B.add_edge(2, 4, weight=100)
  202. B.add_edge(3, 4, weight=50)
  203. B.add_edge(3, 5, weight=50)
  204. B.add_edge(3, 6, weight=50)
  205. with pytest.raises(ValueError):
  206. minimum_weight_full_matching(B)
  207. def test_minimum_weight_full_matching_square(self):
  208. G = nx.complete_bipartite_graph(3, 3)
  209. G.add_edge(0, 3, weight=400)
  210. G.add_edge(0, 4, weight=150)
  211. G.add_edge(0, 5, weight=400)
  212. G.add_edge(1, 3, weight=400)
  213. G.add_edge(1, 4, weight=450)
  214. G.add_edge(1, 5, weight=600)
  215. G.add_edge(2, 3, weight=300)
  216. G.add_edge(2, 4, weight=225)
  217. G.add_edge(2, 5, weight=300)
  218. matching = minimum_weight_full_matching(G)
  219. assert matching == {0: 4, 1: 3, 2: 5, 4: 0, 3: 1, 5: 2}
  220. def test_minimum_weight_full_matching_smaller_left(self):
  221. G = nx.complete_bipartite_graph(3, 4)
  222. G.add_edge(0, 3, weight=400)
  223. G.add_edge(0, 4, weight=150)
  224. G.add_edge(0, 5, weight=400)
  225. G.add_edge(0, 6, weight=1)
  226. G.add_edge(1, 3, weight=400)
  227. G.add_edge(1, 4, weight=450)
  228. G.add_edge(1, 5, weight=600)
  229. G.add_edge(1, 6, weight=2)
  230. G.add_edge(2, 3, weight=300)
  231. G.add_edge(2, 4, weight=225)
  232. G.add_edge(2, 5, weight=290)
  233. G.add_edge(2, 6, weight=3)
  234. matching = minimum_weight_full_matching(G)
  235. assert matching == {0: 4, 1: 6, 2: 5, 4: 0, 5: 2, 6: 1}
  236. def test_minimum_weight_full_matching_smaller_top_nodes_right(self):
  237. G = nx.complete_bipartite_graph(3, 4)
  238. G.add_edge(0, 3, weight=400)
  239. G.add_edge(0, 4, weight=150)
  240. G.add_edge(0, 5, weight=400)
  241. G.add_edge(0, 6, weight=1)
  242. G.add_edge(1, 3, weight=400)
  243. G.add_edge(1, 4, weight=450)
  244. G.add_edge(1, 5, weight=600)
  245. G.add_edge(1, 6, weight=2)
  246. G.add_edge(2, 3, weight=300)
  247. G.add_edge(2, 4, weight=225)
  248. G.add_edge(2, 5, weight=290)
  249. G.add_edge(2, 6, weight=3)
  250. matching = minimum_weight_full_matching(G, top_nodes=[3, 4, 5, 6])
  251. assert matching == {0: 4, 1: 6, 2: 5, 4: 0, 5: 2, 6: 1}
  252. def test_minimum_weight_full_matching_smaller_right(self):
  253. G = nx.complete_bipartite_graph(4, 3)
  254. G.add_edge(0, 4, weight=400)
  255. G.add_edge(0, 5, weight=400)
  256. G.add_edge(0, 6, weight=300)
  257. G.add_edge(1, 4, weight=150)
  258. G.add_edge(1, 5, weight=450)
  259. G.add_edge(1, 6, weight=225)
  260. G.add_edge(2, 4, weight=400)
  261. G.add_edge(2, 5, weight=600)
  262. G.add_edge(2, 6, weight=290)
  263. G.add_edge(3, 4, weight=1)
  264. G.add_edge(3, 5, weight=2)
  265. G.add_edge(3, 6, weight=3)
  266. matching = minimum_weight_full_matching(G)
  267. assert matching == {1: 4, 2: 6, 3: 5, 4: 1, 5: 3, 6: 2}
  268. def test_minimum_weight_full_matching_negative_weights(self):
  269. G = nx.complete_bipartite_graph(2, 2)
  270. G.add_edge(0, 2, weight=-2)
  271. G.add_edge(0, 3, weight=0.2)
  272. G.add_edge(1, 2, weight=-2)
  273. G.add_edge(1, 3, weight=0.3)
  274. matching = minimum_weight_full_matching(G)
  275. assert matching == {0: 3, 1: 2, 2: 1, 3: 0}
  276. def test_minimum_weight_full_matching_different_weight_key(self):
  277. G = nx.complete_bipartite_graph(2, 2)
  278. G.add_edge(0, 2, mass=2)
  279. G.add_edge(0, 3, mass=0.2)
  280. G.add_edge(1, 2, mass=1)
  281. G.add_edge(1, 3, mass=2)
  282. matching = minimum_weight_full_matching(G, weight="mass")
  283. assert matching == {0: 3, 1: 2, 2: 1, 3: 0}