Skip to content

Commit fe0cfa7

Browse files
dourouc05matbesancon
authored andcommitted
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.
1 parent 9883489 commit fe0cfa7

File tree

6 files changed

+218
-63
lines changed

6 files changed

+218
-63
lines changed

REQUIRE

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ LightGraphs 1.2
33
JuMP 0.18
44
MathProgBase 0.7
55
BlossomV 0.4
6+
Hungarian 0.2

src/LightGraphsMatching.jl

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@ using SparseArrays: spzeros
77
using JuMP
88
using MathProgBase: AbstractMathProgSolver
99
import BlossomV # 'using BlossomV' leads to naming conflicts with JuMP
10+
using Hungarian
1011

11-
export MatchingResult, maximum_weight_matching, maximum_weight_maximal_matching, minimum_weight_perfect_matching
12+
export MatchingResult, maximum_weight_matching, maximum_weight_maximal_matching, minimum_weight_perfect_matching, HungarianAlgorithm, LPAlgorithm
1213

1314
"""
1415
struct MatchingResult{U}
@@ -31,6 +32,7 @@ end
3132
include("lp.jl")
3233
include("maximum_weight_matching.jl")
3334
include("blossomv.jl")
35+
include("hungarian.jl")
36+
include("maximum_weight_maximal_matching.jl")
3437

3538
end # module
36-

src/hungarian.jl

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
function maximum_weight_maximal_matching_hungarian(g::Graph,
2+
w::AbstractMatrix{T}=default_weights(g)) where {T <: Real}
3+
edge_list = collect(edges(g))
4+
n = nv(g)
5+
6+
# Determine the bipartition of the graph.
7+
bipartition = bipartite_map(g)
8+
if length(bipartition) != n # Equivalent to !is_bipartite(g), but reuses the results of the previous function call.
9+
error("The Hungarian algorithm only works for bipartite graphs; otherwise, prefer the Blossom algorithm (not yet available in LightGraphsMatching")
10+
end
11+
n_first = count(bipartition .== 1)
12+
n_second = count(bipartition .== 2)
13+
14+
to_bipartition_1 = [count(bipartition[1:i] .== 1) for i in 1:n]
15+
to_bipartition_2 = [count(bipartition[1:i] .== 2) for i in 1:n]
16+
17+
# hungarian() minimises the total cost, while this function is supposed to maximise the total weights.
18+
wDual = maximum(w) .- w
19+
20+
# Remove weights that are not in the graph (Hungarian.jl considers all weights that are not missing values as real edges).
21+
# Assume w is symmetric, so that the weight of matching i->j is the same as the one for j->i.
22+
weights = Matrix{Union{Missing, T}}(missing, n_first, n_second)
23+
24+
for i in 1:n
25+
for j in 1:n
26+
if Edge(i, j) edge_list || Edge(j, i) edge_list
27+
if bipartition[i] == 1 # and bipartition[j] == 2
28+
idx_first = to_bipartition_1[i]
29+
idx_second = to_bipartition_2[j]
30+
else # bipartition[i] == 2 and bipartition[j] == 1
31+
idx_first = to_bipartition_1[j]
32+
idx_second = to_bipartition_2[i]
33+
end
34+
35+
weight_to_add = (Edge(i, j) edge_list) ? wDual[i, j] : wDual[j, i]
36+
37+
weights[idx_first, idx_second] = weight_to_add
38+
end
39+
end
40+
end
41+
42+
# Run the Hungarian algorithm.
43+
assignment, _ = hungarian(weights)
44+
45+
# Convert the output format to match LGMatching's.
46+
pairs = Tuple{Int, Int}[]
47+
mate = fill(-1, n) # Initialise to unmatched.
48+
for i in eachindex(assignment)
49+
if assignment[i] != 0 # If matched:
50+
original_i = findfirst(to_bipartition_1 .== i)
51+
original_j = findfirst(to_bipartition_2 .== assignment[i])
52+
53+
mate[original_i] = original_j
54+
mate[original_j] = original_i
55+
56+
push!(pairs, (original_i, original_j))
57+
end
58+
end
59+
60+
# Compute the cost for this matching (as weights had to be changed for Hungarian.jl, the one returned by hungarian() makes no sense).
61+
cost = sum(w[p[1], p[2]] for p in pairs)
62+
63+
# Return the result.
64+
return MatchingResult(cost, mate)
65+
end

src/lp.jl

Lines changed: 4 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,8 @@
1-
"""
2-
maximum_weight_maximal_matching(g, w::Dict{Edge,Real})
3-
maximum_weight_maximal_matching(g, w::Dict{Edge,Real}, cutoff)
4-
5-
Given a bipartite graph `g` and an edgemap `w` containing weights associated to edges,
6-
returns a matching with the maximum total weight among the ones containing the
7-
greatest number of edges.
8-
9-
Edges in `g` not present in `w` will not be considered for the matching.
10-
11-
The algorithm relies on a linear relaxation on of the matching problem, which is
12-
guaranteed to have integer solution on bipartite graps.
13-
14-
Eventually a `cutoff` argument can be given, to reduce computational times
15-
excluding edges with weights lower than the cutoff.
16-
17-
The package JuMP.jl and one of its supported solvers is required.
18-
19-
The returned object is of type `MatchingResult`.
20-
"""
21-
function maximum_weight_maximal_matching end
22-
23-
function maximum_weight_maximal_matching(g::Graph, solver::AbstractMathProgSolver, w::AbstractMatrix{U}, cutoff::R) where {U<:Real, R<:Real}
24-
return maximum_weight_maximal_matching(g, solver, cutoff_weights(w, cutoff))
1+
function maximum_weight_maximal_matching_lp(g::Graph, solver::AbstractMathProgSolver, w::AbstractMatrix{T}, cutoff::R) where {T<:Real, R<:Real}
2+
return maximum_weight_maximal_matching_lp(g, solver, cutoff_weights(w, cutoff))
253
end
264

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

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

10078
return MatchingResult(cost, mate)
10179
end
102-
103-
"""
104-
cutoff_weights copies the weight matrix with all elements below cutoff set to 0
105-
"""
106-
function cutoff_weights(w::AbstractMatrix{U}, cutoff::R) where {U<:Real, R<:Real}
107-
wnew = copy(w)
108-
for j in 1:size(w,2)
109-
for i in 1:size(w,1)
110-
if wnew[i,j] < cutoff
111-
wnew[i,j] = zero(U)
112-
end
113-
end
114-
end
115-
wnew
116-
end
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
"""
2+
AbstractMaximumWeightMaximalMatchingAlgorithm
3+
Abstract type that allows users to pass in their preferred algorithm
4+
"""
5+
abstract type AbstractMaximumWeightMaximalMatchingAlgorithm end
6+
7+
"""
8+
LPAlgorithm <: AbstractMaximumWeightMaximalMatchingAlgorithm
9+
Forces the maximum_weight_maximal_matching function to use a linear programming formulation.
10+
"""
11+
struct LPAlgorithm <: AbstractMaximumWeightMaximalMatchingAlgorithm end
12+
13+
function maximum_weight_maximal_matching(
14+
g::Graph,
15+
w::AbstractMatrix{T},
16+
algorithm::LPAlgorithm,
17+
solver = nothing
18+
) where {T<:Real}
19+
if ! isa(solver, AbstractMathProgSolver)
20+
error("The keyword argument solver must be an AbstractMathProgSolver, as accepted by JuMP.")
21+
end
22+
23+
return maximum_weight_maximal_matching_lp(g, solver, w)
24+
end
25+
26+
"""
27+
HungarianAlgorithm <: AbstractMaximumWeightMaximalMatchingAlgorithm
28+
Forces the maximum_weight_maximal_matching function to use the Hungarian algorithm.
29+
"""
30+
struct HungarianAlgorithm <: AbstractMaximumWeightMaximalMatchingAlgorithm end
31+
32+
function maximum_weight_maximal_matching(
33+
g::Graph,
34+
w::AbstractMatrix{T},
35+
algorithm::HungarianAlgorithm,
36+
solver = nothing
37+
) where {T<:Real}
38+
return maximum_weight_maximal_matching_hungarian(g, w)
39+
end
40+
41+
"""
42+
maximum_weight_maximal_matching{T<:Real}(g, w::Dict{Edge,T})
43+
44+
Given a bipartite graph `g` and an edge map `w` containing weights associated to edges,
45+
returns a matching with the maximum total weight among the ones containing the
46+
greatest number of edges.
47+
48+
Edges in `g` not present in `w` will not be considered for the matching.
49+
50+
A `cutoff` keyword argument can be given, to reduce computational times
51+
excluding edges with weights lower than the cutoff.
52+
53+
Finally, a specific algorithm can be chosen (`algorithm` keyword argument);
54+
each algorithm has specific dependencies. For instance:
55+
56+
- If `algorithm=HungarianAlgorithm()` (the default), the package Hungarian.jl is used.
57+
This algorithm is always polynomial in time, with complexity O(n³).
58+
- If `algorithm=LPAlgorithm()`, the package JuMP.jl and one of its supported solvers is required.
59+
In this case, the algorithm relies on a linear relaxation on of the matching problem, which is
60+
guaranteed to have integer solution on bipartite graphs. A solver must be provided with
61+
the `solver` keyword parameter.
62+
63+
The returned object is of type `MatchingResult`.
64+
"""
65+
function maximum_weight_maximal_matching(
66+
g::Graph,
67+
w::AbstractMatrix{T};
68+
cutoff = nothing,
69+
algorithm::AbstractMaximumWeightMaximalMatchingAlgorithm = HungarianAlgorithm(),
70+
solver = nothing
71+
) where {T<:Real}
72+
73+
if cutoff != nothing && ! isa(cutoff, Real)
74+
error("The cutoff value must be of type Real or nothing.")
75+
end
76+
77+
if cutoff != nothing
78+
return maximum_weight_maximal_matching(g, cutoff_weights(w, cutoff), algorithm, solver)
79+
else
80+
return maximum_weight_maximal_matching(g, w, algorithm, solver)
81+
end
82+
end
83+
84+
"""
85+
cutoff_weights copies the weight matrix with all elements below cutoff set to 0
86+
"""
87+
function cutoff_weights(w::AbstractMatrix{T}, cutoff::R) where {T<:Real, R<:Real}
88+
wnew = copy(w)
89+
for j in 1:size(w,2)
90+
for i in 1:size(w,1)
91+
if wnew[i,j] < cutoff
92+
wnew[i,j] = zero(T)
93+
end
94+
end
95+
end
96+
wnew
97+
end
98+
99+
@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)

test/runtests.jl

Lines changed: 45 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,6 @@ using LinearAlgebra: I
66

77
@testset "LightGraphsMatching" begin
88

9-
g = CompleteGraph(4)
10-
w = LightGraphsMatching.default_weights(g)
11-
@test all((w + w') .≈ ones(4,4) - Matrix(I, 4,4))
12-
13-
w1 = [
14-
1 3
15-
5 1
16-
]
17-
w0 = [
18-
0 3
19-
5 0
20-
]
21-
@test all(w0 .≈ LightGraphsMatching.cutoff_weights(w1, 2))
22-
239
@testset "maximum_weight_matching" begin
2410
g = CompleteGraph(3)
2511
w = [
@@ -105,7 +91,7 @@ end
10591
w[1,4] = 1.
10692
w[2,3] = 2.
10793
w[2,4] = 11.
108-
match = maximum_weight_maximal_matching(g, CbcSolver(), w)
94+
match = maximum_weight_maximal_matching(g, w, algorithm=LPAlgorithm(), solver=CbcSolver())
10995
@test match.weight 21
11096
@test match.mate[1] == 3
11197
@test match.mate[3] == 1
@@ -118,7 +104,7 @@ end
118104
w[1,4] = 0.5
119105
w[2,3] = 11
120106
w[2,4] = 1
121-
match = maximum_weight_maximal_matching(g, CbcSolver(), w)
107+
match = maximum_weight_maximal_matching(g, w, algorithm=LPAlgorithm(), solver=CbcSolver())
122108
@test match.weight 11.5
123109
@test match.mate[1] == 4
124110
@test match.mate[4] == 1
@@ -133,7 +119,7 @@ end
133119
w[2,4] = 1
134120
w[2,5] = -1
135121
w[2,6] = -1
136-
match = maximum_weight_maximal_matching(g,CbcSolver(),w,0)
122+
match = maximum_weight_maximal_matching(g, w, algorithm=LPAlgorithm(), solver=CbcSolver(), cutoff=0)
137123
@test match.weight 11.5
138124
@test match.mate[1] == 4
139125
@test match.mate[4] == 1
@@ -148,14 +134,53 @@ end
148134
w[1,6] = 1
149135
w[1,5] = -1
150136

151-
match = maximum_weight_maximal_matching(g,CbcSolver(),w,0)
137+
match = maximum_weight_maximal_matching(g, w, algorithm=LPAlgorithm(), solver=CbcSolver(), cutoff=0)
152138
@test match.weight 12
153139
@test match.mate[1] == 6
154140
@test match.mate[2] == 5
155141
@test match.mate[3] == -1
156142
@test match.mate[4] == -1
157143
@test match.mate[5] == 2
158144
@test match.mate[6] == 1
145+
146+
147+
g = CompleteBipartiteGraph(2, 2)
148+
w = zeros(4, 4)
149+
w[1, 3] = 10.
150+
w[1, 4] = 1.
151+
w[2, 3] = 2.
152+
w[2, 4] = 11.
153+
match = maximum_weight_maximal_matching(g, w, algorithm=HungarianAlgorithm())
154+
@test match.weight 21
155+
@test match.mate[1] == 3
156+
@test match.mate[3] == 1
157+
@test match.mate[2] == 4
158+
@test match.mate[4] == 2
159+
160+
g = CompleteGraph(3)
161+
w = zeros(3, 3)
162+
@test ! is_bipartite(g)
163+
@test_throws ErrorException maximum_weight_maximal_matching(g, w, algorithm=HungarianAlgorithm())
164+
165+
g = CompleteBipartiteGraph(2, 4)
166+
w = zeros(6, 6)
167+
w[1, 3] = 10
168+
w[1, 4] = 0.5
169+
w[2, 3] = 11
170+
w[2, 4] = 1
171+
match = maximum_weight_maximal_matching(g, w, algorithm=HungarianAlgorithm())
172+
@test match.weight 11.5
173+
174+
g = Graph(4)
175+
add_edge!(g, 1, 3)
176+
add_edge!(g, 1, 4)
177+
add_edge!(g, 2, 4)
178+
w = zeros(4, 4)
179+
w[1, 3] = 1
180+
w[1, 4] = 3
181+
w[2, 4] = 1
182+
match = maximum_weight_maximal_matching(g, w, algorithm=HungarianAlgorithm())
183+
@test match.weight 2
159184

160185
end
161186

@@ -214,8 +239,8 @@ end
214239
@test match.weight -11.5
215240

216241

217-
g =CompleteGraph(4)
218-
w =Dict{Edge,Float64}()
242+
g = CompleteGraph(4)
243+
w = Dict{Edge,Float64}()
219244
w[Edge(1,3)] = 10
220245
w[Edge(1,4)] = 0.5
221246
w[Edge(2,3)] = 11

0 commit comments

Comments
 (0)