From f7f32477989f90f020b44fa62bec7d6a8f746367 Mon Sep 17 00:00:00 2001 From: Thomas Christensen Date: Wed, 19 Jun 2024 14:16:47 +0200 Subject: [PATCH 1/6] factor out dfs-search from and implement using this --- src/Graphs.jl | 1 + src/biconnectivity/articulation.jl | 184 +++++++++++++++++++--------- test/biconnectivity/articulation.jl | 6 +- 3 files changed, 132 insertions(+), 59 deletions(-) diff --git a/src/Graphs.jl b/src/Graphs.jl index 86bc0946a..0f740efc3 100644 --- a/src/Graphs.jl +++ b/src/Graphs.jl @@ -418,6 +418,7 @@ export # biconnectivity and articulation points articulation, + is_articulation, biconnected_components, bridges, diff --git a/src/biconnectivity/articulation.jl b/src/biconnectivity/articulation.jl index 3156e7a1a..25d389830 100644 --- a/src/biconnectivity/articulation.jl +++ b/src/biconnectivity/articulation.jl @@ -1,8 +1,9 @@ """ articulation(g) -Compute the [articulation points](https://en.wikipedia.org/wiki/Biconnected_component) -of a connected graph `g` and return an array containing all cut vertices. +Compute the [articulation points](https://en.wikipedia.org/wiki/Biconnected_component) (also +known as cut or seperating vertices) of an undirected graph `g` and return an array +containing all the vertices of `g` that are articulation points. # Examples ```jldoctest @@ -22,74 +23,141 @@ julia> articulation(path_graph(5)) function articulation end @traitfn function articulation(g::AG::(!IsDirected)) where {T,AG<:AbstractGraph{T}} s = Vector{Tuple{T,T,T}}() + low = zeros(T, nv(g)) + pre = zeros(T, nv(g)) + is_articulation_pt = falses(nv(g)) + @inbounds for u in vertices(g) + articulation_dfs!(is_articulation_pt, g, u, s, low, pre) + end + + articulation_points = T[v for (v, b) in enumerate(is_articulation_pt) if b] + + return articulation_points +end + +""" + is_articulation(g, v) + +Determine whether `v` is an +[articulation points](https://en.wikipedia.org/wiki/Biconnected_component) of a undirected +graph `g`, returning `true` if so and `false` otherwise. + +See also [`articulation`]. + +# Examples +```jldoctest +julia> using Graphs + +julia> g = path_graph(5) +{5, 4} undirected simple Int64 graph + +julia> articulation(g) +3-element Vector{Int64}: + 2 + 3 + 4 + +julia> is_articulation(g, 2) +true + +julia> is_articulation(g, 1) +false +``` +""" +function is_articulation end +@traitfn function is_articulation( + g::AG::(!IsDirected), + v::T + ) where {T,AG<:AbstractGraph{T}} + + s = Vector{Tuple{T,T,T}}() low = zeros(T, nv(g)) pre = zeros(T, nv(g)) - @inbounds for u in vertices(g) - pre[u] != 0 && continue - v = u - children = 0 - wi::T = zero(T) - w::T = zero(T) - cnt::T = one(T) - first_time = true - - # TODO the algorithm currently relies on the assumption that - # outneighbors(g, v) is indexable. This assumption might not be true - # in general, so in case that outneighbors does not produce a vector - # we collect these vertices. This might lead to a large number of - # allocations, so we should find a way to handle that case differently, - # or require inneighbors, outneighbors and neighbors to always - # return indexable collections. - - while !isempty(s) || first_time - first_time = false - if wi < 1 - pre[v] = cnt - cnt += 1 - low[v] = pre[v] - v_neighbors = collect_if_not_vector(outneighbors(g, v)) - wi = 1 - else - wi, u, v = pop!(s) - v_neighbors = collect_if_not_vector(outneighbors(g, v)) - w = v_neighbors[wi] - low[v] = min(low[v], low[w]) - if low[w] >= pre[v] && u != v + return articulation_dfs!(nothing, g, v, s, low, pre) +end + +@traitfn function articulation_dfs!( + is_articulation_pt::Union{Nothing, BitVector}, + g::AG::(!IsDirected), + u::T, + s::Vector{Tuple{T,T,T}}, + low::Vector{T}, + pre::Vector{T}, + ) where {T,AG<:AbstractGraph{T}} + + if !isnothing(is_articulation_pt) + if pre[u] != 0 + return is_articulation_pt + end + end + + v = u + children = 0 + wi::T = zero(T) + w::T = zero(T) + cnt::T = one(T) + first_time = true + + # TODO the algorithm currently relies on the assumption that + # outneighbors(g, v) is indexable. This assumption might not be true + # in general, so in case that outneighbors does not produce a vector + # we collect these vertices. This might lead to a large number of + # allocations, so we should find a way to handle that case differently, + # or require inneighbors, outneighbors and neighbors to always + # return indexable collections. + + while !isempty(s) || first_time + first_time = false + if wi < 1 + pre[v] = cnt + cnt += 1 + low[v] = pre[v] + v_neighbors = collect_if_not_vector(outneighbors(g, v)) + wi = 1 + else + wi, u, v = pop!(s) + v_neighbors = collect_if_not_vector(outneighbors(g, v)) + w = v_neighbors[wi] + low[v] = min(low[v], low[w]) + if low[w] >= pre[v] && u != v + if isnothing(is_articulation_pt) + if v == u + return true + end + else is_articulation_pt[v] = true end - wi += 1 end - while wi <= length(v_neighbors) - w = v_neighbors[wi] - if pre[w] == 0 - if u == v - children += 1 - end - push!(s, (wi, u, v)) - wi = 0 - u = v - v = w - break - elseif w != u - low[v] = min(low[v], pre[w]) + wi += 1 + end + while wi <= length(v_neighbors) + w = v_neighbors[wi] + if pre[w] == 0 + if u == v + children += 1 end - wi += 1 + push!(s, (wi, u, v)) + wi = 0 + u = v + v = w + break + elseif w != u + low[v] = min(low[v], pre[w]) end - wi < 1 && continue + wi += 1 end + wi < 1 && continue + end - if children > 1 + if children > 1 + if isnothing(is_articulation_pt) + return u == v + else is_articulation_pt[u] = true end end - articulation_points = Vector{T}() - - for u in findall(is_articulation_pt) - push!(articulation_points, T(u)) - end - - return articulation_points -end + return isnothing(is_articulation_pt) ? false : is_articulation_pt +end \ No newline at end of file diff --git a/test/biconnectivity/articulation.jl b/test/biconnectivity/articulation.jl index 0ebaff9bd..38db23630 100644 --- a/test/biconnectivity/articulation.jl +++ b/test/biconnectivity/articulation.jl @@ -22,18 +22,22 @@ art = @inferred(articulation(g)) ans = [1, 7, 8, 12] @test art == ans + @test art == findall(is_articulation.(Ref(g), vertices(g))) end for level in 1:6 btree = Graphs.binary_tree(level) for tree in test_generic_graphs(btree; eltypes=[Int, UInt8, Int16]) artpts = @inferred(articulation(tree)) @test artpts == collect(1:(2^(level - 1) - 1)) + @test artpts == findall(is_articulation.(Ref(tree), vertices(tree))) end end hint = blockdiag(wheel_graph(5), wheel_graph(5)) add_edge!(hint, 5, 6) for h in test_generic_graphs(hint; eltypes=[Int, UInt8, Int16]) - @test @inferred(articulation(h)) == [5, 6] + art = @inferred(articulation(h)) + @test art == [5, 6] + @test art == findall(is_articulation.(Ref(h), vertices(h))) end end From 9a3c370bcb0df7d9bed59ec3c04efc643a5ee660 Mon Sep 17 00:00:00 2001 From: Thomas Christensen Date: Wed, 19 Jun 2024 14:30:23 +0200 Subject: [PATCH 2/6] fix typo and add test for unconnected graph --- test/biconnectivity/articulation.jl | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/biconnectivity/articulation.jl b/test/biconnectivity/articulation.jl index 38db23630..baec0c7ab 100644 --- a/test/biconnectivity/articulation.jl +++ b/test/biconnectivity/articulation.jl @@ -40,4 +40,12 @@ @test art == [5, 6] @test art == findall(is_articulation.(Ref(h), vertices(h))) end + + # graph with disconnected components + g = path_graph(5) + es = collect(edges(g)) + g2 = Graph(vcat(es, [Edge(e.src+nv(g), e.dst+nv(g)) for e in es])) + @test articulation(g) == [2,3,4] # a single connected component + @test articulation(g2) == [2,3,4,7,8,9] # two identical connected components + @test articulation(g2) == findall(is_articulation.(Ref(g2), vertices(g2))) end From 1e41e012b759b1f421a815d5d1170d76cf0059e1 Mon Sep 17 00:00:00 2001 From: Thomas Christensen Date: Wed, 19 Jun 2024 15:49:04 +0200 Subject: [PATCH 3/6] format w/ JuliaFormatter --- src/biconnectivity/articulation.jl | 15 +++++---------- test/biconnectivity/articulation.jl | 6 +++--- 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/src/biconnectivity/articulation.jl b/src/biconnectivity/articulation.jl index 25d389830..3e45c85f1 100644 --- a/src/biconnectivity/articulation.jl +++ b/src/biconnectivity/articulation.jl @@ -25,7 +25,7 @@ function articulation end s = Vector{Tuple{T,T,T}}() low = zeros(T, nv(g)) pre = zeros(T, nv(g)) - + is_articulation_pt = falses(nv(g)) @inbounds for u in vertices(g) articulation_dfs!(is_articulation_pt, g, u, s, low, pre) @@ -66,11 +66,7 @@ false ``` """ function is_articulation end -@traitfn function is_articulation( - g::AG::(!IsDirected), - v::T - ) where {T,AG<:AbstractGraph{T}} - +@traitfn function is_articulation(g::AG::(!IsDirected), v::T) where {T,AG<:AbstractGraph{T}} s = Vector{Tuple{T,T,T}}() low = zeros(T, nv(g)) pre = zeros(T, nv(g)) @@ -79,14 +75,13 @@ function is_articulation end end @traitfn function articulation_dfs!( - is_articulation_pt::Union{Nothing, BitVector}, + is_articulation_pt::Union{Nothing,BitVector}, g::AG::(!IsDirected), u::T, s::Vector{Tuple{T,T,T}}, low::Vector{T}, pre::Vector{T}, - ) where {T,AG<:AbstractGraph{T}} - +) where {T,AG<:AbstractGraph{T}} if !isnothing(is_articulation_pt) if pre[u] != 0 return is_articulation_pt @@ -160,4 +155,4 @@ end end return isnothing(is_articulation_pt) ? false : is_articulation_pt -end \ No newline at end of file +end diff --git a/test/biconnectivity/articulation.jl b/test/biconnectivity/articulation.jl index baec0c7ab..c3a0943bd 100644 --- a/test/biconnectivity/articulation.jl +++ b/test/biconnectivity/articulation.jl @@ -44,8 +44,8 @@ # graph with disconnected components g = path_graph(5) es = collect(edges(g)) - g2 = Graph(vcat(es, [Edge(e.src+nv(g), e.dst+nv(g)) for e in es])) - @test articulation(g) == [2,3,4] # a single connected component - @test articulation(g2) == [2,3,4,7,8,9] # two identical connected components + g2 = Graph(vcat(es, [Edge(e.src + nv(g), e.dst + nv(g)) for e in es])) + @test articulation(g) == [2, 3, 4] # a single connected component + @test articulation(g2) == [2, 3, 4, 7, 8, 9] # two identical connected components @test articulation(g2) == findall(is_articulation.(Ref(g2), vertices(g2))) end From 870597c634c7aa605d39cd4ffed71fe72cdc7600 Mon Sep 17 00:00:00 2001 From: Thomas Christensen Date: Wed, 19 Jun 2024 15:49:26 +0200 Subject: [PATCH 4/6] bump to v1.11.2 --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index aa670e989..cdd254f30 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "Graphs" uuid = "86223c79-3864-5bf0-83f7-82e725a168b6" -version = "1.11.1" +version = "1.11.2" [deps] ArnoldiMethod = "ec485272-7323-5ecc-a04f-4719b315124d" From 669093edcf1ac72331237e91e8233ac9f378f3a2 Mon Sep 17 00:00:00 2001 From: Thomas Christensen Date: Mon, 26 Aug 2024 09:04:30 +0200 Subject: [PATCH 5/6] docstring nits --- src/biconnectivity/articulation.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/biconnectivity/articulation.jl b/src/biconnectivity/articulation.jl index 3e45c85f1..b972160bd 100644 --- a/src/biconnectivity/articulation.jl +++ b/src/biconnectivity/articulation.jl @@ -39,8 +39,8 @@ end """ is_articulation(g, v) -Determine whether `v` is an -[articulation points](https://en.wikipedia.org/wiki/Biconnected_component) of a undirected +Determine whether `v` is an +[articulation point](https://en.wikipedia.org/wiki/Biconnected_component) of an undirected graph `g`, returning `true` if so and `false` otherwise. See also [`articulation`]. From 00f4843e9292b52e6c463f5b5f55a3a4f9d35b24 Mon Sep 17 00:00:00 2001 From: Thomas Christensen Date: Fri, 1 Nov 2024 11:37:43 +0100 Subject: [PATCH 6/6] Apply suggestions from code review Co-authored-by: Guillaume Dalle <22795598+gdalle@users.noreply.github.com> --- src/biconnectivity/articulation.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/biconnectivity/articulation.jl b/src/biconnectivity/articulation.jl index b972160bd..a85864b0b 100644 --- a/src/biconnectivity/articulation.jl +++ b/src/biconnectivity/articulation.jl @@ -2,7 +2,7 @@ articulation(g) Compute the [articulation points](https://en.wikipedia.org/wiki/Biconnected_component) (also -known as cut or seperating vertices) of an undirected graph `g` and return an array +known as cut or seperating vertices) of an undirected graph `g` and return a vector containing all the vertices of `g` that are articulation points. # Examples @@ -43,7 +43,7 @@ Determine whether `v` is an [articulation point](https://en.wikipedia.org/wiki/Biconnected_component) of an undirected graph `g`, returning `true` if so and `false` otherwise. -See also [`articulation`]. +See also [`articulation`](@ref). # Examples ```jldoctest