18
18
# ___________________________________________________________________________
19
19
20
20
import Graphs
21
- import BipartiteMatching as BM
22
21
22
+ const UNMATCHED = nothing
23
+ MatchedNodeType{T} = Union{T,typeof (UNMATCHED)}
23
24
24
25
"""
25
26
TODO: This should probably be promoted to Graphs.jl
@@ -44,20 +45,147 @@ function _is_valid_bipartition(graph::Graphs.Graph, set1::Set)
44
45
return true
45
46
end
46
47
48
+ # The following three functions are copied from the branch in PR #291
49
+ # of Graphs.jl, https://github.com/JuliaGraphs/Graphs.jl/pull/291.
50
+ # They will be removed when/if this PR is merged in favor of using the
51
+ # Graphs.jl maximum_matching function.
52
+
53
+ """
54
+ Determine whether an augmenting path exists and mark distances
55
+ so we can compute shortest-length augmenting paths in the DFS.
56
+ """
57
+ function _hk_augmenting_bfs! (
58
+ graph:: Graphs.AbstractGraph{T} ,
59
+ set1:: Vector{T} ,
60
+ matching:: Dict{T,MatchedNodeType{T}} ,
61
+ distance:: Dict{MatchedNodeType{T},Float64} ,
62
+ ):: Bool where {T<: Integer }
63
+ # Initialize queue with the unmatched nodes in set1
64
+ queue = Vector {MatchedNodeType{eltype(graph)}} ([
65
+ n for n in set1 if matching[n] == UNMATCHED
66
+ ])
67
+
68
+ distance[UNMATCHED] = Inf
69
+ for n in set1
70
+ if matching[n] == UNMATCHED
71
+ distance[n] = 0.0
72
+ else
73
+ distance[n] = Inf
74
+ end
75
+ end
76
+
77
+ while ! isempty (queue)
78
+ n1 = popfirst! (queue)
79
+
80
+ # If n1 is (a) matched or (b) in set1
81
+ if distance[n1] < Inf && n1 != UNMATCHED
82
+ for n2 in Graphs. neighbors (graph, n1)
83
+ # If n2 has not been encountered
84
+ if distance[matching[n2]] == Inf
85
+ # Give it a distance
86
+ distance[matching[n2]] = distance[n1] + 1
87
+
88
+ # Note that n2 could be unmatched
89
+ push! (queue, matching[n2])
90
+ end
91
+ end
92
+ end
93
+ end
94
+
95
+ found_augmenting_path = (distance[UNMATCHED] < Inf )
96
+ # The distance to UNMATCHED is the length of the shortest augmenting path
97
+ return found_augmenting_path
98
+ end
99
+
100
+ """
101
+ Compute augmenting paths and update the matching
102
+ """
103
+ function _hk_augmenting_dfs! (
104
+ graph:: Graphs.AbstractGraph{T} ,
105
+ root:: MatchedNodeType{T} ,
106
+ matching:: Dict{T,MatchedNodeType{T}} ,
107
+ distance:: Dict{MatchedNodeType{T},Float64} ,
108
+ ):: Bool where {T<: Integer }
109
+ if root != UNMATCHED
110
+ for n in Graphs. neighbors (graph, root)
111
+ # Traverse edges of the minimum-length alternating path
112
+ if distance[matching[n]] == distance[root] + 1
113
+ if _hk_augmenting_dfs! (graph, matching[n], matching, distance)
114
+ # If the edge is part of an augmenting path, update the
115
+ # matching
116
+ matching[root] = n
117
+ matching[n] = root
118
+ return true
119
+ end
120
+ end
121
+ end
122
+ # If we could not find a matched edge that was part of an augmenting
123
+ # path, we need to make sure we don't consider this vertex again
124
+ distance[root] = Inf
125
+ return false
126
+ else
127
+ # Return true to indicate that we are part of an augmenting path
128
+ return true
129
+ end
130
+ end
131
+
132
+ """
133
+ hopcroft_karp_matching(graph::AbstractGraph)::Dict
134
+
135
+ Compute a maximum-cardinality matching of a bipartite graph via the
136
+ [Hopcroft-Karp algorithm](https://en.wikipedia.org/wiki/Hopcroft-Karp_algorithm).
137
+
138
+ The return type is a dict mapping nodes to nodes. All matched nodes are included
139
+ as keys. For example, if `i` is matched with `j`, `i => j` and `j => i` are both
140
+ included in the returned dict.
141
+
142
+ ### Performance
143
+
144
+ The algorithms runs in O((m + n)n^0.5), where n is the number of vertices and
145
+ m is the number of edges. As it does not assume the number of edges is O(n^2),
146
+ this algorithm is particularly effective for sparse bipartite graphs.
147
+
148
+ ### Arguments
149
+
150
+ * `graph`: The bipartite `Graph` for which a maximum matching is computed
151
+
152
+ ### Exceptions
153
+
154
+ * `ArgumentError`: The provided graph is not bipartite
155
+
156
+ """
157
+ function hopcroft_karp_matching (graph:: Graphs.AbstractGraph{T} ):: Dict{T,T} where {T<: Integer }
158
+ bmap = Graphs. bipartite_map (graph)
159
+ if length (bmap) != Graphs. nv (graph)
160
+ throw (ArgumentError (" Provided graph is not bipartite" ))
161
+ end
162
+ set1 = [n for n in Graphs. vertices (graph) if bmap[n] == 1 ]
163
+
164
+ # Initialize "state" that is modified during the algorithm
165
+ matching = Dict {eltype(graph),MatchedNodeType{eltype(graph)}} (
166
+ n => UNMATCHED for n in Graphs. vertices (graph)
167
+ )
168
+ distance = Dict {MatchedNodeType{eltype(graph)},Float64} ()
169
+
170
+ # BFS to determine whether any augmenting paths exist
171
+ while _hk_augmenting_bfs! (graph, set1, matching, distance)
172
+ for n1 in set1
173
+ if matching[n1] == UNMATCHED
174
+ # DFS to update the matching along a minimum-length
175
+ # augmenting path
176
+ _hk_augmenting_dfs! (graph, n1, matching, distance)
177
+ end
178
+ end
179
+ end
180
+ matching = Dict (i => j for (i, j) in matching if j != UNMATCHED)
181
+ return matching
182
+ end
47
183
48
184
function maximum_matching (graph:: Graphs.Graph , set1:: Set )
49
185
if ! _is_valid_bipartition (graph, set1)
50
186
throw (Exception)
51
187
end
52
- n_nodes = Graphs. nv (graph)
53
- card1 = length (set1)
54
- nodes1 = sort ([node for node in set1])
55
- set2 = setdiff (Set (1 : n_nodes), set1)
56
- nodes2 = sort ([node for node in set2])
57
- edge_set = Set ((n1, n2) for n1 in nodes1 for n2 in Graphs. neighbors (graph, n1))
58
- amat = BitArray {2} ((r, c) in edge_set for r in nodes1, c in nodes2)
59
- matching, _ = BM. findmaxcardinalitybipartitematching (amat)
60
- # Translate row/column coordinates back into nodes of the graph
61
- graph_matching = Dict (nodes1[r] => nodes2[c] for (r, c) in matching)
62
- return graph_matching
188
+ matching = hopcroft_karp_matching (graph)
189
+ matching = Dict (i => j for (i, j) in matching if i in set1)
190
+ return matching
63
191
end
0 commit comments