Skip to content

Commit 1cb789d

Browse files
authored
Eulerian cycles/trails for undirected graphs (#232)
* add `eulerian` to compute Eulerian cycles/tours for undirected graphs * simplify API: don't allow end vertex as input * actually include new tests in "test/runtests.jl" * nit * Doc nit * fix `@test_throws` use on <v1.8 * fix^N * code review
1 parent 27d9763 commit 1cb789d

File tree

6 files changed

+154
-0
lines changed

6 files changed

+154
-0
lines changed

docs/src/algorithms/traversals.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,5 +20,6 @@ Pages = [
2020
"traversals/greedy_color.jl",
2121
"traversals/maxadjvisit.jl",
2222
"traversals/randomwalks.jl",
23+
"traversals/eulerian.jl",
2324
]
2425
```

src/Graphs.jl

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,9 @@ export
194194
diffusion,
195195
diffusion_rate,
196196

197+
# eulerian
198+
eulerian,
199+
197200
# coloring
198201
greedy_color,
199202

@@ -488,6 +491,7 @@ include("traversals/dfs.jl")
488491
include("traversals/maxadjvisit.jl")
489492
include("traversals/randomwalks.jl")
490493
include("traversals/diffusion.jl")
494+
include("traversals/eulerian.jl")
491495
include("connectivity.jl")
492496
include("distance.jl")
493497
include("editdist.jl")

src/Test/Test.jl

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@ struct GenericGraph{T} <: Graphs.AbstractGraph{T}
3636
g::SimpleGraph{T}
3737
end
3838

39+
function GenericGraph(elist::Vector{Graphs.SimpleGraphEdge{T}}) where {T<:Integer}
40+
GenericGraph{T}(SimpleGraph(elist))
41+
end
42+
3943
"""
4044
GenericDiGraph{T} <: Graphs.AbstractGraph{T}
4145
@@ -46,6 +50,10 @@ struct GenericDiGraph{T} <: Graphs.AbstractGraph{T}
4650
g::SimpleDiGraph{T}
4751
end
4852

53+
function GenericDiGraph(elist::Vector{Graphs.SimpleDiGraphEdge{T}}) where {T<:Integer}
54+
GenericDiGraph{T}(SimpleDiGraph(elist))
55+
end
56+
4957
Graphs.is_directed(::Type{<:GenericGraph}) = false
5058
Graphs.is_directed(::Type{<:GenericDiGraph}) = true
5159

src/traversals/eulerian.jl

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
# Adapted from SimpleGraphs.jl [Copyright (c) 2014, Ed Scheinerman]:
2+
# https://github.com/scheinerman/SimpleGraphs.jl/blob/master/src/simple_euler.jl
3+
# Reproduced under the MIT Expat License.
4+
5+
"""
6+
eulerian(g::AbstractSimpleGraph{T}[, u::T]) --> T[]
7+
8+
Returns a [Eulerian trail or cycle](https://en.wikipedia.org/wiki/Eulerian_path) through an
9+
undirected graph `g`, starting at vertex `u`, returning a vector listing the vertices of `g`
10+
in the order that they are traversed. If no such trail or cycle exists, throws an error.
11+
12+
A Eulerian trail or cycle is a path that visits every edge of `g` exactly once; for a
13+
cycle, the path starts _and_ ends at vertex `u`.
14+
15+
## Optional arguments
16+
- If `u` is omitted, a Eulerian trail or cycle is computed with `u = first(vertices(g))`.
17+
"""
18+
function eulerian(g::AbstractGraph{T}, u::T=first(vertices(g))) where {T}
19+
is_directed(g) && error("`eulerian` is not yet implemented for directed graphs")
20+
21+
_check_eulerian_input(g, u) # perform basic sanity checks
22+
23+
g′ = SimpleGraph{T}(nv(g)) # copy `g` (mutated in `_eulerian!`)
24+
for e in edges(g)
25+
add_edge!(g′, src(e), dst(e))
26+
end
27+
28+
return _eulerian!(g′, u)
29+
end
30+
31+
@traitfn function _eulerian!(g::AG::(!IsDirected), u::T) where {T, AG<:AbstractGraph{T}}
32+
# TODO: This uses Fleury's algorithm which is O(|E|²) in the number of edges |E|.
33+
# Hierholzer's algorithm [https://en.wikipedia.org/wiki/Eulerian_path#Hierholzer's_algorithm]
34+
# is presumably faster, running in O(|E|) time, but requires needing to keep track
35+
# of visited/nonvisited sites in a doubly-linked list/deque.
36+
trail = T[]
37+
38+
nverts = nv(g)
39+
while true
40+
# if last vertex
41+
if nverts == 1
42+
push!(trail, u)
43+
return trail
44+
end
45+
46+
Nu = neighbors(g, u)
47+
if length(Nu) == 1
48+
# if only one neighbor, delete and move on
49+
w = first(Nu)
50+
rem_edge!(g, u, w)
51+
nverts -= 1
52+
push!(trail, u)
53+
u = w
54+
elseif length(Nu) == 0
55+
error("graph is not connected: a eulerian cycle/trail does not exist")
56+
else
57+
# otherwise, pick whichever neighbor is not a bridge/cut-edge
58+
bs = bridges(g)
59+
for w in Nu
60+
if all(e -> _excludes_edge(u, w, e), bs)
61+
# not a bridge/cut-edge; add to trail
62+
rem_edge!(g, u, w)
63+
push!(trail, u)
64+
u = w
65+
break
66+
end
67+
end
68+
end
69+
end
70+
error("unreachable reached")
71+
end
72+
73+
@inline function _excludes_edge(u, w, e::AbstractEdge)
74+
# `true` if `e` is not `Edge(u,w)` or `Edge(w,u)`, otherwise `false`
75+
s, d = src(e), dst(e)
76+
return !((u == s && w == d) || (u == d && w == s))
77+
end
78+
79+
function _check_eulerian_input(g, u)
80+
if !has_vertex(g, u)
81+
error("starting vertex is not in the graph")
82+
end
83+
84+
# special case: if any vertex has degree zero
85+
if any(x->degree(g, x) == 0, vertices(g))
86+
error("some vertices have degree zero (are isolated) and cannot be reached")
87+
end
88+
89+
# vertex degree checks
90+
du = degree(g, u)
91+
if iseven(du) # cycle: start (u) == stop (v) - all nodes must have even degree
92+
if any(x -> isodd(degree(g, x)), vertices(g))
93+
error("starting vertex has even degree but there are other vertices with odd degree: a eulerian cycle does not exist")
94+
end
95+
else # isodd(du) # trail: start (u) != stop (v) - all nodes, except u and v, must have even degree
96+
if count(x -> iseven(degree(g, x)), vertices(g)) != 2
97+
error("starting vertex has odd degree but the total number of vertices of odd degree is not equal to 2: a eulerian trail does not exist")
98+
end
99+
end
100+
101+
# to reduce cost, the graph connectivity check is performed in `_eulerian!` rather
102+
# than through `is_connected(g)`
103+
end

test/runtests.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ tests = [
116116
"traversals/maxadjvisit",
117117
"traversals/randomwalks",
118118
"traversals/diffusion",
119+
"traversals/eulerian",
119120
"community/cliques",
120121
"community/core-periphery",
121122
"community/label_propagation",

test/traversals/eulerian.jl

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
@testset "Eulerian tours/cycles" begin
2+
# a cycle (identical start/end)
3+
g0 = GenericGraph([Edge(1,2), Edge(2,3), Edge(3,1)])
4+
@test eulerian(g0, 1) == eulerian(g0)
5+
@test last(eulerian(g0, 1)) == 1 # a cycle
6+
7+
# a tour (different start/end)
8+
g1 = GenericGraph([Edge(1,2), Edge(2,3), Edge(3,4)])
9+
@test eulerian(g1, 1) == [1,2,3,4]
10+
@test_throws ErrorException("starting vertex has even degree but there are other vertices with odd degree: a eulerian cycle does not exist") eulerian(g1, 2)
11+
12+
# a cycle with a node (vertex 2) with multiple neighbors
13+
g2 = GenericGraph([Edge(1,2), Edge(2,3), Edge(3,4), Edge(4,1), Edge(2,5), Edge(5,6),
14+
Edge(6,2)])
15+
@test eulerian(g2) == eulerian(g2, 1) == [1, 2, 5, 6, 2, 3, 4, 1]
16+
17+
# graph with odd-degree vertices
18+
g3 = GenericGraph([Edge(1,2), Edge(2,3), Edge(3,4), Edge(2,4), Edge(4,1), Edge(4,2)])
19+
@test_throws ErrorException("starting vertex has even degree but there are other vertices with odd degree: a eulerian cycle does not exist") eulerian(g3, 1)
20+
21+
# start/end point not in graph
22+
@test_throws ErrorException("starting vertex is not in the graph") eulerian(g3, 5)
23+
24+
# disconnected components
25+
g4 = GenericGraph([Edge(1,2), Edge(2,3), Edge(3,1), # component 1
26+
Edge(4,5), Edge(5,6), Edge(6,4)]) # component 2
27+
@test_throws ErrorException("graph is not connected: a eulerian cycle/trail does not exist") eulerian(g4)
28+
29+
# zero-degree nodes
30+
g5′ = SimpleGraph(4)
31+
add_edge!(g5′, Edge(1,2)); add_edge!(g5′, Edge(2,3)); add_edge!(g5′, Edge(3,1))
32+
g5 = GenericGraph(g5′)
33+
@test_throws ErrorException("some vertices have degree zero (are isolated) and cannot be reached") eulerian(g5)
34+
35+
# not yet implemented for directed graphs
36+
@test_broken eulerian(GenericDiGraph([Edge(1,2), Edge(2,3), Edge(3,1)]))
37+
end

0 commit comments

Comments
 (0)