From bc290356b1576428183d415946a540dc032ccee9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ole=20Kr=C3=B6ger?= Date: Sun, 17 Nov 2019 10:23:56 +0100 Subject: [PATCH 1/3] first version of cardinality matching --- src/maximum_cardinality_matching.jl | 146 ++++++++++++++++++++++++++++ test/runtests.jl | 19 ++++ 2 files changed, 165 insertions(+) create mode 100644 src/maximum_cardinality_matching.jl diff --git a/src/maximum_cardinality_matching.jl b/src/maximum_cardinality_matching.jl new file mode 100644 index 0000000..361097b --- /dev/null +++ b/src/maximum_cardinality_matching.jl @@ -0,0 +1,146 @@ +""" +maximum_cardinality_matching(g::Graph) + +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) +""" + +struct MatchingResult{U<:Real} + weight::U + mate::Vector{Int} +end + +function maximum_cardinality_matching(g::AbstractGraph{U}) where U<:Integer + n = nv(g) + matching = zeros(U, n) + + # 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] == zero(U) && matching[e.dst] == zero(U) + matching[e.src] = e.dst + matching[e.dst] = e.src + matching_len += 1 + end + end + + # if there are at least two free vertices + if matching_len < max_generally_possible + + parents = zeros(U, n) + visited = falses(n) + + @inbounds while matching_len < max_generally_possible + cur_level = Vector{U}() + sizehint!(cur_level, n) + next_level = Vector{U}() + sizehint!(next_level, n) + + # get starting free vertex + free_vertex = 0 + found = false + for v in vertices(g) + if matching[v] == zero(U) + 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 + # use a non matching edge + if level % 2 == 1 + for t in outneighbors(g, v) + # found an augmenting path + if matching[t] == zero(U) + current_src = t + current_dst = v + while current_dst != zero(U) + matching[current_src] = current_dst + matching[current_dst] = current_src + current_src = current_dst + current_dst = parents[current_dst] + end + matching_len += 1 + found = true + break + end + 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 .= zero(U) + visited .= false + end + end + # no augmenting path found => no better matching + !found && break + end + end + + return MatchingResult(matching_len, matching) +end + +#= +g = Graph(12) +add_edge!(g, 1, 8) +add_edge!(g, 1, 9) +add_edge!(g, 1,12) +add_edge!(g, 2, 7) +add_edge!(g, 3, 8) +add_edge!(g, 3, 9) +add_edge!(g, 3,10) +add_edge!(g, 3,11) +add_edge!(g, 4,10) +add_edge!(g, 4,11) +add_edge!(g, 5,12) +add_edge!(g, 6, 8) +add_edge!(g, 6,12) + +maximum_cardinality_matching(g) +=# + +function wikipedia() + 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) + println(typeof(mr.mate)) +end \ No newline at end of file diff --git a/test/runtests.jl b/test/runtests.jl index fa0853f..91afceb 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -255,5 +255,24 @@ end @test match.weight ≈ 11.5 end +@testset "maximum_cardinality_matching" + g = Graph(12) + add_edge!(g, 1, 8) + add_edge!(g, 1, 9) + add_edge!(g, 1,12) + add_edge!(g, 2, 7) + add_edge!(g, 3, 8) + add_edge!(g, 3, 9) + add_edge!(g, 3,10) + add_edge!(g, 3,11) + add_edge!(g, 4,10) + add_edge!(g, 4,11) + add_edge!(g, 5,12) + add_edge!(g, 6,11) + add_edge!(g, 6,12) + + # TODO: + # match = maximum_weight_matching(g,Cbc.Optimizer) +end end From 2c610658d23afdf38db673662ae44fa01ead09e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ole=20Kr=C3=B6ger?= Date: Sun, 17 Nov 2019 22:55:02 +0100 Subject: [PATCH 2/3] test cases, bugfix in backtrack path and -1 in matching --- src/LightGraphsMatching.jl | 3 +- src/maximum_cardinality_matching.jl | 76 ++++++++---------------- test/runtests.jl | 90 +++++++++++++++++++++++------ 3 files changed, 98 insertions(+), 71 deletions(-) diff --git a/src/LightGraphsMatching.jl b/src/LightGraphsMatching.jl index cff164e..c62abc6 100644 --- a/src/LightGraphsMatching.jl +++ b/src/LightGraphsMatching.jl @@ -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} @@ -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") diff --git a/src/maximum_cardinality_matching.jl b/src/maximum_cardinality_matching.jl index 361097b..fa84493 100644 --- a/src/maximum_cardinality_matching.jl +++ b/src/maximum_cardinality_matching.jl @@ -9,15 +9,9 @@ Returns MatchingResult containing: - the maximum cardinality that can be achieved - a list of each vertex's match (or -1 for unmatched vertices) """ - -struct MatchingResult{U<:Real} - weight::U - mate::Vector{Int} -end - function maximum_cardinality_matching(g::AbstractGraph{U}) where U<:Integer n = nv(g) - matching = zeros(U, n) + matching = -ones(Int, n) # the number of edges that can possibly be part of a matching max_generally_possible = fld(n, 2) @@ -26,7 +20,7 @@ function maximum_cardinality_matching(g::AbstractGraph{U}) where U<:Integer matching_len = 0 for e in edges(g) # if unmatched - if matching[e.src] == zero(U) && matching[e.dst] == zero(U) + if matching[e.src] == -1 && matching[e.dst] == -1 matching[e.src] = e.dst matching[e.dst] = e.src matching_len += 1 @@ -36,20 +30,20 @@ function maximum_cardinality_matching(g::AbstractGraph{U}) where U<:Integer # if there are at least two free vertices if matching_len < max_generally_possible - parents = zeros(U, n) + parents = zeros(Int, n) visited = falses(n) @inbounds while matching_len < max_generally_possible - cur_level = Vector{U}() + cur_level = Vector{Int}() sizehint!(cur_level, n) - next_level = Vector{U}() + next_level = Vector{Int}() sizehint!(next_level, n) # get starting free vertex free_vertex = 0 found = false for v in vertices(g) - if matching[v] == zero(U) + if matching[v] == -1 visited[v] = true free_vertex = v push!(cur_level, v) @@ -59,23 +53,33 @@ function maximum_cardinality_matching(g::AbstractGraph{U}) where U<:Integer # find augmenting path while !isempty(cur_level) for v in cur_level - # use a non matching edge + if level % 2 == 1 for t in outneighbors(g, v) - # found an augmenting path - if matching[t] == zero(U) + # 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 - while current_dst != zero(U) - matching[current_src] = current_dst - matching[current_dst] = current_src + 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 + 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 @@ -100,10 +104,11 @@ function maximum_cardinality_matching(g::AbstractGraph{U}) where U<:Integer found && break end # end finding augmenting path found && break - parents .= zero(U) + parents .= 0 visited .= false end end + # checked all free vertices: # no augmenting path found => no better matching !found && break end @@ -111,36 +116,3 @@ function maximum_cardinality_matching(g::AbstractGraph{U}) where U<:Integer return MatchingResult(matching_len, matching) end - -#= -g = Graph(12) -add_edge!(g, 1, 8) -add_edge!(g, 1, 9) -add_edge!(g, 1,12) -add_edge!(g, 2, 7) -add_edge!(g, 3, 8) -add_edge!(g, 3, 9) -add_edge!(g, 3,10) -add_edge!(g, 3,11) -add_edge!(g, 4,10) -add_edge!(g, 4,11) -add_edge!(g, 5,12) -add_edge!(g, 6, 8) -add_edge!(g, 6,12) - -maximum_cardinality_matching(g) -=# - -function wikipedia() - 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) - println(typeof(mr.mate)) -end \ No newline at end of file diff --git a/test/runtests.jl b/test/runtests.jl index 764b4e8..1c1e7a8 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -256,24 +256,78 @@ end @test match.weight ≈ 11.5 end -@testset "maximum_cardinality_matching" - g = Graph(12) - add_edge!(g, 1, 8) - add_edge!(g, 1, 9) - add_edge!(g, 1,12) - add_edge!(g, 2, 7) - add_edge!(g, 3, 8) - add_edge!(g, 3, 9) - add_edge!(g, 3,10) - add_edge!(g, 3,11) - add_edge!(g, 4,10) - add_edge!(g, 4,11) - add_edge!(g, 5,12) - add_edge!(g, 6,11) - add_edge!(g, 6,12) - - # TODO: - # match = maximum_weight_matching(g,Cbc.Optimizer) +@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 From 5cec648b7cccb6e9d91ae93ae1897ba07d8e79a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ole=20Kr=C3=B6ger?= Date: Mon, 18 Nov 2019 19:59:39 +0100 Subject: [PATCH 3/3] function maximal matching + boolean for level --- src/maximum_cardinality_matching.jl | 176 +++++++++++++++------------- 1 file changed, 94 insertions(+), 82 deletions(-) diff --git a/src/maximum_cardinality_matching.jl b/src/maximum_cardinality_matching.jl index fa84493..0c19a1e 100644 --- a/src/maximum_cardinality_matching.jl +++ b/src/maximum_cardinality_matching.jl @@ -1,5 +1,5 @@ """ -maximum_cardinality_matching(g::Graph) +maximum_cardinality_matching(g::AbstractGraph{U}) where {U<:Integer} Given a graph `g` returns a maximum cardinality matching. This is the same as running `maximum_weight_matching` with default weights (`1`) @@ -9,110 +9,122 @@ 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 +function maximum_cardinality_matching(g::AbstractGraph{U}) where {U<:Integer} n = nv(g) - matching = -ones(Int, n) - + init_matching = maximal_matching(g) + + matching = init_matching.mate + matching_len = init_matching.weight + # 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 + # the maximal matching is a maximum matching + if matching_len == max_generally_possible + return init_matching end + # => there are at least two free vertices - # if there are at least two free vertices - if matching_len < max_generally_possible + parents = zeros(Int, n) + visited = falses(n) + + cur_level = Vector{Int}() + sizehint!(cur_level, n) + next_level = Vector{Int}() + sizehint!(next_level, n) - parents = zeros(Int, n) - visited = falses(n) + @inbounds while matching_len < max_generally_possible + # 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) - @inbounds while matching_len < max_generally_possible - cur_level = Vector{Int}() - sizehint!(cur_level, n) - next_level = Vector{Int}() - sizehint!(next_level, n) - - # 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 - matching[current_src] = current_dst - matching[current_dst] = current_src - end - current_src = current_dst - current_dst = parents[current_dst] - back_level += 1 + odd_level = true + found = false + # find augmenting path + while !isempty(cur_level) + for v in cur_level + if odd_level + 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_odd_level = true + while current_dst != 0 + # add every second edge to the matching (this also overwrites the current matching) + if back_odd_level + matching[current_src] = current_dst + matching[current_dst] = current_src 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) + current_src = current_dst + current_dst = parents[current_dst] + back_odd_level = !back_odd_level end + # added exactly one edge to the matching + matching_len += 1 + # terminate current search + found = true + break end - found && break - else # use a matching edge - t = matching[v] - if !visited[t] + + # 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 + empty!(cur_level) + cur_level, next_level = next_level, cur_level - level += 1 - found && break - end # end finding augmenting path + odd_level = !odd_level found && break - parents .= 0 - visited .= false - end + end # end finding augmenting path + found && break + parents .= 0 + visited .= false end - # checked all free vertices: - # no augmenting path found => no better matching - !found && break end + # checked all free vertices: + # no augmenting path found => no better matching + !found && break end return MatchingResult(matching_len, matching) end + +function maximal_matching(g::AbstractGraph{U}) where {U<:Integer} + n = nv(g) + matching = fill(-1, n) + + # get initial matching + matching_len = 0 + for e in edges(g) + # no self loops in matching + e.src == e.dst && continue + # 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 + return MatchingResult(matching_len, matching) +end \ No newline at end of file