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

[WIP] Feature cardinality matching #18

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
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
3 changes: 2 additions & 1 deletion src/LightGraphsMatching.jl
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const MOI = MathOptInterface
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, HungarianAlgorithm, LPAlgorithm
export MatchingResult, maximum_cardinality_matching, maximum_weight_matching, maximum_weight_maximal_matching, minimum_weight_perfect_matching, HungarianAlgorithm, LPAlgorithm

"""
struct MatchingResult{U}
Expand All @@ -31,6 +31,7 @@ struct MatchingResult{U<:Real}
end

include("lp.jl")
include("maximum_cardinality_matching.jl")
include("maximum_weight_matching.jl")
include("blossomv.jl")
include("hungarian.jl")
Expand Down
118 changes: 118 additions & 0 deletions src/maximum_cardinality_matching.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
"""
maximum_cardinality_matching(g::Graph)
Wikunia marked this conversation as resolved.
Show resolved Hide resolved

Given a graph `g` returns a maximum cardinality matching.
This is the same as running `maximum_weight_matching` with default weights (`1`)
but is faster without needing a JuMP solver.

Returns MatchingResult containing:
- the maximum cardinality that can be achieved
- a list of each vertex's match (or -1 for unmatched vertices)
"""
function maximum_cardinality_matching(g::AbstractGraph{U}) where U<:Integer
Wikunia marked this conversation as resolved.
Show resolved Hide resolved
n = nv(g)
matching = -ones(Int, n)
Wikunia marked this conversation as resolved.
Show resolved Hide resolved

# the number of edges that can possibly be part of a matching
max_generally_possible = fld(n, 2)

# get initial matching
matching_len = 0
for e in edges(g)
# if unmatched
if matching[e.src] == -1 && matching[e.dst] == -1
matching[e.src] = e.dst
matching[e.dst] = e.src
matching_len += 1
end
end
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe it would be a good idea, to have this as a separate function? Then it could also be used for finding a greed maximal cardinality matching and be used by other functions. We could pass it in as an optional argument or a keyword argument.

Furthermore, I was thinking, maybe it would be more efficient to loop over the vertices and then over all neighbors of that vertex? I.e something like

for u in vertices(g)
   matching[u] == -1 || continue
   for v in neighbors[u]
      u <= v && continue
      matching[u] = v
      matching[v] = u
     matching_len += 1
   end
end

This approach only works, if we assume that vertices(g) is ordered in increasing order, but it can also be adapted, such that the increasing order is not required.

Also, be careful with self-loops, these should never be in a matching.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If neighbors is implemented in a fast manner yes. Thought that looping over the edges might be faster than accessing the neighbors but yours has an earlier break. Maybe I can test it on a larger graph. Good point with self loops!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think for SimpleGraphthe edges iterator is implemented using neighbors anyway.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is now a separate function but still uses edges for now.


# if there are at least two free vertices
if matching_len < max_generally_possible
Wikunia marked this conversation as resolved.
Show resolved Hide resolved

parents = zeros(Int, n)
visited = falses(n)

@inbounds while matching_len < max_generally_possible
cur_level = Vector{Int}()
sizehint!(cur_level, n)
next_level = Vector{Int}()
sizehint!(next_level, n)
Wikunia marked this conversation as resolved.
Show resolved Hide resolved

# get starting free vertex
free_vertex = 0
found = false
for v in vertices(g)
if matching[v] == -1
visited[v] = true
free_vertex = v
push!(cur_level, v)

level = 1
found = false
# find augmenting path
while !isempty(cur_level)
for v in cur_level

if level % 2 == 1
for t in outneighbors(g, v)
# found an augmenting path if connected to a free vertex
if matching[t] == -1
# traverse the augmenting path backwards and change the matching
current_src = t
current_dst = v
back_level = 1
while current_dst != 0
# add every second edge to the matching (this also overwrites the current matching)
if back_level % 2 == 1
Wikunia marked this conversation as resolved.
Show resolved Hide resolved
matching[current_src] = current_dst
matching[current_dst] = current_src
end
current_src = current_dst
current_dst = parents[current_dst]
back_level += 1
end
# added exactly one edge to the matching
matching_len += 1
# terminate current search
found = true
break
end

# use a non matching edge
if !visited[t] && matching[v] != t
visited[t] = true
parents[t] = v
push!(next_level, t)
end
end
found && break
else # use a matching edge
t = matching[v]
if !visited[t]
visited[t] = true
parents[t] = v
push!(next_level, t)
end
end
end

empty!(cur_level)
cur_level, next_level = next_level, cur_level

level += 1
found && break
end # end finding augmenting path
found && break
parents .= 0
visited .= false
end
end
# checked all free vertices:
# no augmenting path found => no better matching
!found && break
end
end

return MatchingResult(matching_len, matching)
end
73 changes: 73 additions & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -256,5 +256,78 @@ end
@test match.weight ≈ 11.5
end

@testset "maximum_cardinality_matching" begin
# graph a in https://en.wikipedia.org/wiki/Matching_(graph_theory)#/media/File:Maximum-matching-labels.svg
g = Graph(6)
add_edge!(g, 1, 2)
add_edge!(g, 1, 6)
add_edge!(g, 2, 3)
add_edge!(g, 2, 4)
add_edge!(g, 2, 5)
add_edge!(g, 2, 6)

mr = maximum_cardinality_matching(g)
@test mr.weight == 2
@test mr.mate[1] == 6
@test 3 <= mr.mate[2] <= 5
@test mr.mate[mr.mate[2]] == 2
@test mr.mate[6] == 1

# graph b in https://en.wikipedia.org/wiki/Matching_(graph_theory)#/media/File:Maximum-matching-labels.svg
g = Graph(Int8(6))
add_edge!(g, 1, 2)
add_edge!(g, 1, 4)
add_edge!(g, 2, 3)
add_edge!(g, 2, 4)
add_edge!(g, 3, 5)
add_edge!(g, 4, 5)
add_edge!(g, 5, 6)

mr = maximum_cardinality_matching(g)
@test mr.weight == 3
@test mr.mate[1] == 4
@test mr.mate[2] == 3
@test mr.mate[3] == 2
@test mr.mate[4] == 1
@test mr.mate[5] == 6
@test mr.mate[6] == 5

# graph c in https://en.wikipedia.org/wiki/Matching_(graph_theory)#/media/File:Maximum-matching-labels.svg
g = Graph(5)
add_edge!(g, 1, 2)
add_edge!(g, 1, 5)
add_edge!(g, 2, 3)
add_edge!(g, 2, 5)
add_edge!(g, 3, 4)
add_edge!(g, 4, 5)

mr = maximum_cardinality_matching(g)
@test mr.weight == 2

g = Graph()
add_edge!(g, 1, 2)
add_edge!(g, 1, 4)
add_edge!(g, 2, 3)
add_edge!(g, 2, 4)
add_edge!(g, 3, 5)
add_edge!(g, 4, 5)
add_edge!(g, 5, 6)

# same as in maximum_weight_matching
g = Graph(4)
add_edge!(g, 1,2)
add_edge!(g, 2,3)
add_edge!(g, 3,1)
add_edge!(g, 3,4)

match = maximum_cardinality_matching(g)
@test match.weight == 2
@test match.mate[1] == 2
@test match.mate[2] == 1
@test match.mate[3] == 4
@test match.mate[4] == 3


end

end