Skip to content

Commit 1a627ad

Browse files
authored
Merge pull request #15 from Robbybp/local-hk-matching
Use Hopcroft-Karp matching algorithm Merging despite failing coverage test. Including the HK implementation here should be temporary, and the non-bipartite exception is tested in the Graphs.jl implementation.
2 parents b932bcc + fe60062 commit 1a627ad

File tree

3 files changed

+142
-15
lines changed

3 files changed

+142
-15
lines changed

Project.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ authors = ["Robert Parker and contributors"]
44
version = "0.1.0"
55

66
[deps]
7-
BipartiteMatching = "79040ab4-24c8-4c92-950c-d48b5991a0f6"
87
Graphs = "86223c79-3864-5bf0-83f7-82e725a168b6"
98
JuMP = "4076af6c-e467-56ae-b986-b466b2749572"
109
MathOptInterface = "b8f27783-ece8-5eb3-8dc8-9495eed66fee"

src/dulmage_mendelsohn.jl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -129,15 +129,15 @@ function dulmage_mendelsohn(graph::Graphs.Graph, set1::Set)
129129
matched_with_reachable1 = [matching[n] for n in reachable1]
130130
matched_with_reachable2 = [matching[n] for n in reachable2]
131131

132-
filter = cat(
132+
filter = Set(cat(
133133
unmatched1,
134134
unmatched2,
135135
reachable1,
136136
reachable2,
137137
matched_with_reachable1,
138138
matched_with_reachable2;
139139
dims=1,
140-
)
140+
))
141141
other1 = [n for n in nodes1 if !(n in filter)]
142142
other2 = [n for n in nodes2 if !(n in filter)]
143143

src/maximum_matching.jl

Lines changed: 140 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,9 @@
1818
# ___________________________________________________________________________
1919

2020
import Graphs
21-
import BipartiteMatching as BM
2221

22+
const UNMATCHED = nothing
23+
MatchedNodeType{T} = Union{T,typeof(UNMATCHED)}
2324

2425
"""
2526
TODO: This should probably be promoted to Graphs.jl
@@ -44,20 +45,147 @@ function _is_valid_bipartition(graph::Graphs.Graph, set1::Set)
4445
return true
4546
end
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

48184
function 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
63191
end

0 commit comments

Comments
 (0)