Skip to content

Commit 6311244

Browse files
committed
initial transposition
1 parent 541042b commit 6311244

File tree

6 files changed

+468
-3
lines changed

6 files changed

+468
-3
lines changed

REQUIRE

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,6 @@
11
julia 0.6
2+
LightGraphs 0.9.0
3+
JuMP 0.13.2
4+
MatrixDepot
5+
BlossomV 0.1
6+
GLPKMathProgInterface 0.3.2

src/LightGraphsMatching.jl

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,34 @@
1+
__precompile__(true)
12
module LightGraphsMatching
3+
using LightGraphs
4+
export MatchingResult, maximum_weight_matching, maximum_weight_maximal_matching, minimum_weight_perfect_matching
25

3-
# package code goes here
6+
"""
7+
type MatchingResult{T}
8+
weight::T
9+
mate::Vector{Int}
10+
end
11+
12+
A type representing the result of a matching algorithm.
13+
14+
weight: total weight of the matching
15+
16+
mate: `mate[i] = j` if vertex `i` is matched to vertex `j`.
17+
`mate[i] = -1` for unmatched vertices.
18+
"""
19+
struct MatchingResult{T<:Real}
20+
weight::T
21+
mate::Vector{Int}
22+
end
23+
24+
import BlossomV
25+
include("blossomv.jl")
26+
27+
using GLPKMathProgInterface: GLPKSolverMIP
28+
29+
using JuMP
30+
include("lp.jl")
31+
include("maximum_weight_matching.jl")
432

533
end # module
34+

src/blossomv.jl

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
"""
2+
minimum_weight_perfect_matching{T<:Real}(g, w::Dict{Edge,T})
3+
minimum_weight_perfect_matching{T<:Real}(g, w::Dict{Edge,T}, cutoff)
4+
5+
Given a graph `g` and an edgemap `w` containing weights associated to edges,
6+
returns a matching with the mimimum total weight among the ones containing
7+
exactly `nv(g)/2` edges.
8+
9+
Edges in `g` not present in `w` will not be considered for the matching.
10+
11+
This function relies on the BlossomV.jl package, a julia wrapper
12+
around Kolmogorov's BlossomV algorithm.
13+
14+
Eventually a `cutoff` argument can be given, to the reduce computational time
15+
excluding edges with weights higher than the cutoff.
16+
17+
The returned object is of type `MatchingResult`.
18+
19+
In case of error try to change the optional argument `tmaxscale` (default is `tmaxscale=10`).
20+
"""
21+
function minimum_weight_perfect_matching end
22+
23+
function minimum_weight_perfect_matching(g::Graph, w::Dict{E,T}, cutoff, kws...) where {T<:Real, E<:Edge}
24+
wnew = Dict{E, T}()
25+
for (e, c) in w
26+
if c <= cutoff
27+
wnew[e] = c
28+
end
29+
end
30+
return minimum_weight_perfect_matching(g, wnew; kws...)
31+
end
32+
33+
function minimum_weight_perfect_matching(g::Graph, w::Dict{E,T}; tmaxscale=10.) where {T<:AbstractFloat, E<:Edge}
34+
wnew = Dict{E, Int32}()
35+
cmax = maximum(values(w))
36+
cmin = minimum(values(w))
37+
tmax = typemax(Int32) / tmaxscale # /10 is kinda arbitrary,
38+
# hopefully high enough to not incurr in overflow problems
39+
for (e, c) in w
40+
wnew[e] = round(Int32, (c-cmin) / (cmax-cmin) * tmax)
41+
end
42+
match = minimum_weight_perfect_matching(g, wnew)
43+
weight = T(0)
44+
for i=1:nv(g)
45+
j = match.mate[i]
46+
if j > i
47+
weight += w[E(i,j)]
48+
end
49+
end
50+
return MatchingResult(weight, match.mate)
51+
end
52+
53+
function minimum_weight_perfect_matching(g::Graph, w::Dict{E,T}) where {T<:Integer, E<:Edge}
54+
m = BlossomV.Matching(nv(g))
55+
for (e, c) in w
56+
BlossomV.add_edge(m, src(e)-1, dst(e)-1, c)
57+
end
58+
BlossomV.solve(m)
59+
60+
mate = fill(-1, nv(g))
61+
totweight = T(0)
62+
for i=1:nv(g)
63+
j = BlossomV.get_match(m, i-1) + 1
64+
mate[i] = j <= 0 ? -1 : j
65+
if i < j
66+
totweight += w[Edge(i,j)]
67+
end
68+
end
69+
return MatchingResult(totweight, mate)
70+
end

src/lp.jl

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
"""
2+
maximum_weight_maximal_matching{T<:Real}(g, w::Dict{Edge,T})
3+
maximum_weight_maximal_matching{T<:Real}(g, w::Dict{Edge,T}, 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, w::Dict{Edge,T}, cutoff; solver = GLPKSolverMIP) where {T<:Real}
24+
wnew = Dict{Edge,T}()
25+
for (e,x) in w
26+
if x >= cutoff
27+
wnew[e] = x
28+
end
29+
end
30+
31+
return maximum_weight_maximal_matching(g, wnew)
32+
end
33+
34+
35+
function maximum_weight_maximal_matching(g::Graph, w::Dict{Edge,T}; solver = GLPKSolverMIP) where {T<:Real}
36+
# TODO support for graphs with zero degree nodes
37+
# TODO apply separately on each connected component
38+
bpmap = bipartite_map(g)
39+
length(bpmap) != nv(g) && error("Graph is not bipartite")
40+
v1 = findin(bpmap, 1)
41+
v2 = findin(bpmap, 2)
42+
if length(v1) > length(v2)
43+
v1, v2 = v2, v1
44+
end
45+
46+
nedg = 0
47+
edgemap = Dict{Edge,Int}()
48+
for (e,_) in w
49+
nedg += 1
50+
edgemap[e] = nedg
51+
edgemap[reverse(e)] = nedg
52+
end
53+
54+
model = Model(solver=solver())
55+
@variable(model, x[1:length(w)] >= 0)
56+
57+
for i in v1
58+
idx = Vector{Int}()
59+
for j in neighbors(g, i)
60+
if haskey(edgemap, Edge(i,j))
61+
push!(idx, edgemap[Edge(i,j)])
62+
end
63+
end
64+
if length(idx) > 0
65+
@constraint(model, sum{x[id], id=idx} == 1)
66+
end
67+
end
68+
69+
for j in v2
70+
idx = Vector{Int}()
71+
for i in neighbors(g, j)
72+
if haskey(edgemap, Edge(i,j))
73+
push!(idx, edgemap[Edge(i,j)])
74+
end
75+
end
76+
77+
if length(idx) > 0
78+
@constraint(model, sum{x[id], id=idx} <= 1)
79+
end
80+
end
81+
82+
@objective(model, Max, sum{c * x[edgemap[e]], (e,c)=w})
83+
84+
status = solve(model)
85+
status != :Optimal && error("JuMP solver failed to find optimal solution.")
86+
sol = getvalue(x)
87+
88+
all(Bool[s == 1 || s == 0 for s in sol]) || error("Found non-integer solution.")
89+
90+
cost = getobjectivevalue(model)
91+
92+
mate = fill(-1, nv(g))
93+
for e in edges(g)
94+
if haskey(w, e)
95+
inmatch = convert(Bool, sol[edgemap[e]])
96+
if inmatch
97+
mate[src(e)] = dst(e)
98+
mate[dst(e)] = src(e)
99+
end
100+
end
101+
end
102+
103+
return MatchingResult(cost, mate)
104+
end

src/maximum_weight_matching.jl

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
"""
2+
maximum_weight_matching{T <:Real}(g::Graph, w::Dict{Edge,T} = Dict{Edge,Int64}())
3+
4+
Given a graph `g` and an edgemap `w` containing weights associated to edges,
5+
returns a matching with the maximum total weight.
6+
`w` is a dictionary that maps edges i => j to weights.
7+
If no weight parameter is given, all edges will be considered to have weight 1
8+
(results in max cardinality matching)
9+
10+
The efficiency of the algorithm depends on the input graph:
11+
- If the graph is bipartite, then the LP relaxation is integral.
12+
- If the graph is not bipartite, then it requires a MIP solver and
13+
the computation time may grow exponentially.
14+
15+
The package JuMP.jl and one of its supported solvers is required.
16+
17+
Returns MatchingResult containing:
18+
- a solve status (indicating whether the problem was solved to optimality)
19+
- the optimal cost
20+
- a list of each vertex's match (or -1 for unmatched vertices)
21+
"""
22+
function maximum_weight_matching end
23+
24+
function maximum_weight_matching(g::Graph,
25+
w::Dict{Edge,T} = Dict{Edge,Int64}(i => 1 for i in collect(edges(g)));
26+
solver = GLPKSolverMIP) where {T <:Real}
27+
28+
model = Model(solver = solver())
29+
n = nv(g)
30+
edge_list = collect(edges(g))
31+
32+
# put the edge weights in w in the right order to be compatible with edge_list
33+
for edge in keys(w)
34+
redge = reverse(edge)
35+
if !is_ordered(edge) && !haskey(w, redge) # replace i=>j by j=>i if necessary.
36+
w[redge] = w[edge]
37+
end
38+
end
39+
40+
if setdiff(edge_list, keys(w)) != [] # If some edges do not have a key in w.
41+
error("Some edge weights are missing, check that keys i => j in w satisfy i <= j")
42+
end
43+
44+
if is_bipartite(g)
45+
@variable(model, x[edge_list] >= 0) # no need to enforce integrality
46+
else
47+
@variable(model, x[edge_list] >= 0, Int) # requires MIP solver
48+
end
49+
@objective(model, Max, sum(x[edge]*w[edge] for edge in edge_list))
50+
@constraint(model, c1[i=1:n],
51+
sum(x[Edge(i,j)] for j=filter(l -> l > i, neighbors(g,i))) +
52+
sum(x[Edge(j,i)] for j=filter(l -> l <= i, neighbors(g,i)))
53+
<= 1)
54+
55+
status = solve(model)
56+
solution = getvalue(x)
57+
cost = getobjectivevalue(model)
58+
## TODO: add the option of returning the solve status as part of the MatchingResult type.
59+
return MatchingResult(cost, dict_to_arr(n, solution))
60+
end
61+
62+
""" Returns an array of mates from a dictionary that maps edges to {0,1} """
63+
function dict_to_arr(n::Int64, solution::JuMP.JuMPArray{T,1,Tuple{Array{E,1}}}) where {T<: Real, E<: Edge}
64+
mate = fill(-1,n)
65+
for i in keys(solution)
66+
key = i[1] # i is a tuple with 1 element.
67+
if solution[key] >= 1 - 1e-5 # Some tolerance to numerical approximations by the solver.
68+
mate[src(key)] = dst(key)
69+
mate[dst(key)] = src(key)
70+
end
71+
end
72+
return mate
73+
end

0 commit comments

Comments
 (0)