This repository has been archived by the owner on Oct 22, 2021. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Use the Hungarian algorithm for maximum-weight matching (#7)
* 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
1 parent
9883489
commit fe0cfa7
Showing
6 changed files
with
218 additions
and
63 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,3 +3,4 @@ LightGraphs 1.2 | |
JuMP 0.18 | ||
MathProgBase 0.7 | ||
BlossomV 0.4 | ||
Hungarian 0.2 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters