Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Hopcroft-Karp matching algorithm #291

Open
wants to merge 21 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/make.jl
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ pages_files = [
"algorithms/editdist.md",
"algorithms/independentset.md",
"algorithms/linalg.md",
"algorithms/matching.md",
"algorithms/shortestpaths.md",
"algorithms/spanningtrees.md",
"algorithms/steinertree.md",
Expand Down
18 changes: 18 additions & 0 deletions docs/src/algorithms/matching.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Matchings

_Graphs.jl_ contains functions related to matchings.

## Index

```@index
Pages = ["matching.md"]
```

## Full docs

```@docs
AbstractMaximumMatchingAlgorithm
HopcroftKarpAlgorithm
maximum_cardinality_matching
hopcroft_karp_matching
```
9 changes: 8 additions & 1 deletion src/Graphs.jl
Original file line number Diff line number Diff line change
Expand Up @@ -422,7 +422,13 @@ export
independent_set,

# vertexcover
vertex_cover
vertex_cover,

# matching
AbstractMaximumMatchingAlgorithm,
HopcroftKarpAlgorithm,
hopcroft_karp_matching,
maximum_cardinality_matching

"""
Graphs
Expand Down Expand Up @@ -538,6 +544,7 @@ include("vertexcover/random_vertex_cover.jl")
include("Experimental/Experimental.jl")
include("Parallel/Parallel.jl")
include("Test/Test.jl")
include("matching/maximum_matching.jl")

using .LinAlg
end # module
133 changes: 133 additions & 0 deletions src/matching/hopcroft_karp.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
const UNMATCHED = nothing
MatchedNodeType{T} = Union{T,typeof(UNMATCHED)}

"""
Determine whether an augmenting path exists and mark distances
so we can compute shortest-length augmenting paths in the DFS.
"""
function _hk_augmenting_bfs!(
graph::AbstractGraph{T},
set1::Vector{T},
matching::Dict{T,MatchedNodeType{T}},
distance::Dict{MatchedNodeType{T},Float64},
)::Bool where {T<:Integer}
# Initialize queue with the unmatched nodes in set1
queue = Vector{MatchedNodeType{eltype(graph)}}([
n for n in set1 if matching[n] == UNMATCHED
])

distance[UNMATCHED] = Inf
for n in set1
if matching[n] == UNMATCHED
distance[n] = 0.0
else
distance[n] = Inf
end
end

while !isempty(queue)
n1 = popfirst!(queue)

# If n1 is (a) matched or (b) in set1
if distance[n1] < Inf && n1 != UNMATCHED
for n2 in neighbors(graph, n1)
# If n2 has not been encountered
if distance[matching[n2]] == Inf
# Give it a distance
distance[matching[n2]] = distance[n1] + 1

# Note that n2 could be unmatched
push!(queue, matching[n2])
end
end
end
end

found_augmenting_path = (distance[UNMATCHED] < Inf)
# The distance to UNMATCHED is the length of the shortest augmenting path
return found_augmenting_path
end

"""
Compute augmenting paths and update the matching
"""
function _hk_augmenting_dfs!(
graph::AbstractGraph{T},
root::MatchedNodeType{T},
matching::Dict{T,MatchedNodeType{T}},
distance::Dict{MatchedNodeType{T},Float64},
)::Bool where {T<:Integer}
if root != UNMATCHED
for n in neighbors(graph, root)
# Traverse edges of the minimum-length alternating path
if distance[matching[n]] == distance[root] + 1
if _hk_augmenting_dfs!(graph, matching[n], matching, distance)
# If the edge is part of an augmenting path, update the
# matching
matching[root] = n
matching[n] = root
return true
end
end
end
# If we could not find a matched edge that was part of an augmenting
# path, we need to make sure we don't consider this vertex again
distance[root] = Inf
return false
else
# Return true to indicate that we are part of an augmenting path
return true
end
end

"""
hopcroft_karp_matching(graph::AbstractGraph)::Dict

Compute a maximum-cardinality matching of a bipartite graph via the
[Hopcroft-Karp algorithm](https://en.wikipedia.org/wiki/Hopcroft-Karp_algorithm).

The return type is a dict mapping nodes to nodes. All matched nodes are included
as keys. For example, if `i` is matched with `j`, `i => j` and `j => i` are both
included in the returned dict.

### Performance

The algorithms runs in O((m + n)n^0.5), where n is the number of vertices and
m is the number of edges. As it does not assume the number of edges is O(n^2),
this algorithm is particularly effective for sparse bipartite graphs.

### Arguments

* `graph`: The bipartite `Graph` for which a maximum matching is computed

### Exceptions

* `ArgumentError`: The provided graph is not bipartite

"""
function hopcroft_karp_matching(graph::AbstractGraph{T})::Dict{T,T} where {T<:Integer}
bmap = bipartite_map(graph)
if length(bmap) != nv(graph)
throw(ArgumentError("Provided graph is not bipartite"))
end
set1 = [n for n in vertices(graph) if bmap[n] == 1]

# Initialize "state" that is modified during the algorithm
matching = Dict{eltype(graph),MatchedNodeType{eltype(graph)}}(
n => UNMATCHED for n in vertices(graph)
)
distance = Dict{MatchedNodeType{eltype(graph)},Float64}()

# BFS to determine whether any augmenting paths exist
while _hk_augmenting_bfs!(graph, set1, matching, distance)
for n1 in set1
if matching[n1] == UNMATCHED
# DFS to update the matching along a minimum-length
# augmenting path
_hk_augmenting_dfs!(graph, n1, matching, distance)
end
end
end
matching = Dict(i => j for (i, j) in matching if j != UNMATCHED)
return matching
end
76 changes: 76 additions & 0 deletions src/matching/maximum_matching.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
include("hopcroft_karp.jl")

"""
AbstractMaximumMatchingAlgorithm

Abstract type for maximum cardinality matching algorithms
"""
abstract type AbstractMaximumMatchingAlgorithm end

"""
HopcroftKarpAlgorithm

The [Hopcroft-Karp algorithm](https://en.wikipedia.org/wiki/Hopcroft-Karp_algorithm)
for computing a maximum cardinality matching of a bipartite graph.
"""
struct HopcroftKarpAlgorithm <: AbstractMaximumMatchingAlgorithm end

"""
maximum_cardinality_matching(
graph::AbstractGraph,
algorithm::AbstractMaximumMatchingAlgorithm,
)::Dict{Int, Int}

Compute a maximum-cardinality matching.

The return type is a dict mapping nodes to nodes. All matched nodes are included
as keys. For example, if `i` is matched with `j`, `i => j` and `j => i` are both
included in the returned dict.

### Arguments

* `graph`: The `Graph` for which a maximum matching is computed

* `algorithm`: The algorithm to use to compute the matching. Default is
`HopcroftKarpAlgorithm`.

### Algorithms
Currently implemented algorithms are:

* Hopcroft-Karp

### Exceptions

* `ArgumentError`: The provided graph is not bipartite but an algorithm that
only applies to bipartite graphs, e.g. Hopcroft-Karp, was chosen

### Example
```jldoctest
julia> using Graphs

julia> g = path_graph(6)
{6, 5} undirected simple Int64 graph

julia> maximum_cardinality_matching(g)
Dict{Int64, Int64} with 6 entries:
5 => 6
4 => 3
6 => 5
2 => 1
3 => 4
1 => 2

```
"""
function maximum_cardinality_matching(
graph::AbstractGraph{T};
algorithm::AbstractMaximumMatchingAlgorithm=HopcroftKarpAlgorithm(),
)::Dict{T,T} where {T<:Integer}
return maximum_cardinality_matching(graph, algorithm)
end

function maximum_cardinality_matching(
graph::AbstractGraph{T}, algorithm::HopcroftKarpAlgorithm
)::Dict{T,T} where {T<:Integer}
return hopcroft_karp_matching(graph)
end
Loading