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" 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..a85864b0b 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 a vector +containing all the vertices of `g` that are articulation points. # Examples ```jldoctest @@ -22,74 +23,136 @@ 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}}() - is_articulation_pt = falses(nv(g)) low = zeros(T, nv(g)) pre = zeros(T, nv(g)) + is_articulation_pt = falses(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 + 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 point](https://en.wikipedia.org/wiki/Biconnected_component) of an undirected +graph `g`, returning `true` if so and `false` otherwise. + +See also [`articulation`](@ref). + +# 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)) + + 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 + return isnothing(is_articulation_pt) ? false : is_articulation_pt end diff --git a/test/biconnectivity/articulation.jl b/test/biconnectivity/articulation.jl index 0ebaff9bd..c3a0943bd 100644 --- a/test/biconnectivity/articulation.jl +++ b/test/biconnectivity/articulation.jl @@ -22,18 +22,30 @@ 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 + + # 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