From 9c568dba497c99aebb311ab70a02ee2dfc15c5a7 Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Sat, 29 Jul 2023 10:58:10 -0600 Subject: [PATCH 01/21] initial implementation hopcroft-karp matching --- src/Graphs.jl | 6 ++- src/matching/maximum_matching.jl | 88 ++++++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 src/matching/maximum_matching.jl diff --git a/src/Graphs.jl b/src/Graphs.jl index c8d209cf5..2d52ae556 100644 --- a/src/Graphs.jl +++ b/src/Graphs.jl @@ -422,7 +422,10 @@ export independent_set, # vertexcover - vertex_cover + vertex_cover, + + # matching + hopcroft_karp_matching """ Graphs @@ -538,6 +541,7 @@ include("vertexcover/random_vertex_cover.jl") include("Experimental/Experimental.jl") include("Parallel/Parallel.jl") include("Test/Test.jl") +include("matching/maximum_matching.jl") using .LinAlg end # module diff --git a/src/matching/maximum_matching.jl b/src/matching/maximum_matching.jl new file mode 100644 index 000000000..deb897834 --- /dev/null +++ b/src/matching/maximum_matching.jl @@ -0,0 +1,88 @@ +using Graphs + +const UNMATCHED = -1 + +""" +Compute the length of the shortest augmenting path +""" +function _hk_augmenting_bfs!( + graph::Graph, + set1::Vector{Int}, + matching::Dict{Int, Int}, + distance::Dict{Int, Float64}, +)::Bool + # Initialize queue with the unmatched nodes in set1 + queue = Vector{Int}([n for n in set1 if matching[n] == UNMATCHED]) + + distance[UNMATCHED] = Inf + for n in set1 + if matching[n] == UNMATCHED + distance[n] = 0.0 + else + distance[n] = Inf + end + end + + while !isempty(queue) + n1 = popfirst!(queue) + + # If n1 is (a) matched or (b) in set1 + if distance[n1] < Inf && n1 != UNMATCHED + for n2 in neighbors(graph, n1) + # If n2 has not been encountered + if distance[matching[n2]] == Inf + # Give it a distance + distance[matching[n2]] = distance[n1] + 1 + + # Note that n2 could be unmatched + push!(queue, matching[n2]) + end + end + end + end + + found_augmenting_path = (distance[UNMATCHED] < Inf) + # The distance to UNMATCHED is the length of the shortest augmenting path + return found_augmenting_path +end + +""" +Compute augmenting paths +""" +function _hk_dfs!( + graph::Graph, + root::Int, + matching::Dict{Int, Int}, + distance::Dict{Int, Float64}, +)::Bool + if root != UNMATCHED + for n in neighbors(graph, root) + # We traverse along matched edges + if distance[matching[n]] == distance[root] + 1 + if _hk_dfs!(graph, matching[n], matching, distance) + matching[root] = n + matching[n] = root + return true + end + end + end + distance[root] = Inf + return false + end + return true +end + +function hopcroft_karp_matching(graph::Graph, set1::Set) + matching = Dict(n => UNMATCHED for n in vertices(graph)) + set1 = [n for n in vertices(graph) if n in set1] + distance = Dict{Int, Float64}() + while _hk_augmenting_bfs!(graph, set1, matching, distance) + for n1 in set1 + if matching[n1] == UNMATCHED + _hk_dfs!(graph, n1, matching, distance) + end + end + end + matching = Dict(i => j for (i, j) in matching if j != UNMATCHED) + return matching +end From c547d3778ce05f716df260fb28fbfb0e2dfc68fa Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Sat, 29 Jul 2023 13:44:38 -0600 Subject: [PATCH 02/21] add docstring for hopcroft_karp_matching --- src/matching/maximum_matching.jl | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/matching/maximum_matching.jl b/src/matching/maximum_matching.jl index deb897834..9f797c75a 100644 --- a/src/matching/maximum_matching.jl +++ b/src/matching/maximum_matching.jl @@ -72,6 +72,22 @@ function _hk_dfs!( return true end +""" + hopcroft_karp_matching(graph::Graph, set1::Set)::Dict + +Compute a maximum-cardinality matching of a bipartite graph via the +[Hopcroft-Karp algorithm](https://en.wikipedia.org/wiki/Hopcroft-Karp_algorithm). + +The return type is a dict mapping nodes in `set1` to other nodes *and* other +nodes back to their matched nodes in `set`. + +### Arguments + +* `graph`: The bipartite `Graph` for which a maximum matching is computed + +* `set1`: A set of vertices in a bipartition + +""" function hopcroft_karp_matching(graph::Graph, set1::Set) matching = Dict(n => UNMATCHED for n in vertices(graph)) set1 = [n for n in vertices(graph) if n in set1] From 2bb43ff000f6bc64b0c84fcf14e7eb768426517e Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Sat, 29 Jul 2023 14:08:15 -0600 Subject: [PATCH 03/21] add documentation for hopcroft-karp matching --- docs/make.jl | 1 + docs/src/algorithms/matching.md | 15 +++++++++++++++ 2 files changed, 16 insertions(+) create mode 100644 docs/src/algorithms/matching.md diff --git a/docs/make.jl b/docs/make.jl index d7f62b704..e82bffed5 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -60,6 +60,7 @@ pages_files = [ "algorithms/editdist.md", "algorithms/independentset.md", "algorithms/linalg.md", + "algorithms/matching.md", "algorithms/shortestpaths.md", "algorithms/spanningtrees.md", "algorithms/steinertree.md", diff --git a/docs/src/algorithms/matching.md b/docs/src/algorithms/matching.md new file mode 100644 index 000000000..a08d3c93c --- /dev/null +++ b/docs/src/algorithms/matching.md @@ -0,0 +1,15 @@ +# Matchings + +_Graphs.jl_ contains functions related to matchings. + +## Index + +```@index +Pages = ["matching.md"] +``` + +## Full docs + +```@docs +hopcroft_karp_matching +``` From 2a4f664475735ce3bc32d6d37d17ea28a83af0a3 Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Sat, 29 Jul 2023 14:21:12 -0600 Subject: [PATCH 04/21] add note on runtime performance to docstring --- src/matching/maximum_matching.jl | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/matching/maximum_matching.jl b/src/matching/maximum_matching.jl index 9f797c75a..74d4c7cd6 100644 --- a/src/matching/maximum_matching.jl +++ b/src/matching/maximum_matching.jl @@ -81,6 +81,11 @@ Compute a maximum-cardinality matching of a bipartite graph via the The return type is a dict mapping nodes in `set1` to other nodes *and* other nodes back to their matched nodes in `set`. +### Performance +The algorithms runs in O((m + n)n^0.5), where n is the number of vertices and +m is the number of edges. As it does not assume the number of edges is O(n^2), +this algorithm is particularly effective for sparse bipartite graphs. + ### Arguments * `graph`: The bipartite `Graph` for which a maximum matching is computed From c822bf537f49d74c8277cc0b7feb2adabc2a312e Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Sat, 29 Jul 2023 14:55:57 -0600 Subject: [PATCH 05/21] update hopcroft_karp_matching to not require a bipartite set --- src/matching/maximum_matching.jl | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/matching/maximum_matching.jl b/src/matching/maximum_matching.jl index 74d4c7cd6..941983a2f 100644 --- a/src/matching/maximum_matching.jl +++ b/src/matching/maximum_matching.jl @@ -73,13 +73,14 @@ function _hk_dfs!( end """ - hopcroft_karp_matching(graph::Graph, set1::Set)::Dict + hopcroft_karp_matching(graph::Graph)::Dict Compute a maximum-cardinality matching of a bipartite graph via the [Hopcroft-Karp algorithm](https://en.wikipedia.org/wiki/Hopcroft-Karp_algorithm). -The return type is a dict mapping nodes in `set1` to other nodes *and* other -nodes back to their matched nodes in `set`. +The return type is a dict mapping nodes to nodes. All matched nodes are included +as keys. For exmaple, if `i` is matched with `j`, `i => j` and `j => i` are both +included in the returned dict. ### Performance The algorithms runs in O((m + n)n^0.5), where n is the number of vertices and @@ -90,12 +91,11 @@ this algorithm is particularly effective for sparse bipartite graphs. * `graph`: The bipartite `Graph` for which a maximum matching is computed -* `set1`: A set of vertices in a bipartition - """ -function hopcroft_karp_matching(graph::Graph, set1::Set) +function hopcroft_karp_matching(graph::Graph) + bmap = bipartite_map(graph) + set1 = [n for n in vertices(graph) if bmap[n] == 1] matching = Dict(n => UNMATCHED for n in vertices(graph)) - set1 = [n for n in vertices(graph) if n in set1] distance = Dict{Int, Float64}() while _hk_augmenting_bfs!(graph, set1, matching, distance) for n1 in set1 From 4898101549c2ef2e95996782df123937c7bfe75a Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Sat, 29 Jul 2023 17:10:16 -0600 Subject: [PATCH 06/21] raise error if graph is not bipartite --- src/matching/maximum_matching.jl | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/matching/maximum_matching.jl b/src/matching/maximum_matching.jl index 941983a2f..b4143b59e 100644 --- a/src/matching/maximum_matching.jl +++ b/src/matching/maximum_matching.jl @@ -94,6 +94,9 @@ this algorithm is particularly effective for sparse bipartite graphs. """ function hopcroft_karp_matching(graph::Graph) bmap = bipartite_map(graph) + if length(bmap) != nv(graph) + throw(ArgumentError("Provided graph is not bipartite")) + end set1 = [n for n in vertices(graph) if bmap[n] == 1] matching = Dict(n => UNMATCHED for n in vertices(graph)) distance = Dict{Int, Float64}() From f10a96674c3ac1d219d51fda29c23e4da8e77a8b Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Sat, 29 Jul 2023 18:00:15 -0600 Subject: [PATCH 07/21] add comments --- src/matching/maximum_matching.jl | 30 ++++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/src/matching/maximum_matching.jl b/src/matching/maximum_matching.jl index b4143b59e..b1666ff10 100644 --- a/src/matching/maximum_matching.jl +++ b/src/matching/maximum_matching.jl @@ -3,7 +3,8 @@ using Graphs const UNMATCHED = -1 """ -Compute the length of the shortest augmenting path +Determine whether an augmenting path exists and mark distances +so we can compute shortest-length augmenting paths in the DFS. """ function _hk_augmenting_bfs!( graph::Graph, @@ -47,9 +48,9 @@ function _hk_augmenting_bfs!( end """ -Compute augmenting paths +Compute augmenting paths and update the matching """ -function _hk_dfs!( +function _hk_augmenting_dfs!( graph::Graph, root::Int, matching::Dict{Int, Int}, @@ -57,19 +58,25 @@ function _hk_dfs!( )::Bool if root != UNMATCHED for n in neighbors(graph, root) - # We traverse along matched edges + # Traverse edges of the minimum-length alternating path if distance[matching[n]] == distance[root] + 1 - if _hk_dfs!(graph, matching[n], matching, distance) + if _hk_augmenting_dfs!(graph, matching[n], matching, distance) + # If the edge is part of an augmenting path, update the + # matching matching[root] = n matching[n] = root return true end end end + # If we could not find a matched edge that was part of an augmenting + # path, we need to make sure we don't consider this vertex again distance[root] = Inf return false + else + # Return true to indicate that we are part of an augmenting path + return true end - return true end """ @@ -79,10 +86,11 @@ Compute a maximum-cardinality matching of a bipartite graph via the [Hopcroft-Karp algorithm](https://en.wikipedia.org/wiki/Hopcroft-Karp_algorithm). The return type is a dict mapping nodes to nodes. All matched nodes are included -as keys. For exmaple, if `i` is matched with `j`, `i => j` and `j => i` are both +as keys. For example, if `i` is matched with `j`, `i => j` and `j => i` are both included in the returned dict. ### Performance + The algorithms runs in O((m + n)n^0.5), where n is the number of vertices and m is the number of edges. As it does not assume the number of edges is O(n^2), this algorithm is particularly effective for sparse bipartite graphs. @@ -98,12 +106,18 @@ function hopcroft_karp_matching(graph::Graph) throw(ArgumentError("Provided graph is not bipartite")) end set1 = [n for n in vertices(graph) if bmap[n] == 1] + + # Initialize "state" that is modified during the algorithm matching = Dict(n => UNMATCHED for n in vertices(graph)) distance = Dict{Int, Float64}() + + # BFS to determine whether any augmenting paths exist while _hk_augmenting_bfs!(graph, set1, matching, distance) for n1 in set1 if matching[n1] == UNMATCHED - _hk_dfs!(graph, n1, matching, distance) + # DFS to update the matching along a minimum-length + # augmenting path + _hk_augmenting_dfs!(graph, n1, matching, distance) end end end From 3e35a31229e1db725f905da6c86821581d169c77 Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Sat, 29 Jul 2023 18:54:20 -0600 Subject: [PATCH 08/21] add tests for hopcroft_karp_matching --- test/matching/maximum_matching.jl | 97 +++++++++++++++++++++++++++++++ test/runtests.jl | 1 + 2 files changed, 98 insertions(+) create mode 100644 test/matching/maximum_matching.jl diff --git a/test/matching/maximum_matching.jl b/test/matching/maximum_matching.jl new file mode 100644 index 000000000..7cbd51ff8 --- /dev/null +++ b/test/matching/maximum_matching.jl @@ -0,0 +1,97 @@ +using Graphs +using Test + +function test_simple_example() + # From the wikipedia page for the Hopcroft-Karp algorithm + # https://en.wikipedia.org/wiki/Hopcroft–Karp_algorithm + g = Graph() + add_vertices!(g, 10) + add_edge!(g, (1, 6)) + add_edge!(g, (1, 7)) + add_edge!(g, (2, 6)) + add_edge!(g, (2, 10)) + add_edge!(g, (3, 8)) + add_edge!(g, (3, 9)) + add_edge!(g, (4, 6)) + add_edge!(g, (4, 10)) + add_edge!(g, (5, 6)) + add_edge!(g, (5, 9)) + + matching = hopcroft_karp_matching(g) + @test length(matching) == 10 + for i in 1:10 + @test i in keys(matching) + @test matching[i] in neighbors(g, i) + end +end + +function test_imperfect_matching() + g = Graph() + add_vertices!(g, 16) + add_edge!(g, (1, 9)) + add_edge!(g, (2, 9)) + add_edge!(g, (3, 9)) + add_edge!(g, (1, 10)) + add_edge!(g, (4, 10)) + add_edge!(g, (2, 11)) + add_edge!(g, (4, 11)) + add_edge!(g, (3, 12)) + add_edge!(g, (4, 12)) + add_edge!(g, (1, 13)) + add_edge!(g, (2, 13)) + add_edge!(g, (3, 13)) + add_edge!(g, (4, 13)) + add_edge!(g, (1, 14)) + add_edge!(g, (5, 14)) + add_edge!(g, (8, 14)) + add_edge!(g, (2, 15)) + add_edge!(g, (6, 15)) + add_edge!(g, (8, 15)) + add_edge!(g, (3, 16)) + add_edge!(g, (7, 16)) + add_edge!(g, (8, 16)) + + matching = hopcroft_karp_matching(g) + println(matching) + println(length(matching)) + @test length(matching) == 14 + + possibly_unmatched_1 = Set([5, 6, 7, 8]) + possibly_unmatched_2 = Set([9, 10, 11, 12, 13]) + + for i in 1:16 + if i in keys(matching) + # Sanity check + @test matching[i] in neighbors(g, i) + elseif i <= 8 + # Make sure the unmatched vertices match what we predict. + # (Possibly unmatched vertices are computed via the + # Dulmage-Mendelsohn decomposition.) + @test i in possibly_unmatched_1 + else + @test i in possibly_unmatched_2 + end + end +end + +function test_complete_bipartite() + g = complete_bipartite_graph(10, 20) + matching = hopcroft_karp_matching(g) + @test length(matching) == 20 + for i in 1:10 + @test i in keys(matching) + @test matching[i] > 10 + end +end + +function test_not_bipartite() + g = complete_graph(5) + @test_throws(ArgumentError, hopcroft_karp_matching(g)) +end + +@testset "Hopcroft-Karp matching" begin + test_simple_example() + test_imperfect_matching() + test_complete_bipartite() + test_not_bipartite() +end diff --git a/test/runtests.jl b/test/runtests.jl index cbb8763bb..66c6776e1 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -151,6 +151,7 @@ tests = [ "vertexcover/random_vertex_cover", "trees/prufer", "experimental/experimental", + "matching/maximum_matching", ] @testset verbose = true "Graphs" begin From 5bafc3b80bec4a705597101edd59438749b70469 Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Sat, 29 Jul 2023 19:43:42 -0600 Subject: [PATCH 09/21] add example to hopcroft_karp_matching docstring --- src/matching/maximum_matching.jl | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/matching/maximum_matching.jl b/src/matching/maximum_matching.jl index b1666ff10..c38cd28c5 100644 --- a/src/matching/maximum_matching.jl +++ b/src/matching/maximum_matching.jl @@ -99,6 +99,24 @@ this algorithm is particularly effective for sparse bipartite graphs. * `graph`: The bipartite `Graph` for which a maximum matching is computed +### Example +```jldoctest +julia> using Graphs + +julia> g = complete_bipartite_graph(3, 5) +{8, 15} undirected simple Int64 graph + +julia> # Note that the exact matching we compute here is implementation-dependent + +julia> hopcroft_karp_matching(g) +Dict{Int64, Int64} with 6 entries: + 5 => 2 + 4 => 1 + 6 => 3 + 2 => 5 + 3 => 6 + 1 => 4 +``` """ function hopcroft_karp_matching(graph::Graph) bmap = bipartite_map(graph) From 16c8770c57eb48604e96309b499306655fc50554 Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Sun, 30 Jul 2023 08:56:06 -0600 Subject: [PATCH 10/21] remove prinln from test --- test/matching/maximum_matching.jl | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/matching/maximum_matching.jl b/test/matching/maximum_matching.jl index 7cbd51ff8..220636d8b 100644 --- a/test/matching/maximum_matching.jl +++ b/test/matching/maximum_matching.jl @@ -52,8 +52,6 @@ function test_imperfect_matching() add_edge!(g, (8, 16)) matching = hopcroft_karp_matching(g) - println(matching) - println(length(matching)) @test length(matching) == 14 possibly_unmatched_1 = Set([5, 6, 7, 8]) From 0ca74b9ef505af6510fb0b74c7733589d12e7a2a Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Sun, 30 Jul 2023 09:44:58 -0600 Subject: [PATCH 11/21] add maximum_matching function, which defaults to hopcroft_karp_matching --- src/Graphs.jl | 5 ++- src/matching/maximum_matching.jl | 65 +++++++++++++++++++++++++++++++- 2 files changed, 68 insertions(+), 2 deletions(-) diff --git a/src/Graphs.jl b/src/Graphs.jl index 2d52ae556..318d2f66e 100644 --- a/src/Graphs.jl +++ b/src/Graphs.jl @@ -425,7 +425,10 @@ export vertex_cover, # matching - hopcroft_karp_matching + AbstractMaximumMatchingAlgorithm, + HopcroftKarpAlgorithm, + hopcroft_karp_matching, + maximum_cardinality_matching """ Graphs diff --git a/src/matching/maximum_matching.jl b/src/matching/maximum_matching.jl index c38cd28c5..8d7835bcd 100644 --- a/src/matching/maximum_matching.jl +++ b/src/matching/maximum_matching.jl @@ -2,6 +2,21 @@ using Graphs const UNMATCHED = -1 +""" + AbstractMaximumMatchingAlgorithm + +Abstract type for maximum cardinality matching algorithms +""" +abstract type AbstractMaximumMatchingAlgorithm end + +""" + HopcroftKarpAlgorithm + +The [Hopcroft-Karp algorithm](https://en.wikipedia.org/wiki/Hopcroft-Karp_algorithm) +for computing a maximum cardinality matching of a bipartite graph. +""" +struct HopcroftKarpAlgorithm <: AbstractMaximumMatchingAlgorithm end + """ Determine whether an augmenting path exists and mark distances so we can compute shortest-length augmenting paths in the DFS. @@ -99,6 +114,10 @@ this algorithm is particularly effective for sparse bipartite graphs. * `graph`: The bipartite `Graph` for which a maximum matching is computed +### Exceptions + +* `ArgumentError`: The provided graph is not bipartite + ### Example ```jldoctest julia> using Graphs @@ -118,7 +137,7 @@ Dict{Int64, Int64} with 6 entries: 1 => 4 ``` """ -function hopcroft_karp_matching(graph::Graph) +function hopcroft_karp_matching(graph::Graph)::Dict{Int, Int} bmap = bipartite_map(graph) if length(bmap) != nv(graph) throw(ArgumentError("Provided graph is not bipartite")) @@ -142,3 +161,47 @@ function hopcroft_karp_matching(graph::Graph) matching = Dict(i => j for (i, j) in matching if j != UNMATCHED) return matching end + +function maximum_cardinality_matching( + graph::Graph, + algorithm::HopcroftKarpAlgorithm, +)::Dict{Int, Int} + return hopcroft_karp_matching(graph) +end + +""" + maximum_cardinality_matching( + graph::Graph, + algorithm::AbstractMaximumMatchingAlgorithm, + )::Dict{Int, Int} + +Compute a maximum-cardinality matching. + +The return type is a dict mapping nodes to nodes. All matched nodes are included +as keys. For example, if `i` is matched with `j`, `i => j` and `j => i` are both +included in the returned dict. + +### Arguments + +* `graph`: The bipartite `Graph` for which a maximum matching is computed + +* `algorithm`: The algorithm to use to compute the matching. Default is + `HopcroftKarpAlgorithm`. + +### Algorithms +Currently implemented algorithms are: + +* Hopcroft-Karp + +### Exceptions + +* `ArgumentError`: The provided graph is not bipartite but an algorithm that + only applies to bipartite graphs, e.g. Hopcroft-Karp, was chosen + +""" +function maximum_cardinality_matching( + graph::Graph; + algorithm::AbstractMaximumMatchingAlgorithm = HopcroftKarpAlgorithm(), +)::Dict{Int, Int} + return maximum_cardinality_matching(graph, algorithm) +end From 753dcd72cd3c559b3c718878ae7ad30099d23cdd Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Sun, 30 Jul 2023 09:46:15 -0600 Subject: [PATCH 12/21] update tests to use maximum_matching --- test/matching/maximum_matching.jl | 62 +++++++++++++++++++++++++++++-- 1 file changed, 58 insertions(+), 4 deletions(-) diff --git a/test/matching/maximum_matching.jl b/test/matching/maximum_matching.jl index 220636d8b..8b56abe3c 100644 --- a/test/matching/maximum_matching.jl +++ b/test/matching/maximum_matching.jl @@ -17,6 +17,55 @@ function test_simple_example() add_edge!(g, (5, 6)) add_edge!(g, (5, 9)) + matching = maximum_cardinality_matching(g) + @test length(matching) == 10 + for i in 1:10 + @test i in keys(matching) + @test matching[i] in neighbors(g, i) + end +end + +function test_simple_example_algorithm_argument() + # From the wikipedia page for the Hopcroft-Karp algorithm + # https://en.wikipedia.org/wiki/Hopcroft–Karp_algorithm + g = Graph() + add_vertices!(g, 10) + add_edge!(g, (1, 6)) + add_edge!(g, (1, 7)) + add_edge!(g, (2, 6)) + add_edge!(g, (2, 10)) + add_edge!(g, (3, 8)) + add_edge!(g, (3, 9)) + add_edge!(g, (4, 6)) + add_edge!(g, (4, 10)) + add_edge!(g, (5, 6)) + add_edge!(g, (5, 9)) + + algorithm = HopcroftKarpAlgorithm() + matching = maximum_cardinality_matching(g, algorithm) + @test length(matching) == 10 + for i in 1:10 + @test i in keys(matching) + @test matching[i] in neighbors(g, i) + end +end + +function test_simple_example_hopcroft_karp() + # From the wikipedia page for the Hopcroft-Karp algorithm + # https://en.wikipedia.org/wiki/Hopcroft–Karp_algorithm + g = Graph() + add_vertices!(g, 10) + add_edge!(g, (1, 6)) + add_edge!(g, (1, 7)) + add_edge!(g, (2, 6)) + add_edge!(g, (2, 10)) + add_edge!(g, (3, 8)) + add_edge!(g, (3, 9)) + add_edge!(g, (4, 6)) + add_edge!(g, (4, 10)) + add_edge!(g, (5, 6)) + add_edge!(g, (5, 9)) + matching = hopcroft_karp_matching(g) @test length(matching) == 10 for i in 1:10 @@ -51,7 +100,7 @@ function test_imperfect_matching() add_edge!(g, (7, 16)) add_edge!(g, (8, 16)) - matching = hopcroft_karp_matching(g) + matching = maximum_cardinality_matching(g) @test length(matching) == 14 possibly_unmatched_1 = Set([5, 6, 7, 8]) @@ -74,7 +123,7 @@ end function test_complete_bipartite() g = complete_bipartite_graph(10, 20) - matching = hopcroft_karp_matching(g) + matching = maximum_cardinality_matching(g) @test length(matching) == 20 for i in 1:10 @test i in keys(matching) @@ -84,11 +133,16 @@ end function test_not_bipartite() g = complete_graph(5) - @test_throws(ArgumentError, hopcroft_karp_matching(g)) + @test_throws(ArgumentError, maximum_cardinality_matching(g)) end -@testset "Hopcroft-Karp matching" begin +@testset "Maximum cardinality matching" begin test_simple_example() + test_simple_example_algorithm_argument() + test_simple_example_hopcroft_karp() + + # NOTE: Right now there is only one algorithm to test. When we add more, + # we should loop over the algorithms to run these tests for each. test_imperfect_matching() test_complete_bipartite() test_not_bipartite() From 670ba02cac4ccc65e7a8c7c25a5af244afe21166 Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Sun, 30 Jul 2023 09:46:43 -0600 Subject: [PATCH 13/21] add docs for maximum_matching --- docs/src/algorithms/matching.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/src/algorithms/matching.md b/docs/src/algorithms/matching.md index a08d3c93c..392b0561e 100644 --- a/docs/src/algorithms/matching.md +++ b/docs/src/algorithms/matching.md @@ -11,5 +11,8 @@ Pages = ["matching.md"] ## Full docs ```@docs +AbstractMaximumMatchingAlgorithm +HopcroftKarpAlgorithm +maximum_cardinality_matching hopcroft_karp_matching ``` From 2bbd49e237bbee681619b2ffb9c00ec6b08c5be1 Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Sun, 30 Jul 2023 11:12:26 -0600 Subject: [PATCH 14/21] update to support graphs that use a subtype of int --- src/matching/maximum_matching.jl | 71 ++++++++++++++++---------------- 1 file changed, 36 insertions(+), 35 deletions(-) diff --git a/src/matching/maximum_matching.jl b/src/matching/maximum_matching.jl index 8d7835bcd..347ed59dc 100644 --- a/src/matching/maximum_matching.jl +++ b/src/matching/maximum_matching.jl @@ -1,6 +1,4 @@ -using Graphs - -const UNMATCHED = -1 +const UNMATCHED = nothing """ AbstractMaximumMatchingAlgorithm @@ -22,13 +20,15 @@ Determine whether an augmenting path exists and mark distances so we can compute shortest-length augmenting paths in the DFS. """ function _hk_augmenting_bfs!( - graph::Graph, - set1::Vector{Int}, - matching::Dict{Int, Int}, - distance::Dict{Int, Float64}, -)::Bool + graph::Graph{T}, + set1::Vector{T}, + matching::Dict{T, Union{T, typeof(UNMATCHED)}}, + distance::Dict{Union{T, typeof(UNMATCHED)}, Float64}, +)::Bool where {T <: Integer} # Initialize queue with the unmatched nodes in set1 - queue = Vector{Int}([n for n in set1 if matching[n] == UNMATCHED]) + queue = Vector{Union{eltype(graph), typeof(UNMATCHED)}}( + [n for n in set1 if matching[n] == UNMATCHED] + ) distance[UNMATCHED] = Inf for n in set1 @@ -66,11 +66,11 @@ end Compute augmenting paths and update the matching """ function _hk_augmenting_dfs!( - graph::Graph, - root::Int, - matching::Dict{Int, Int}, - distance::Dict{Int, Float64}, -)::Bool + graph::Graph{T}, + root::Union{T, typeof(UNMATCHED)}, + matching::Dict{T, Union{T, typeof(UNMATCHED)}}, + distance::Dict{Union{T, typeof(UNMATCHED)}, Float64}, +)::Bool where {T <: Integer} if root != UNMATCHED for n in neighbors(graph, root) # Traverse edges of the minimum-length alternating path @@ -118,24 +118,6 @@ this algorithm is particularly effective for sparse bipartite graphs. * `ArgumentError`: The provided graph is not bipartite -### Example -```jldoctest -julia> using Graphs - -julia> g = complete_bipartite_graph(3, 5) -{8, 15} undirected simple Int64 graph - -julia> # Note that the exact matching we compute here is implementation-dependent - -julia> hopcroft_karp_matching(g) -Dict{Int64, Int64} with 6 entries: - 5 => 2 - 4 => 1 - 6 => 3 - 2 => 5 - 3 => 6 - 1 => 4 -``` """ function hopcroft_karp_matching(graph::Graph)::Dict{Int, Int} bmap = bipartite_map(graph) @@ -145,8 +127,10 @@ function hopcroft_karp_matching(graph::Graph)::Dict{Int, Int} set1 = [n for n in vertices(graph) if bmap[n] == 1] # Initialize "state" that is modified during the algorithm - matching = Dict(n => UNMATCHED for n in vertices(graph)) - distance = Dict{Int, Float64}() + matching = Dict{eltype(graph), Union{eltype(graph), typeof(UNMATCHED)}}( + n => UNMATCHED for n in vertices(graph) + ) + distance = Dict{Union{eltype(graph), typeof(UNMATCHED)}, Float64}() # BFS to determine whether any augmenting paths exist while _hk_augmenting_bfs!(graph, set1, matching, distance) @@ -183,7 +167,7 @@ included in the returned dict. ### Arguments -* `graph`: The bipartite `Graph` for which a maximum matching is computed +* `graph`: The `Graph` for which a maximum matching is computed * `algorithm`: The algorithm to use to compute the matching. Default is `HopcroftKarpAlgorithm`. @@ -198,6 +182,23 @@ Currently implemented algorithms are: * `ArgumentError`: The provided graph is not bipartite but an algorithm that only applies to bipartite graphs, e.g. Hopcroft-Karp, was chosen +### Example +```jldoctest +julia> using Graphs + +julia> g = path_graph(6) +{6, 5} undirected simple Int64 graph + +julia> maximum_cardinality_matching(g) +Dict{Int64, Int64} with 6 entries: + 5 => 6 + 4 => 3 + 6 => 5 + 2 => 1 + 3 => 4 + 1 => 2 + +``` """ function maximum_cardinality_matching( graph::Graph; From 909894c9bae5a0d58f65ea73d5a9e04989904e77 Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Sun, 30 Jul 2023 11:20:29 -0600 Subject: [PATCH 15/21] update return types to match element type of graph --- src/matching/maximum_matching.jl | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/matching/maximum_matching.jl b/src/matching/maximum_matching.jl index 347ed59dc..2d281886a 100644 --- a/src/matching/maximum_matching.jl +++ b/src/matching/maximum_matching.jl @@ -119,7 +119,9 @@ this algorithm is particularly effective for sparse bipartite graphs. * `ArgumentError`: The provided graph is not bipartite """ -function hopcroft_karp_matching(graph::Graph)::Dict{Int, Int} +function hopcroft_karp_matching( + graph::Graph{T} +)::Dict{T, T} where {T <: Integer} bmap = bipartite_map(graph) if length(bmap) != nv(graph) throw(ArgumentError("Provided graph is not bipartite")) @@ -147,9 +149,9 @@ function hopcroft_karp_matching(graph::Graph)::Dict{Int, Int} end function maximum_cardinality_matching( - graph::Graph, + graph::Graph{T}, algorithm::HopcroftKarpAlgorithm, -)::Dict{Int, Int} +)::Dict{T, T} where {T <: Integer} return hopcroft_karp_matching(graph) end @@ -201,8 +203,8 @@ Dict{Int64, Int64} with 6 entries: ``` """ function maximum_cardinality_matching( - graph::Graph; + graph::Graph{T}; algorithm::AbstractMaximumMatchingAlgorithm = HopcroftKarpAlgorithm(), -)::Dict{Int, Int} +)::Dict{T, T} where {T <: Integer} return maximum_cardinality_matching(graph, algorithm) end From e4f1b4af3d153e24fb713f6d4d022b7c5b3ab222 Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Sun, 30 Jul 2023 11:36:04 -0600 Subject: [PATCH 16/21] declare a parameterized MatchedNodeType for convenience --- src/matching/maximum_matching.jl | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/matching/maximum_matching.jl b/src/matching/maximum_matching.jl index 2d281886a..bb2e7545a 100644 --- a/src/matching/maximum_matching.jl +++ b/src/matching/maximum_matching.jl @@ -1,4 +1,5 @@ const UNMATCHED = nothing +MatchedNodeType{T} = Union{T, typeof(UNMATCHED)} """ AbstractMaximumMatchingAlgorithm @@ -22,11 +23,11 @@ so we can compute shortest-length augmenting paths in the DFS. function _hk_augmenting_bfs!( graph::Graph{T}, set1::Vector{T}, - matching::Dict{T, Union{T, typeof(UNMATCHED)}}, - distance::Dict{Union{T, typeof(UNMATCHED)}, Float64}, + matching::Dict{T, MatchedNodeType{T}}, + distance::Dict{MatchedNodeType{T}, Float64}, )::Bool where {T <: Integer} # Initialize queue with the unmatched nodes in set1 - queue = Vector{Union{eltype(graph), typeof(UNMATCHED)}}( + queue = Vector{MatchedNodeType{eltype(graph)}}( [n for n in set1 if matching[n] == UNMATCHED] ) @@ -67,9 +68,9 @@ Compute augmenting paths and update the matching """ function _hk_augmenting_dfs!( graph::Graph{T}, - root::Union{T, typeof(UNMATCHED)}, - matching::Dict{T, Union{T, typeof(UNMATCHED)}}, - distance::Dict{Union{T, typeof(UNMATCHED)}, Float64}, + root::MatchedNodeType{T}, + matching::Dict{T, MatchedNodeType{T}}, + distance::Dict{MatchedNodeType{T}, Float64}, )::Bool where {T <: Integer} if root != UNMATCHED for n in neighbors(graph, root) @@ -129,10 +130,10 @@ function hopcroft_karp_matching( set1 = [n for n in vertices(graph) if bmap[n] == 1] # Initialize "state" that is modified during the algorithm - matching = Dict{eltype(graph), Union{eltype(graph), typeof(UNMATCHED)}}( + matching = Dict{eltype(graph), MatchedNodeType{eltype(graph)}}( n => UNMATCHED for n in vertices(graph) ) - distance = Dict{Union{eltype(graph), typeof(UNMATCHED)}, Float64}() + distance = Dict{MatchedNodeType{eltype(graph)}, Float64}() # BFS to determine whether any augmenting paths exist while _hk_augmenting_bfs!(graph, set1, matching, distance) From 1299684cde18d5772d8dede6ef2a37a909920058 Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Sun, 30 Jul 2023 11:36:47 -0600 Subject: [PATCH 17/21] test where a graph with UInt8 vertices is used --- test/matching/maximum_matching.jl | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/test/matching/maximum_matching.jl b/test/matching/maximum_matching.jl index 8b56abe3c..1d3d57de8 100644 --- a/test/matching/maximum_matching.jl +++ b/test/matching/maximum_matching.jl @@ -74,6 +74,31 @@ function test_simple_example_hopcroft_karp() end end +function test_simple_example_different_node_type() + # From the wikipedia page for the Hopcroft-Karp algorithm + # https://en.wikipedia.org/wiki/Hopcroft–Karp_algorithm + g = Graph{UInt8}() + add_vertices!(g, 10) + add_edge!(g, (1, 6)) + add_edge!(g, (1, 7)) + add_edge!(g, (2, 6)) + add_edge!(g, (2, 10)) + add_edge!(g, (3, 8)) + add_edge!(g, (3, 9)) + add_edge!(g, (4, 6)) + add_edge!(g, (4, 10)) + add_edge!(g, (5, 6)) + add_edge!(g, (5, 9)) + + matching = hopcroft_karp_matching(g) + @test eltype(matching) === Pair{UInt8, UInt8} + @test length(matching) == 10 + for i in 1:10 + @test i in keys(matching) + @test matching[i] in neighbors(g, i) + end +end + function test_imperfect_matching() g = Graph() add_vertices!(g, 16) @@ -102,7 +127,6 @@ function test_imperfect_matching() matching = maximum_cardinality_matching(g) @test length(matching) == 14 - possibly_unmatched_1 = Set([5, 6, 7, 8]) possibly_unmatched_2 = Set([9, 10, 11, 12, 13]) @@ -140,6 +164,7 @@ end test_simple_example() test_simple_example_algorithm_argument() test_simple_example_hopcroft_karp() + test_simple_example_different_node_type() # NOTE: Right now there is only one algorithm to test. When we add more, # we should loop over the algorithms to run these tests for each. From e08d71d72fff8bade9a8bdf930e0de99ac3d8572 Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Sun, 30 Jul 2023 11:38:47 -0600 Subject: [PATCH 18/21] allow AbstractGraph as arguments --- src/matching/maximum_matching.jl | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/matching/maximum_matching.jl b/src/matching/maximum_matching.jl index bb2e7545a..e05d8582d 100644 --- a/src/matching/maximum_matching.jl +++ b/src/matching/maximum_matching.jl @@ -21,7 +21,7 @@ Determine whether an augmenting path exists and mark distances so we can compute shortest-length augmenting paths in the DFS. """ function _hk_augmenting_bfs!( - graph::Graph{T}, + graph::AbstractGraph{T}, set1::Vector{T}, matching::Dict{T, MatchedNodeType{T}}, distance::Dict{MatchedNodeType{T}, Float64}, @@ -67,7 +67,7 @@ end Compute augmenting paths and update the matching """ function _hk_augmenting_dfs!( - graph::Graph{T}, + graph::AbstractGraph{T}, root::MatchedNodeType{T}, matching::Dict{T, MatchedNodeType{T}}, distance::Dict{MatchedNodeType{T}, Float64}, @@ -96,7 +96,7 @@ function _hk_augmenting_dfs!( end """ - hopcroft_karp_matching(graph::Graph)::Dict + hopcroft_karp_matching(graph::AbstractGraph)::Dict Compute a maximum-cardinality matching of a bipartite graph via the [Hopcroft-Karp algorithm](https://en.wikipedia.org/wiki/Hopcroft-Karp_algorithm). @@ -121,7 +121,7 @@ this algorithm is particularly effective for sparse bipartite graphs. """ function hopcroft_karp_matching( - graph::Graph{T} + graph::AbstractGraph{T} )::Dict{T, T} where {T <: Integer} bmap = bipartite_map(graph) if length(bmap) != nv(graph) @@ -150,7 +150,7 @@ function hopcroft_karp_matching( end function maximum_cardinality_matching( - graph::Graph{T}, + graph::AbstractGraph{T}, algorithm::HopcroftKarpAlgorithm, )::Dict{T, T} where {T <: Integer} return hopcroft_karp_matching(graph) @@ -158,7 +158,7 @@ end """ maximum_cardinality_matching( - graph::Graph, + graph::AbstractGraph, algorithm::AbstractMaximumMatchingAlgorithm, )::Dict{Int, Int} @@ -204,7 +204,7 @@ Dict{Int64, Int64} with 6 entries: ``` """ function maximum_cardinality_matching( - graph::Graph{T}; + graph::AbstractGraph{T}; algorithm::AbstractMaximumMatchingAlgorithm = HopcroftKarpAlgorithm(), )::Dict{T, T} where {T <: Integer} return maximum_cardinality_matching(graph, algorithm) From 7ba9fc8cdd6c4f65e8dce07c9518bdb8c417d807 Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Sun, 30 Jul 2023 11:43:22 -0600 Subject: [PATCH 19/21] move hopcroft-karp-specific code into its own file --- src/matching/hopcroft_karp.jl | 136 ++++++++++++++++++++++++++++ src/matching/maximum_matching.jl | 150 ++----------------------------- 2 files changed, 144 insertions(+), 142 deletions(-) create mode 100644 src/matching/hopcroft_karp.jl diff --git a/src/matching/hopcroft_karp.jl b/src/matching/hopcroft_karp.jl new file mode 100644 index 000000000..8f11d2440 --- /dev/null +++ b/src/matching/hopcroft_karp.jl @@ -0,0 +1,136 @@ +const UNMATCHED = nothing +MatchedNodeType{T} = Union{T, typeof(UNMATCHED)} + +""" +Determine whether an augmenting path exists and mark distances +so we can compute shortest-length augmenting paths in the DFS. +""" +function _hk_augmenting_bfs!( + graph::AbstractGraph{T}, + set1::Vector{T}, + matching::Dict{T, MatchedNodeType{T}}, + distance::Dict{MatchedNodeType{T}, Float64}, +)::Bool where {T <: Integer} + # Initialize queue with the unmatched nodes in set1 + queue = Vector{MatchedNodeType{eltype(graph)}}( + [n for n in set1 if matching[n] == UNMATCHED] + ) + + distance[UNMATCHED] = Inf + for n in set1 + if matching[n] == UNMATCHED + distance[n] = 0.0 + else + distance[n] = Inf + end + end + + while !isempty(queue) + n1 = popfirst!(queue) + + # If n1 is (a) matched or (b) in set1 + if distance[n1] < Inf && n1 != UNMATCHED + for n2 in neighbors(graph, n1) + # If n2 has not been encountered + if distance[matching[n2]] == Inf + # Give it a distance + distance[matching[n2]] = distance[n1] + 1 + + # Note that n2 could be unmatched + push!(queue, matching[n2]) + end + end + end + end + + found_augmenting_path = (distance[UNMATCHED] < Inf) + # The distance to UNMATCHED is the length of the shortest augmenting path + return found_augmenting_path +end + +""" +Compute augmenting paths and update the matching +""" +function _hk_augmenting_dfs!( + graph::AbstractGraph{T}, + root::MatchedNodeType{T}, + matching::Dict{T, MatchedNodeType{T}}, + distance::Dict{MatchedNodeType{T}, Float64}, +)::Bool where {T <: Integer} + if root != UNMATCHED + for n in neighbors(graph, root) + # Traverse edges of the minimum-length alternating path + if distance[matching[n]] == distance[root] + 1 + if _hk_augmenting_dfs!(graph, matching[n], matching, distance) + # If the edge is part of an augmenting path, update the + # matching + matching[root] = n + matching[n] = root + return true + end + end + end + # If we could not find a matched edge that was part of an augmenting + # path, we need to make sure we don't consider this vertex again + distance[root] = Inf + return false + else + # Return true to indicate that we are part of an augmenting path + return true + end +end + +""" + hopcroft_karp_matching(graph::AbstractGraph)::Dict + +Compute a maximum-cardinality matching of a bipartite graph via the +[Hopcroft-Karp algorithm](https://en.wikipedia.org/wiki/Hopcroft-Karp_algorithm). + +The return type is a dict mapping nodes to nodes. All matched nodes are included +as keys. For example, if `i` is matched with `j`, `i => j` and `j => i` are both +included in the returned dict. + +### Performance + +The algorithms runs in O((m + n)n^0.5), where n is the number of vertices and +m is the number of edges. As it does not assume the number of edges is O(n^2), +this algorithm is particularly effective for sparse bipartite graphs. + +### Arguments + +* `graph`: The bipartite `Graph` for which a maximum matching is computed + +### Exceptions + +* `ArgumentError`: The provided graph is not bipartite + +""" +function hopcroft_karp_matching( + graph::AbstractGraph{T} +)::Dict{T, T} where {T <: Integer} + bmap = bipartite_map(graph) + if length(bmap) != nv(graph) + throw(ArgumentError("Provided graph is not bipartite")) + end + set1 = [n for n in vertices(graph) if bmap[n] == 1] + + # Initialize "state" that is modified during the algorithm + matching = Dict{eltype(graph), MatchedNodeType{eltype(graph)}}( + n => UNMATCHED for n in vertices(graph) + ) + distance = Dict{MatchedNodeType{eltype(graph)}, Float64}() + + # BFS to determine whether any augmenting paths exist + while _hk_augmenting_bfs!(graph, set1, matching, distance) + for n1 in set1 + if matching[n1] == UNMATCHED + # DFS to update the matching along a minimum-length + # augmenting path + _hk_augmenting_dfs!(graph, n1, matching, distance) + end + end + end + matching = Dict(i => j for (i, j) in matching if j != UNMATCHED) + return matching +end + diff --git a/src/matching/maximum_matching.jl b/src/matching/maximum_matching.jl index e05d8582d..8afe010b9 100644 --- a/src/matching/maximum_matching.jl +++ b/src/matching/maximum_matching.jl @@ -1,5 +1,4 @@ -const UNMATCHED = nothing -MatchedNodeType{T} = Union{T, typeof(UNMATCHED)} +include("hopcroft_karp.jl") """ AbstractMaximumMatchingAlgorithm @@ -16,146 +15,6 @@ for computing a maximum cardinality matching of a bipartite graph. """ struct HopcroftKarpAlgorithm <: AbstractMaximumMatchingAlgorithm end -""" -Determine whether an augmenting path exists and mark distances -so we can compute shortest-length augmenting paths in the DFS. -""" -function _hk_augmenting_bfs!( - graph::AbstractGraph{T}, - set1::Vector{T}, - matching::Dict{T, MatchedNodeType{T}}, - distance::Dict{MatchedNodeType{T}, Float64}, -)::Bool where {T <: Integer} - # Initialize queue with the unmatched nodes in set1 - queue = Vector{MatchedNodeType{eltype(graph)}}( - [n for n in set1 if matching[n] == UNMATCHED] - ) - - distance[UNMATCHED] = Inf - for n in set1 - if matching[n] == UNMATCHED - distance[n] = 0.0 - else - distance[n] = Inf - end - end - - while !isempty(queue) - n1 = popfirst!(queue) - - # If n1 is (a) matched or (b) in set1 - if distance[n1] < Inf && n1 != UNMATCHED - for n2 in neighbors(graph, n1) - # If n2 has not been encountered - if distance[matching[n2]] == Inf - # Give it a distance - distance[matching[n2]] = distance[n1] + 1 - - # Note that n2 could be unmatched - push!(queue, matching[n2]) - end - end - end - end - - found_augmenting_path = (distance[UNMATCHED] < Inf) - # The distance to UNMATCHED is the length of the shortest augmenting path - return found_augmenting_path -end - -""" -Compute augmenting paths and update the matching -""" -function _hk_augmenting_dfs!( - graph::AbstractGraph{T}, - root::MatchedNodeType{T}, - matching::Dict{T, MatchedNodeType{T}}, - distance::Dict{MatchedNodeType{T}, Float64}, -)::Bool where {T <: Integer} - if root != UNMATCHED - for n in neighbors(graph, root) - # Traverse edges of the minimum-length alternating path - if distance[matching[n]] == distance[root] + 1 - if _hk_augmenting_dfs!(graph, matching[n], matching, distance) - # If the edge is part of an augmenting path, update the - # matching - matching[root] = n - matching[n] = root - return true - end - end - end - # If we could not find a matched edge that was part of an augmenting - # path, we need to make sure we don't consider this vertex again - distance[root] = Inf - return false - else - # Return true to indicate that we are part of an augmenting path - return true - end -end - -""" - hopcroft_karp_matching(graph::AbstractGraph)::Dict - -Compute a maximum-cardinality matching of a bipartite graph via the -[Hopcroft-Karp algorithm](https://en.wikipedia.org/wiki/Hopcroft-Karp_algorithm). - -The return type is a dict mapping nodes to nodes. All matched nodes are included -as keys. For example, if `i` is matched with `j`, `i => j` and `j => i` are both -included in the returned dict. - -### Performance - -The algorithms runs in O((m + n)n^0.5), where n is the number of vertices and -m is the number of edges. As it does not assume the number of edges is O(n^2), -this algorithm is particularly effective for sparse bipartite graphs. - -### Arguments - -* `graph`: The bipartite `Graph` for which a maximum matching is computed - -### Exceptions - -* `ArgumentError`: The provided graph is not bipartite - -""" -function hopcroft_karp_matching( - graph::AbstractGraph{T} -)::Dict{T, T} where {T <: Integer} - bmap = bipartite_map(graph) - if length(bmap) != nv(graph) - throw(ArgumentError("Provided graph is not bipartite")) - end - set1 = [n for n in vertices(graph) if bmap[n] == 1] - - # Initialize "state" that is modified during the algorithm - matching = Dict{eltype(graph), MatchedNodeType{eltype(graph)}}( - n => UNMATCHED for n in vertices(graph) - ) - distance = Dict{MatchedNodeType{eltype(graph)}, Float64}() - - # BFS to determine whether any augmenting paths exist - while _hk_augmenting_bfs!(graph, set1, matching, distance) - for n1 in set1 - if matching[n1] == UNMATCHED - # DFS to update the matching along a minimum-length - # augmenting path - _hk_augmenting_dfs!(graph, n1, matching, distance) - end - end - end - matching = Dict(i => j for (i, j) in matching if j != UNMATCHED) - return matching -end - -function maximum_cardinality_matching( - graph::AbstractGraph{T}, - algorithm::HopcroftKarpAlgorithm, -)::Dict{T, T} where {T <: Integer} - return hopcroft_karp_matching(graph) -end - """ maximum_cardinality_matching( graph::AbstractGraph, @@ -209,3 +68,10 @@ function maximum_cardinality_matching( )::Dict{T, T} where {T <: Integer} return maximum_cardinality_matching(graph, algorithm) end + +function maximum_cardinality_matching( + graph::AbstractGraph{T}, + algorithm::HopcroftKarpAlgorithm, +)::Dict{T, T} where {T <: Integer} + return hopcroft_karp_matching(graph) +end From ca2818775a0fecf20eee4036fa6f65805c6e1cf7 Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Sun, 30 Jul 2023 21:55:38 -0600 Subject: [PATCH 20/21] add tests for empty and disconnected graphs --- test/matching/maximum_matching.jl | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/test/matching/maximum_matching.jl b/test/matching/maximum_matching.jl index 1d3d57de8..5b11505ad 100644 --- a/test/matching/maximum_matching.jl +++ b/test/matching/maximum_matching.jl @@ -160,6 +160,24 @@ function test_not_bipartite() @test_throws(ArgumentError, maximum_cardinality_matching(g)) end +function test_empty() + g = Graph() + matching = maximum_cardinality_matching(g) + @test length(matching) == 0 +end + +function test_disconnected() + g = Graph() + add_vertices!(g, 5) + add_edge!(g, (1, 3)) + add_edge!(g, (2, 4)) + add_edge!(g, (2, 5)) + matching = maximum_cardinality_matching(g) + @test length(matching) == 4 + @test matching[1] == 3 && matching[3] == 1 + @test matching[2] == 4 || matching[2] == 5 +end + @testset "Maximum cardinality matching" begin test_simple_example() test_simple_example_algorithm_argument() @@ -171,4 +189,6 @@ end test_imperfect_matching() test_complete_bipartite() test_not_bipartite() + test_empty() + test_disconnected() end From be4c0979e3f30c5aef661d169884be70b51cfed6 Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Sun, 30 Jul 2023 22:07:36 -0600 Subject: [PATCH 21/21] run JuliaFormatter --- src/matching/hopcroft_karp.jl | 29 +++++++++++++---------------- src/matching/maximum_matching.jl | 9 ++++----- test/matching/maximum_matching.jl | 2 +- 3 files changed, 18 insertions(+), 22 deletions(-) diff --git a/src/matching/hopcroft_karp.jl b/src/matching/hopcroft_karp.jl index 8f11d2440..0c66ae82d 100644 --- a/src/matching/hopcroft_karp.jl +++ b/src/matching/hopcroft_karp.jl @@ -1,5 +1,5 @@ const UNMATCHED = nothing -MatchedNodeType{T} = Union{T, typeof(UNMATCHED)} +MatchedNodeType{T} = Union{T,typeof(UNMATCHED)} """ Determine whether an augmenting path exists and mark distances @@ -8,13 +8,13 @@ so we can compute shortest-length augmenting paths in the DFS. function _hk_augmenting_bfs!( graph::AbstractGraph{T}, set1::Vector{T}, - matching::Dict{T, MatchedNodeType{T}}, - distance::Dict{MatchedNodeType{T}, Float64}, -)::Bool where {T <: Integer} + matching::Dict{T,MatchedNodeType{T}}, + distance::Dict{MatchedNodeType{T},Float64}, +)::Bool where {T<:Integer} # Initialize queue with the unmatched nodes in set1 - queue = Vector{MatchedNodeType{eltype(graph)}}( - [n for n in set1 if matching[n] == UNMATCHED] - ) + queue = Vector{MatchedNodeType{eltype(graph)}}([ + n for n in set1 if matching[n] == UNMATCHED + ]) distance[UNMATCHED] = Inf for n in set1 @@ -54,9 +54,9 @@ Compute augmenting paths and update the matching function _hk_augmenting_dfs!( graph::AbstractGraph{T}, root::MatchedNodeType{T}, - matching::Dict{T, MatchedNodeType{T}}, - distance::Dict{MatchedNodeType{T}, Float64}, -)::Bool where {T <: Integer} + matching::Dict{T,MatchedNodeType{T}}, + distance::Dict{MatchedNodeType{T},Float64}, +)::Bool where {T<:Integer} if root != UNMATCHED for n in neighbors(graph, root) # Traverse edges of the minimum-length alternating path @@ -105,9 +105,7 @@ this algorithm is particularly effective for sparse bipartite graphs. * `ArgumentError`: The provided graph is not bipartite """ -function hopcroft_karp_matching( - graph::AbstractGraph{T} -)::Dict{T, T} where {T <: Integer} +function hopcroft_karp_matching(graph::AbstractGraph{T})::Dict{T,T} where {T<:Integer} bmap = bipartite_map(graph) if length(bmap) != nv(graph) throw(ArgumentError("Provided graph is not bipartite")) @@ -115,10 +113,10 @@ function hopcroft_karp_matching( set1 = [n for n in vertices(graph) if bmap[n] == 1] # Initialize "state" that is modified during the algorithm - matching = Dict{eltype(graph), MatchedNodeType{eltype(graph)}}( + matching = Dict{eltype(graph),MatchedNodeType{eltype(graph)}}( n => UNMATCHED for n in vertices(graph) ) - distance = Dict{MatchedNodeType{eltype(graph)}, Float64}() + distance = Dict{MatchedNodeType{eltype(graph)},Float64}() # BFS to determine whether any augmenting paths exist while _hk_augmenting_bfs!(graph, set1, matching, distance) @@ -133,4 +131,3 @@ function hopcroft_karp_matching( matching = Dict(i => j for (i, j) in matching if j != UNMATCHED) return matching end - diff --git a/src/matching/maximum_matching.jl b/src/matching/maximum_matching.jl index 8afe010b9..02bcb63bb 100644 --- a/src/matching/maximum_matching.jl +++ b/src/matching/maximum_matching.jl @@ -64,14 +64,13 @@ Dict{Int64, Int64} with 6 entries: """ function maximum_cardinality_matching( graph::AbstractGraph{T}; - algorithm::AbstractMaximumMatchingAlgorithm = HopcroftKarpAlgorithm(), -)::Dict{T, T} where {T <: Integer} + algorithm::AbstractMaximumMatchingAlgorithm=HopcroftKarpAlgorithm(), +)::Dict{T,T} where {T<:Integer} return maximum_cardinality_matching(graph, algorithm) end function maximum_cardinality_matching( - graph::AbstractGraph{T}, - algorithm::HopcroftKarpAlgorithm, -)::Dict{T, T} where {T <: Integer} + graph::AbstractGraph{T}, algorithm::HopcroftKarpAlgorithm +)::Dict{T,T} where {T<:Integer} return hopcroft_karp_matching(graph) end diff --git a/test/matching/maximum_matching.jl b/test/matching/maximum_matching.jl index 5b11505ad..d6e4bc283 100644 --- a/test/matching/maximum_matching.jl +++ b/test/matching/maximum_matching.jl @@ -91,7 +91,7 @@ function test_simple_example_different_node_type() add_edge!(g, (5, 9)) matching = hopcroft_karp_matching(g) - @test eltype(matching) === Pair{UInt8, UInt8} + @test eltype(matching) === Pair{UInt8,UInt8} @test length(matching) == 10 for i in 1:10 @test i in keys(matching)