Skip to content

Commit 1a7594a

Browse files
authored
Prufer coding for trees (#206)
* Add the function `is_tree` * Add the functions `pruefer_encode` and `pruefer_decode` * Add the function `uniform_tree`
1 parent 9ee3a21 commit 1a7594a

File tree

10 files changed

+233
-3
lines changed

10 files changed

+233
-3
lines changed

docs/make.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ pages_files = [
5252
"algorithms/connectivity.md",
5353
"algorithms/cut.md",
5454
"algorithms/cycles.md",
55+
"algorithms/trees.md",
5556
"algorithms/degeneracy.md",
5657
"algorithms/digraph.md",
5758
"algorithms/distance.md",

docs/src/algorithms/cycles.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Cycles
22

3-
_Graphs.jl_ contains numerous algorithms related to [cycles](https://en.wikipedia.org/wiki/Cycle_(graph_theory)).
3+
_Graphs.jl_ contains numerous algorithms related to [cycles](https://en.wikipedia.org/wiki/Cycle_(graph_theory)).
44

55
## Index
66

docs/src/algorithms/trees.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Trees
2+
3+
_Graphs.jl_ algorithms related to [trees](https://en.wikipedia.org/wiki/Tree_(graph_theory)).
4+
5+
## Index
6+
7+
```@index
8+
Pages = ["trees.md"]
9+
```
10+
11+
## Full docs
12+
13+
```@autodocs
14+
Modules = [Graphs]
15+
Pages = [
16+
"trees/prufer.jl",
17+
]
18+
19+
```

src/Graphs.jl

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@ using DataStructures:
2121
in_same_set,
2222
peek,
2323
union!,
24-
find_root!
24+
find_root!,
25+
BinaryMaxHeap,
26+
BinaryMinHeap
2527
using LinearAlgebra: I, Symmetric, diagm, eigen, eigvals, norm, rmul!, tril, triu
2628
import LinearAlgebra: Diagonal, issymmetric, mul!
2729
using Random:
@@ -280,6 +282,7 @@ export
280282
watts_strogatz,
281283
newman_watts_strogatz,
282284
random_regular_graph,
285+
uniform_tree,
283286
random_regular_digraph,
284287
random_configuration_model,
285288
random_tournament_digraph,
@@ -392,6 +395,11 @@ export
392395
kruskal_mst,
393396
prim_mst,
394397

398+
# trees and prufer
399+
is_tree,
400+
prufer_encode,
401+
prufer_decode,
402+
395403
# steinertree
396404
steiner_tree,
397405

@@ -514,6 +522,7 @@ include("community/rich_club.jl")
514522
include("spanningtrees/boruvka.jl")
515523
include("spanningtrees/kruskal.jl")
516524
include("spanningtrees/prim.jl")
525+
include("trees/prufer.jl")
517526
include("steinertree/steiner_tree.jl")
518527
include("biconnectivity/articulation.jl")
519528
include("biconnectivity/biconnect.jl")

src/SimpleGraphs/SimpleGraphs.jl

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ import Graphs:
3636
num_self_loops,
3737
insorted,
3838
squash,
39-
rng_from_rng_or_seed
39+
rng_from_rng_or_seed,
40+
prufer_decode
4041

4142
export AbstractSimpleGraph,
4243
AbstractSimpleEdge,
@@ -61,6 +62,7 @@ export AbstractSimpleGraph,
6162
random_regular_graph,
6263
random_regular_digraph,
6364
random_configuration_model,
65+
uniform_tree,
6466
random_tournament_digraph,
6567
StochasticBlockModel,
6668
make_edgestream,

src/SimpleGraphs/generators/randgraphs.jl

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -967,6 +967,27 @@ function random_configuration_model(
967967
end
968968
return g
969969
end
970+
"""
971+
uniform_tree(n)
972+
973+
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).
974+
975+
### Optional Arguments
976+
- `rng=nothing`: set the Random Number Generator.
977+
978+
# Examples
979+
```jldoctest
980+
julia> uniform_tree(10)
981+
{10, 9} undirected simple Int64 graph
982+
```
983+
"""
984+
function uniform_tree(n::Integer; rng::Union{Nothing,AbstractRNG}=nothing)
985+
n <= 1 && return Graph(n)
986+
n == 2 && return path_graph(n)
987+
rng = rng_from_rng_or_seed(rng, nothing)
988+
random_code = rand(rng, Base.OneTo(n), n - 2)
989+
return prufer_decode(random_code)
990+
end
970991

971992
"""
972993
random_regular_digraph(n, k)

src/trees/prufer.jl

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
"""
2+
is_tree(g)
3+
4+
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.
5+
6+
This function does not apply to directed graphs. Directed trees are sometimes called [polytrees](https://en.wikipedia.org/wiki/Polytree)).
7+
8+
"""
9+
function is_tree end
10+
11+
@traitfn function is_tree(g::::(!IsDirected))
12+
return ne(g) == nv(g) - 1 && is_connected(g)
13+
end
14+
15+
function _is_prufer(c::AbstractVector{T}) where {T<:Integer}
16+
return isempty(c) || (minimum(c) >= 1 && maximum(c) <= length(c) + 2)
17+
end
18+
19+
function _degree_from_prufer(c::Vector{T})::Vector{T} where {T<:Integer}
20+
"""
21+
Degree sequence from Prüfer code.
22+
Returns d such that d[i] = 1 + number of occurences of i in c
23+
"""
24+
n = length(c) + 2
25+
d = ones(T, n)
26+
for value in c
27+
d[value] += 1
28+
end
29+
return d
30+
end
31+
32+
"""
33+
prufer_decode(code)
34+
35+
Returns the unique tree associated with the given (Prüfer) code.
36+
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.
37+
Ref: [Prüfer sequence on Wikipedia](https://en.wikipedia.org/wiki/Pr%C3%BCfer_sequence)
38+
39+
"""
40+
function prufer_decode(code::AbstractVector{T})::SimpleGraph{T} where {T<:Integer}
41+
!_is_prufer(code) && throw(
42+
ArgumentError("The code must have one dimension and must be a Prüfer sequence. "),
43+
)
44+
isempty(code) && return path_graph(T(2)) # the empty Prüfer sequence codes for the one-edge tree
45+
46+
n = length(code) + 2
47+
d = _degree_from_prufer(code)
48+
L = BinaryMinHeap{T}(findall(==(1), d))
49+
g = Graph{T}(n, 0)
50+
51+
for i in 1:(n - 2)
52+
l = pop!(L) # extract leaf with priority rule (max)
53+
d[l] -= 1 # update degree sequence
54+
add_edge!(g, l, code[i]) # add edge
55+
d[code[i]] -= 1 # update degree sequence
56+
d[code[i]] == 1 && push!(L, code[i]) # add new leaf if any
57+
end
58+
59+
add_edge!(g, pop!(L), pop!(L)) # add last leaf
60+
61+
return g
62+
end
63+
64+
"""
65+
prufer_encode(g::SimpleGraph)
66+
67+
Given a tree (a connected minimal undirected graph) of size n⩾3, returns the unique Prüfer sequence associated with this tree.
68+
69+
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.
70+
71+
Ref: [Prüfer sequence on Wikipedia](https://en.wikipedia.org/wiki/Pr%C3%BCfer_sequence)
72+
73+
"""
74+
function prufer_encode(G::SimpleGraph{T})::Vector{T} where {T<:Integer}
75+
(nv(G) < 2 || !is_tree(G)) &&
76+
throw(ArgumentError("The graph must be a tree with n ⩾ 2 vertices. "))
77+
78+
n = nv(G)
79+
n == 2 && return Vector{T}() # empty Prüfer sequence
80+
g = copy(G)
81+
code = zeros(T, n - 2)
82+
d = degree(g)
83+
L = BinaryMinHeap(findall(==(1), d))
84+
85+
for i in 1:(n - 2)
86+
u = pop!(L)
87+
v = neighbors(g, u)[1]
88+
rem_edge!(g, u, v)
89+
d[u] -= 1
90+
d[v] -= 1
91+
d[v] == 1 && push!(L, v)
92+
code[i] = v
93+
end
94+
95+
return code
96+
end

test/runtests.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ tests = [
116116
"independentset/maximal_ind_set",
117117
"vertexcover/degree_vertex_cover",
118118
"vertexcover/random_vertex_cover",
119+
"trees/prufer",
119120
"experimental/experimental",
120121
]
121122

test/simplegraphs/generators/randgraphs.jl

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,18 @@
245245
@test is_directed(rd)
246246
end
247247

248+
@testset "uniform trees" begin
249+
t = uniform_tree(50; rng=rng)
250+
@test nv(t) == 50
251+
@test ne(t) == 49
252+
@test is_tree(t)
253+
254+
t2 = uniform_tree(50; rng=StableRNG(4))
255+
@test nv(t2) == 50
256+
@test ne(t2) == 49
257+
@test is_tree(t2)
258+
end
259+
248260
@testset "random configuration model" begin
249261
rr = random_configuration_model(10, repeat([2, 4], 5); rng=StableRNG(3))
250262
@test nv(rr) == 10

test/trees/prufer.jl

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
function _harmonize_type(g::AbstractGraph{T}, c::Vector{S}) where {T,S<:Integer}
2+
return convert(Vector{T}, c)
3+
end
4+
5+
@testset "Tree utilities" begin
6+
t1 = Graph(6)
7+
for e in [(1, 4), (2, 4), (3, 4), (4, 5), (5, 6)]
8+
add_edge!(t1, e...)
9+
end
10+
code = [4, 4, 4, 5]
11+
12+
t2 = path_graph(2)
13+
t3 = star_graph(10)
14+
t4 = binary_tree(3)
15+
16+
f1 = Graph(8, 0) #forest with 4 tree components
17+
for e in [(1, 2), (2, 3), (4, 5), (6, 7)]
18+
add_edge!(f1, e...)
19+
end
20+
21+
g1 = cycle_graph(8)
22+
g2 = complete_graph(4)
23+
g3 = Graph(1, 0)
24+
g4 = Graph(2, 0)
25+
g5 = Graph(1, 0)
26+
add_edge!(g5, 1, 1) # loop
27+
28+
d1 = cycle_digraph(5)
29+
d2 = path_digraph(5)
30+
d3 = DiGraph(2)
31+
add_edge!(d3, 1, 2)
32+
add_edge!(d3, 2, 1)
33+
34+
@testset "tree_check" begin
35+
for t in testgraphs(t1, t2, t3, t4, g3)
36+
@test is_tree(t)
37+
end
38+
for g in testgraphs(g1, g2, g4, g5, f1)
39+
@test !is_tree(g)
40+
end
41+
for g in testgraphs(d1, d2, d3)
42+
@test_throws MethodError is_tree(g)
43+
end
44+
end
45+
46+
@testset "encode/decode" begin
47+
@test prufer_decode(code) == t1
48+
@test prufer_encode(t1) == code
49+
for g in testgraphs(t2, t3, t4)
50+
ret_code = prufer_encode(g)
51+
@test prufer_decode(ret_code) == g
52+
end
53+
end
54+
55+
@testset "errors" begin
56+
b1 = [5, 8, 10, 1, 2]
57+
b2 = [0.5, 1.1]
58+
@test_throws ArgumentError prufer_decode(b1)
59+
@test_throws MethodError prufer_decode(b2)
60+
61+
for g in testgraphs(g1, g2, g3, g4, g5, f1)
62+
@test_throws ArgumentError prufer_encode(g)
63+
end
64+
65+
for g in testgraphs(d1, d2, d3)
66+
@test_throws MethodError prufer_encode(g)
67+
end
68+
end
69+
end

0 commit comments

Comments
 (0)