1818# ___________________________________________________________________________
1919
2020import Graphs
21- import BipartiteMatching as BM
2221
22+ const UNMATCHED = nothing
23+ MatchedNodeType{T} = Union{T,typeof (UNMATCHED)}
2324
2425"""
2526TODO: This should probably be promoted to Graphs.jl
@@ -44,20 +45,147 @@ function _is_valid_bipartition(graph::Graphs.Graph, set1::Set)
4445 return true
4546end
4647
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
47183
48184function maximum_matching (graph:: Graphs.Graph , set1:: Set )
49185 if ! _is_valid_bipartition (graph, set1)
50186 throw (Exception)
51187 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
63191end
0 commit comments