Skip to content
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