Skip to content

Prufer coding for trees #206

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 28 commits into from
Jan 6, 2023
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
5ee514c
add is_tree func + exports
SimonCoste Dec 30, 2022
1567bfc
add doc for exported functions
SimonCoste Dec 30, 2022
3bee0c1
add test structure (wip)
SimonCoste Dec 30, 2022
8cf308a
treesize must be >2
SimonCoste Dec 31, 2022
8cb4069
fixed types
SimonCoste Dec 31, 2022
887f8e0
prufer tests
SimonCoste Dec 31, 2022
1078143
renaming prufer to trees
SimonCoste Dec 31, 2022
56c00f6
added docs
SimonCoste Dec 31, 2022
98fdf29
@traitfn for is_tree
SimonCoste Jan 1, 2023
6f1eb44
Update src/trees/prufer.jl
SimonCoste Jan 1, 2023
1cf22a8
better input types
SimonCoste Jan 1, 2023
a8fa527
Support for empty sequence / 1-edge tree
SimonCoste Jan 1, 2023
18765b5
update doc for the 1-edge tree
SimonCoste Jan 1, 2023
3c88fd3
formatting
SimonCoste Jan 2, 2023
b048c2f
fix bug for 2-nodes trees
SimonCoste Jan 2, 2023
9e44c46
strengthen return types
SimonCoste Jan 2, 2023
f1628bb
convention for naming vertices
SimonCoste Jan 2, 2023
e81f6b7
Initialize empty graphs with zero edges
SimonCoste Jan 2, 2023
fe6303e
various small improvements
SimonCoste Jan 4, 2023
27f497f
added edge tests (forest, empty trees, self-loops)
SimonCoste Jan 4, 2023
fc93e6f
uniform tree generator
SimonCoste Jan 4, 2023
5d1a1fb
tests for uniform trees
SimonCoste Jan 4, 2023
4a2d916
revert some silly code
SimonCoste Jan 4, 2023
be8be2c
fixed typos in docs
SimonCoste Jan 4, 2023
d2e6f56
removed trailing space
SimonCoste Jan 4, 2023
e59a993
determine return type from arg in uniform tree
SimonCoste Jan 4, 2023
c635042
removed seed from arg
SimonCoste Jan 4, 2023
426d438
added missing arg in rng_from_rng_or_seed
SimonCoste Jan 5, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/make.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion docs/src/algorithms/cycles.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down
19 changes: 19 additions & 0 deletions docs/src/algorithms/trees.md
Original file line number Diff line number Diff line change
@@ -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",
]

```
11 changes: 10 additions & 1 deletion src/Graphs.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -280,6 +282,7 @@ export
watts_strogatz,
newman_watts_strogatz,
random_regular_graph,
uniform_tree,
random_regular_digraph,
random_configuration_model,
random_tournament_digraph,
Expand Down Expand Up @@ -392,6 +395,11 @@ export
kruskal_mst,
prim_mst,

# trees and prufer
is_tree,
prufer_encode,
prufer_decode,

# steinertree
steiner_tree,

Expand Down Expand Up @@ -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")
Expand Down
4 changes: 3 additions & 1 deletion src/SimpleGraphs/SimpleGraphs.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -61,6 +62,7 @@ export AbstractSimpleGraph,
random_regular_graph,
random_regular_digraph,
random_configuration_model,
uniform_tree,
random_tournament_digraph,
StochasticBlockModel,
make_edgestream,
Expand Down
27 changes: 27 additions & 0 deletions src/SimpleGraphs/generators/randgraphs.jl
Original file line number Diff line number Diff line change
Expand Up @@ -967,6 +967,33 @@ 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.
- `seed=nothing`: set the RNG seed.

# Examples
```jldoctest
julia> uniform_tree(10)
{10, 9} undirected simple Int64 graph
```
"""
function uniform_tree(
n::Integer;
rng::Union{Nothing,AbstractRNG}=nothing,
seed::Union{Nothing,Integer}=nothing,
)
n == 1 && return Graph(1, 0)
n == 2 && return path_graph(2)

rng = rng_from_rng_or_seed(rng, seed)
random_code = rand(rng, 1:n, n - 2)
return prufer_decode(random_code)
end

"""
random_regular_digraph(n, k)
Expand Down
96 changes: 96 additions & 0 deletions src/trees/prufer.jl
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ tests = [
"independentset/maximal_ind_set",
"vertexcover/degree_vertex_cover",
"vertexcover/random_vertex_cover",
"trees/prufer",
"experimental/experimental",
]

Expand Down
12 changes: 12 additions & 0 deletions test/simplegraphs/generators/randgraphs.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
69 changes: 69 additions & 0 deletions test/trees/prufer.jl
Original file line number Diff line number Diff line change
@@ -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