Skip to content
This repository has been archived by the owner on Oct 22, 2021. It is now read-only.

Commit

Permalink
Use the Hungarian algorithm for maximum-weight matching (#7)
Browse files Browse the repository at this point in the history
* First implementation of link to Hungarian.jl.

* More consistent spacing in tests.

* More consistent way of writing tests.

* Allow using the package even if BlossomV is not properly installed (it only works on Linux).

* Add a test suite and make it pass.

* Restore BlossomV importing.

* Correctly pass information to the Hungarian.jl solver (WIP).

* Triple check implementation and tests.

* Implementation should now be correct (Hungarian algorithm is just for bipartite graphs).

* @matbesancon feedback.

* Restore old tests (but avoid checking many times for a bipartite graph).

* Change suggested by @Gnimuc.

* Add a common interface behind maximum_weight_maximal_matching

* Deprecate the old interface (based on LP) and add tests.

* Help solve conflicts)

* Typo.

* @matbesancon comment.

* Solve deprecations.
  • Loading branch information
dourouc05 authored and matbesancon committed Nov 7, 2018
1 parent 9883489 commit fe0cfa7
Show file tree
Hide file tree
Showing 6 changed files with 218 additions and 63 deletions.
1 change: 1 addition & 0 deletions REQUIRE
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ LightGraphs 1.2
JuMP 0.18
MathProgBase 0.7
BlossomV 0.4
Hungarian 0.2
6 changes: 4 additions & 2 deletions src/LightGraphsMatching.jl
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ using SparseArrays: spzeros
using JuMP
using MathProgBase: AbstractMathProgSolver
import BlossomV # 'using BlossomV' leads to naming conflicts with JuMP
using Hungarian

export MatchingResult, maximum_weight_matching, maximum_weight_maximal_matching, minimum_weight_perfect_matching
export MatchingResult, maximum_weight_matching, maximum_weight_maximal_matching, minimum_weight_perfect_matching, HungarianAlgorithm, LPAlgorithm

"""
struct MatchingResult{U}
Expand All @@ -31,6 +32,7 @@ end
include("lp.jl")
include("maximum_weight_matching.jl")
include("blossomv.jl")
include("hungarian.jl")
include("maximum_weight_maximal_matching.jl")

end # module

65 changes: 65 additions & 0 deletions src/hungarian.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
function maximum_weight_maximal_matching_hungarian(g::Graph,
w::AbstractMatrix{T}=default_weights(g)) where {T <: Real}
edge_list = collect(edges(g))
n = nv(g)

# Determine the bipartition of the graph.
bipartition = bipartite_map(g)
if length(bipartition) != n # Equivalent to !is_bipartite(g), but reuses the results of the previous function call.
error("The Hungarian algorithm only works for bipartite graphs; otherwise, prefer the Blossom algorithm (not yet available in LightGraphsMatching")
end
n_first = count(bipartition .== 1)
n_second = count(bipartition .== 2)

to_bipartition_1 = [count(bipartition[1:i] .== 1) for i in 1:n]
to_bipartition_2 = [count(bipartition[1:i] .== 2) for i in 1:n]

# hungarian() minimises the total cost, while this function is supposed to maximise the total weights.
wDual = maximum(w) .- w

# Remove weights that are not in the graph (Hungarian.jl considers all weights that are not missing values as real edges).
# Assume w is symmetric, so that the weight of matching i->j is the same as the one for j->i.
weights = Matrix{Union{Missing, T}}(missing, n_first, n_second)

for i in 1:n
for j in 1:n
if Edge(i, j) edge_list || Edge(j, i) edge_list
if bipartition[i] == 1 # and bipartition[j] == 2
idx_first = to_bipartition_1[i]
idx_second = to_bipartition_2[j]
else # bipartition[i] == 2 and bipartition[j] == 1
idx_first = to_bipartition_1[j]
idx_second = to_bipartition_2[i]
end

weight_to_add = (Edge(i, j) edge_list) ? wDual[i, j] : wDual[j, i]

weights[idx_first, idx_second] = weight_to_add
end
end
end

# Run the Hungarian algorithm.
assignment, _ = hungarian(weights)

# Convert the output format to match LGMatching's.
pairs = Tuple{Int, Int}[]
mate = fill(-1, n) # Initialise to unmatched.
for i in eachindex(assignment)
if assignment[i] != 0 # If matched:
original_i = findfirst(to_bipartition_1 .== i)
original_j = findfirst(to_bipartition_2 .== assignment[i])

mate[original_i] = original_j
mate[original_j] = original_i

push!(pairs, (original_i, original_j))
end
end

# Compute the cost for this matching (as weights had to be changed for Hungarian.jl, the one returned by hungarian() makes no sense).
cost = sum(w[p[1], p[2]] for p in pairs)

# Return the result.
return MatchingResult(cost, mate)
end
45 changes: 4 additions & 41 deletions src/lp.jl
Original file line number Diff line number Diff line change
@@ -1,30 +1,8 @@
"""
maximum_weight_maximal_matching(g, w::Dict{Edge,Real})
maximum_weight_maximal_matching(g, w::Dict{Edge,Real}, cutoff)
Given a bipartite graph `g` and an edgemap `w` containing weights associated to edges,
returns a matching with the maximum total weight among the ones containing the
greatest number of edges.
Edges in `g` not present in `w` will not be considered for the matching.
The algorithm relies on a linear relaxation on of the matching problem, which is
guaranteed to have integer solution on bipartite graps.
Eventually a `cutoff` argument can be given, to reduce computational times
excluding edges with weights lower than the cutoff.
The package JuMP.jl and one of its supported solvers is required.
The returned object is of type `MatchingResult`.
"""
function maximum_weight_maximal_matching end

function maximum_weight_maximal_matching(g::Graph, solver::AbstractMathProgSolver, w::AbstractMatrix{U}, cutoff::R) where {U<:Real, R<:Real}
return maximum_weight_maximal_matching(g, solver, cutoff_weights(w, cutoff))
function maximum_weight_maximal_matching_lp(g::Graph, solver::AbstractMathProgSolver, w::AbstractMatrix{T}, cutoff::R) where {T<:Real, R<:Real}
return maximum_weight_maximal_matching_lp(g, solver, cutoff_weights(w, cutoff))
end

function maximum_weight_maximal_matching(g::Graph, solver::AbstractMathProgSolver, w::AbstractMatrix{U}) where {U<:Real}
function maximum_weight_maximal_matching_lp(g::Graph, solver::AbstractMathProgSolver, w::AbstractMatrix{T}) where {T<:Real}
# TODO support for graphs with zero degree nodes
# TODO apply separately on each connected component
bpmap = bipartite_map(g)
Expand Down Expand Up @@ -88,7 +66,7 @@ function maximum_weight_maximal_matching(g::Graph, solver::AbstractMathProgSolve

mate = fill(-1, nv(g))
for e in edges(g)
if w[src(e),dst(e)] > zero(U)
if w[src(e),dst(e)] > zero(T)
inmatch = convert(Bool, sol[edgemap[e]])
if inmatch
mate[src(e)] = dst(e)
Expand All @@ -99,18 +77,3 @@ function maximum_weight_maximal_matching(g::Graph, solver::AbstractMathProgSolve

return MatchingResult(cost, mate)
end

"""
cutoff_weights copies the weight matrix with all elements below cutoff set to 0
"""
function cutoff_weights(w::AbstractMatrix{U}, cutoff::R) where {U<:Real, R<:Real}
wnew = copy(w)
for j in 1:size(w,2)
for i in 1:size(w,1)
if wnew[i,j] < cutoff
wnew[i,j] = zero(U)
end
end
end
wnew
end
99 changes: 99 additions & 0 deletions src/maximum_weight_maximal_matching.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
"""
AbstractMaximumWeightMaximalMatchingAlgorithm
Abstract type that allows users to pass in their preferred algorithm
"""
abstract type AbstractMaximumWeightMaximalMatchingAlgorithm end

"""
LPAlgorithm <: AbstractMaximumWeightMaximalMatchingAlgorithm
Forces the maximum_weight_maximal_matching function to use a linear programming formulation.
"""
struct LPAlgorithm <: AbstractMaximumWeightMaximalMatchingAlgorithm end

function maximum_weight_maximal_matching(
g::Graph,
w::AbstractMatrix{T},
algorithm::LPAlgorithm,
solver = nothing
) where {T<:Real}
if ! isa(solver, AbstractMathProgSolver)
error("The keyword argument solver must be an AbstractMathProgSolver, as accepted by JuMP.")
end

return maximum_weight_maximal_matching_lp(g, solver, w)
end

"""
HungarianAlgorithm <: AbstractMaximumWeightMaximalMatchingAlgorithm
Forces the maximum_weight_maximal_matching function to use the Hungarian algorithm.
"""
struct HungarianAlgorithm <: AbstractMaximumWeightMaximalMatchingAlgorithm end

function maximum_weight_maximal_matching(
g::Graph,
w::AbstractMatrix{T},
algorithm::HungarianAlgorithm,
solver = nothing
) where {T<:Real}
return maximum_weight_maximal_matching_hungarian(g, w)
end

"""
maximum_weight_maximal_matching{T<:Real}(g, w::Dict{Edge,T})
Given a bipartite graph `g` and an edge map `w` containing weights associated to edges,
returns a matching with the maximum total weight among the ones containing the
greatest number of edges.
Edges in `g` not present in `w` will not be considered for the matching.
A `cutoff` keyword argument can be given, to reduce computational times
excluding edges with weights lower than the cutoff.
Finally, a specific algorithm can be chosen (`algorithm` keyword argument);
each algorithm has specific dependencies. For instance:
- If `algorithm=HungarianAlgorithm()` (the default), the package Hungarian.jl is used.
This algorithm is always polynomial in time, with complexity O(n³).
- If `algorithm=LPAlgorithm()`, the package JuMP.jl and one of its supported solvers is required.
In this case, the algorithm relies on a linear relaxation on of the matching problem, which is
guaranteed to have integer solution on bipartite graphs. A solver must be provided with
the `solver` keyword parameter.
The returned object is of type `MatchingResult`.
"""
function maximum_weight_maximal_matching(
g::Graph,
w::AbstractMatrix{T};
cutoff = nothing,
algorithm::AbstractMaximumWeightMaximalMatchingAlgorithm = HungarianAlgorithm(),
solver = nothing
) where {T<:Real}

if cutoff != nothing && ! isa(cutoff, Real)
error("The cutoff value must be of type Real or nothing.")
end

if cutoff != nothing
return maximum_weight_maximal_matching(g, cutoff_weights(w, cutoff), algorithm, solver)
else
return maximum_weight_maximal_matching(g, w, algorithm, solver)
end
end

"""
cutoff_weights copies the weight matrix with all elements below cutoff set to 0
"""
function cutoff_weights(w::AbstractMatrix{T}, cutoff::R) where {T<:Real, R<:Real}
wnew = copy(w)
for j in 1:size(w,2)
for i in 1:size(w,1)
if wnew[i,j] < cutoff
wnew[i,j] = zero(T)
end
end
end
wnew
end

@deprecate maximum_weight_maximal_matching(g::Graph, solver::AbstractMathProgSolver, w::AbstractMatrix{T}, cutoff::R) where {T<:Real, R<:Real} maximum_weight_maximal_matching(g, w, algorithm=LPAlgorithm(), cutoff=cutoff, solver=solver)
65 changes: 45 additions & 20 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,6 @@ using LinearAlgebra: I

@testset "LightGraphsMatching" begin

g = CompleteGraph(4)
w = LightGraphsMatching.default_weights(g)
@test all((w + w') .≈ ones(4,4) - Matrix(I, 4,4))

w1 = [
1 3
5 1
]
w0 = [
0 3
5 0
]
@test all(w0 .≈ LightGraphsMatching.cutoff_weights(w1, 2))

@testset "maximum_weight_matching" begin
g = CompleteGraph(3)
w = [
Expand Down Expand Up @@ -105,7 +91,7 @@ end
w[1,4] = 1.
w[2,3] = 2.
w[2,4] = 11.
match = maximum_weight_maximal_matching(g, CbcSolver(), w)
match = maximum_weight_maximal_matching(g, w, algorithm=LPAlgorithm(), solver=CbcSolver())
@test match.weight 21
@test match.mate[1] == 3
@test match.mate[3] == 1
Expand All @@ -118,7 +104,7 @@ end
w[1,4] = 0.5
w[2,3] = 11
w[2,4] = 1
match = maximum_weight_maximal_matching(g, CbcSolver(), w)
match = maximum_weight_maximal_matching(g, w, algorithm=LPAlgorithm(), solver=CbcSolver())
@test match.weight 11.5
@test match.mate[1] == 4
@test match.mate[4] == 1
Expand All @@ -133,7 +119,7 @@ end
w[2,4] = 1
w[2,5] = -1
w[2,6] = -1
match = maximum_weight_maximal_matching(g,CbcSolver(),w,0)
match = maximum_weight_maximal_matching(g, w, algorithm=LPAlgorithm(), solver=CbcSolver(), cutoff=0)
@test match.weight 11.5
@test match.mate[1] == 4
@test match.mate[4] == 1
Expand All @@ -148,14 +134,53 @@ end
w[1,6] = 1
w[1,5] = -1

match = maximum_weight_maximal_matching(g,CbcSolver(),w,0)
match = maximum_weight_maximal_matching(g, w, algorithm=LPAlgorithm(), solver=CbcSolver(), cutoff=0)
@test match.weight 12
@test match.mate[1] == 6
@test match.mate[2] == 5
@test match.mate[3] == -1
@test match.mate[4] == -1
@test match.mate[5] == 2
@test match.mate[6] == 1


g = CompleteBipartiteGraph(2, 2)
w = zeros(4, 4)
w[1, 3] = 10.
w[1, 4] = 1.
w[2, 3] = 2.
w[2, 4] = 11.
match = maximum_weight_maximal_matching(g, w, algorithm=HungarianAlgorithm())
@test match.weight 21
@test match.mate[1] == 3
@test match.mate[3] == 1
@test match.mate[2] == 4
@test match.mate[4] == 2

g = CompleteGraph(3)
w = zeros(3, 3)
@test ! is_bipartite(g)
@test_throws ErrorException maximum_weight_maximal_matching(g, w, algorithm=HungarianAlgorithm())

g = CompleteBipartiteGraph(2, 4)
w = zeros(6, 6)
w[1, 3] = 10
w[1, 4] = 0.5
w[2, 3] = 11
w[2, 4] = 1
match = maximum_weight_maximal_matching(g, w, algorithm=HungarianAlgorithm())
@test match.weight 11.5

g = Graph(4)
add_edge!(g, 1, 3)
add_edge!(g, 1, 4)
add_edge!(g, 2, 4)
w = zeros(4, 4)
w[1, 3] = 1
w[1, 4] = 3
w[2, 4] = 1
match = maximum_weight_maximal_matching(g, w, algorithm=HungarianAlgorithm())
@test match.weight 2

end

Expand Down Expand Up @@ -214,8 +239,8 @@ end
@test match.weight -11.5


g =CompleteGraph(4)
w =Dict{Edge,Float64}()
g = CompleteGraph(4)
w = Dict{Edge,Float64}()
w[Edge(1,3)] = 10
w[Edge(1,4)] = 0.5
w[Edge(2,3)] = 11
Expand Down

0 comments on commit fe0cfa7

Please sign in to comment.