diff --git a/docs/make.jl b/docs/make.jl index 7e40b3a68..d7f62b704 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -52,6 +52,7 @@ pages_files = [ "algorithms/connectivity.md", "algorithms/cut.md", "algorithms/cycles.md", + "algorithms/trees.md", "algorithms/degeneracy.md", "algorithms/digraph.md", "algorithms/distance.md", diff --git a/docs/src/algorithms/cycles.md b/docs/src/algorithms/cycles.md index 274eb6875..cdc0c43be 100644 --- a/docs/src/algorithms/cycles.md +++ b/docs/src/algorithms/cycles.md @@ -1,6 +1,6 @@ # Cycles -_Graphs.jl_ contains numerous algorithms related to [cycles](https://en.wikipedia.org/wiki/Cycle_(graph_theory)). +_Graphs.jl_ contains numerous algorithms related to [cycles](https://en.wikipedia.org/wiki/Cycle_(graph_theory)). ## Index diff --git a/docs/src/algorithms/trees.md b/docs/src/algorithms/trees.md new file mode 100644 index 000000000..2e1779bda --- /dev/null +++ b/docs/src/algorithms/trees.md @@ -0,0 +1,19 @@ +# Trees + +_Graphs.jl_ algorithms related to [trees](https://en.wikipedia.org/wiki/Tree_(graph_theory)). + +## Index + +```@index +Pages = ["trees.md"] +``` + +## Full docs + +```@autodocs +Modules = [Graphs] +Pages = [ + "trees/prufer.jl", +] + +``` diff --git a/src/Graphs.jl b/src/Graphs.jl index 171204112..7e42715c1 100644 --- a/src/Graphs.jl +++ b/src/Graphs.jl @@ -21,7 +21,9 @@ using DataStructures: in_same_set, peek, union!, - find_root! + find_root!, + BinaryMaxHeap, + BinaryMinHeap using LinearAlgebra: I, Symmetric, diagm, eigen, eigvals, norm, rmul!, tril, triu import LinearAlgebra: Diagonal, issymmetric, mul! using Random: @@ -280,6 +282,7 @@ export watts_strogatz, newman_watts_strogatz, random_regular_graph, + uniform_tree, random_regular_digraph, random_configuration_model, random_tournament_digraph, @@ -392,6 +395,11 @@ export kruskal_mst, prim_mst, + # trees and prufer + is_tree, + prufer_encode, + prufer_decode, + # steinertree steiner_tree, @@ -514,6 +522,7 @@ include("community/rich_club.jl") include("spanningtrees/boruvka.jl") include("spanningtrees/kruskal.jl") include("spanningtrees/prim.jl") +include("trees/prufer.jl") include("steinertree/steiner_tree.jl") include("biconnectivity/articulation.jl") include("biconnectivity/biconnect.jl") diff --git a/src/SimpleGraphs/SimpleGraphs.jl b/src/SimpleGraphs/SimpleGraphs.jl index d60d56436..bcb7c0c97 100644 --- a/src/SimpleGraphs/SimpleGraphs.jl +++ b/src/SimpleGraphs/SimpleGraphs.jl @@ -36,7 +36,8 @@ import Graphs: num_self_loops, insorted, squash, - rng_from_rng_or_seed + rng_from_rng_or_seed, + prufer_decode export AbstractSimpleGraph, AbstractSimpleEdge, @@ -61,6 +62,7 @@ export AbstractSimpleGraph, random_regular_graph, random_regular_digraph, random_configuration_model, + uniform_tree, random_tournament_digraph, StochasticBlockModel, make_edgestream, diff --git a/src/SimpleGraphs/generators/randgraphs.jl b/src/SimpleGraphs/generators/randgraphs.jl index efc122a87..8fc22b768 100644 --- a/src/SimpleGraphs/generators/randgraphs.jl +++ b/src/SimpleGraphs/generators/randgraphs.jl @@ -967,6 +967,27 @@ function random_configuration_model( end return g end +""" + uniform_tree(n) + +Generates a random labelled tree, drawn uniformly at random over the ``n^{n-2}`` such trees. A uniform word of length `n-2` over the alphabet `1:n` is generated (Prüfer sequence) then decoded. See also the `prufer_decode` function and [this page on Prüfer codes](https://en.wikipedia.org/wiki/Pr%C3%BCfer_sequence). + +### Optional Arguments +- `rng=nothing`: set the Random Number Generator. + +# Examples +```jldoctest +julia> uniform_tree(10) +{10, 9} undirected simple Int64 graph +``` +""" +function uniform_tree(n::Integer; rng::Union{Nothing,AbstractRNG}=nothing) + n <= 1 && return Graph(n) + n == 2 && return path_graph(n) + rng = rng_from_rng_or_seed(rng, nothing) + random_code = rand(rng, Base.OneTo(n), n - 2) + return prufer_decode(random_code) +end """ random_regular_digraph(n, k) diff --git a/src/trees/prufer.jl b/src/trees/prufer.jl new file mode 100644 index 000000000..b3046ba2a --- /dev/null +++ b/src/trees/prufer.jl @@ -0,0 +1,96 @@ +""" + is_tree(g) + +Returns true if g is a tree: that is, a simple, connected undirected graph, with nv-1 edges (nv = number of vertices). Trees are the minimal connected graphs; equivalently they have no cycles. + +This function does not apply to directed graphs. Directed trees are sometimes called [polytrees](https://en.wikipedia.org/wiki/Polytree)). + +""" +function is_tree end + +@traitfn function is_tree(g::::(!IsDirected)) + return ne(g) == nv(g) - 1 && is_connected(g) +end + +function _is_prufer(c::AbstractVector{T}) where {T<:Integer} + return isempty(c) || (minimum(c) >= 1 && maximum(c) <= length(c) + 2) +end + +function _degree_from_prufer(c::Vector{T})::Vector{T} where {T<:Integer} + """ + Degree sequence from Prüfer code. + Returns d such that d[i] = 1 + number of occurences of i in c + """ + n = length(c) + 2 + d = ones(T, n) + for value in c + d[value] += 1 + end + return d +end + +""" + prufer_decode(code) + +Returns the unique tree associated with the given (Prüfer) code. +Each tree of size n is associated with a Prüfer sequence (a[1], ..., a[n-2]) with 1 ⩽ a[i] ⩽ n. The sequence is constructed recursively by the leaf removal algoritm. At step k, the leaf with smallest index is removed and its unique neighbor is added to the Prüfer sequence, giving a[k]. The decoding algorithm goes backward. The tree associated with the empty sequence is the 2-vertices tree with one edge. +Ref: [Prüfer sequence on Wikipedia](https://en.wikipedia.org/wiki/Pr%C3%BCfer_sequence) + +""" +function prufer_decode(code::AbstractVector{T})::SimpleGraph{T} where {T<:Integer} + !_is_prufer(code) && throw( + ArgumentError("The code must have one dimension and must be a Prüfer sequence. "), + ) + isempty(code) && return path_graph(T(2)) # the empty Prüfer sequence codes for the one-edge tree + + n = length(code) + 2 + d = _degree_from_prufer(code) + L = BinaryMinHeap{T}(findall(==(1), d)) + g = Graph{T}(n, 0) + + for i in 1:(n - 2) + l = pop!(L) # extract leaf with priority rule (max) + d[l] -= 1 # update degree sequence + add_edge!(g, l, code[i]) # add edge + d[code[i]] -= 1 # update degree sequence + d[code[i]] == 1 && push!(L, code[i]) # add new leaf if any + end + + add_edge!(g, pop!(L), pop!(L)) # add last leaf + + return g +end + +""" + prufer_encode(g::SimpleGraph) + +Given a tree (a connected minimal undirected graph) of size n⩾3, returns the unique Prüfer sequence associated with this tree. + +Each tree of size n ⩾ 2 is associated with a Prüfer sequence (a[1], ..., a[n-2]) with 1 ⩽ a[i] ⩽ n. The sequence is constructed recursively by the leaf removal algoritm. At step k, the leaf with smallest index is removed and its unique neighbor is added to the Prüfer sequence, giving a[k]. The Prüfer sequence of the tree with only one edge is the empty sequence. + +Ref: [Prüfer sequence on Wikipedia](https://en.wikipedia.org/wiki/Pr%C3%BCfer_sequence) + +""" +function prufer_encode(G::SimpleGraph{T})::Vector{T} where {T<:Integer} + (nv(G) < 2 || !is_tree(G)) && + throw(ArgumentError("The graph must be a tree with n ⩾ 2 vertices. ")) + + n = nv(G) + n == 2 && return Vector{T}() # empty Prüfer sequence + g = copy(G) + code = zeros(T, n - 2) + d = degree(g) + L = BinaryMinHeap(findall(==(1), d)) + + for i in 1:(n - 2) + u = pop!(L) + v = neighbors(g, u)[1] + rem_edge!(g, u, v) + d[u] -= 1 + d[v] -= 1 + d[v] == 1 && push!(L, v) + code[i] = v + end + + return code +end diff --git a/test/runtests.jl b/test/runtests.jl index 786bdb949..de706154c 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -116,6 +116,7 @@ tests = [ "independentset/maximal_ind_set", "vertexcover/degree_vertex_cover", "vertexcover/random_vertex_cover", + "trees/prufer", "experimental/experimental", ] diff --git a/test/simplegraphs/generators/randgraphs.jl b/test/simplegraphs/generators/randgraphs.jl index 0932a6775..8004b7cc8 100644 --- a/test/simplegraphs/generators/randgraphs.jl +++ b/test/simplegraphs/generators/randgraphs.jl @@ -245,6 +245,18 @@ @test is_directed(rd) end + @testset "uniform trees" begin + t = uniform_tree(50; rng=rng) + @test nv(t) == 50 + @test ne(t) == 49 + @test is_tree(t) + + t2 = uniform_tree(50; rng=StableRNG(4)) + @test nv(t2) == 50 + @test ne(t2) == 49 + @test is_tree(t2) + end + @testset "random configuration model" begin rr = random_configuration_model(10, repeat([2, 4], 5); rng=StableRNG(3)) @test nv(rr) == 10 diff --git a/test/trees/prufer.jl b/test/trees/prufer.jl new file mode 100644 index 000000000..b39d9de8e --- /dev/null +++ b/test/trees/prufer.jl @@ -0,0 +1,69 @@ +function _harmonize_type(g::AbstractGraph{T}, c::Vector{S}) where {T,S<:Integer} + return convert(Vector{T}, c) +end + +@testset "Tree utilities" begin + t1 = Graph(6) + for e in [(1, 4), (2, 4), (3, 4), (4, 5), (5, 6)] + add_edge!(t1, e...) + end + code = [4, 4, 4, 5] + + t2 = path_graph(2) + t3 = star_graph(10) + t4 = binary_tree(3) + + f1 = Graph(8, 0) #forest with 4 tree components + for e in [(1, 2), (2, 3), (4, 5), (6, 7)] + add_edge!(f1, e...) + end + + g1 = cycle_graph(8) + g2 = complete_graph(4) + g3 = Graph(1, 0) + g4 = Graph(2, 0) + g5 = Graph(1, 0) + add_edge!(g5, 1, 1) # loop + + d1 = cycle_digraph(5) + d2 = path_digraph(5) + d3 = DiGraph(2) + add_edge!(d3, 1, 2) + add_edge!(d3, 2, 1) + + @testset "tree_check" begin + for t in testgraphs(t1, t2, t3, t4, g3) + @test is_tree(t) + end + for g in testgraphs(g1, g2, g4, g5, f1) + @test !is_tree(g) + end + for g in testgraphs(d1, d2, d3) + @test_throws MethodError is_tree(g) + end + end + + @testset "encode/decode" begin + @test prufer_decode(code) == t1 + @test prufer_encode(t1) == code + for g in testgraphs(t2, t3, t4) + ret_code = prufer_encode(g) + @test prufer_decode(ret_code) == g + end + end + + @testset "errors" begin + b1 = [5, 8, 10, 1, 2] + b2 = [0.5, 1.1] + @test_throws ArgumentError prufer_decode(b1) + @test_throws MethodError prufer_decode(b2) + + for g in testgraphs(g1, g2, g3, g4, g5, f1) + @test_throws ArgumentError prufer_encode(g) + end + + for g in testgraphs(d1, d2, d3) + @test_throws MethodError prufer_encode(g) + end + end +end