Skip to content

Commit

Permalink
Eulerian cycles/trails for undirected graphs (#232)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
thchr committed Jul 27, 2023
1 parent 27d9763 commit 1cb789d
Show file tree
Hide file tree
Showing 6 changed files with 154 additions and 0 deletions.
1 change: 1 addition & 0 deletions docs/src/algorithms/traversals.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,6 @@ Pages = [
"traversals/greedy_color.jl",
"traversals/maxadjvisit.jl",
"traversals/randomwalks.jl",
"traversals/eulerian.jl",
]
```
4 changes: 4 additions & 0 deletions src/Graphs.jl
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,9 @@ export
diffusion,
diffusion_rate,

# eulerian
eulerian,

# coloring
greedy_color,

Expand Down Expand Up @@ -488,6 +491,7 @@ include("traversals/dfs.jl")
include("traversals/maxadjvisit.jl")
include("traversals/randomwalks.jl")
include("traversals/diffusion.jl")
include("traversals/eulerian.jl")
include("connectivity.jl")
include("distance.jl")
include("editdist.jl")
Expand Down
8 changes: 8 additions & 0 deletions src/Test/Test.jl
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ struct GenericGraph{T} <: Graphs.AbstractGraph{T}
g::SimpleGraph{T}
end

function GenericGraph(elist::Vector{Graphs.SimpleGraphEdge{T}}) where {T<:Integer}
GenericGraph{T}(SimpleGraph(elist))
end

"""
GenericDiGraph{T} <: Graphs.AbstractGraph{T}
Expand All @@ -46,6 +50,10 @@ struct GenericDiGraph{T} <: Graphs.AbstractGraph{T}
g::SimpleDiGraph{T}
end

function GenericDiGraph(elist::Vector{Graphs.SimpleDiGraphEdge{T}}) where {T<:Integer}
GenericDiGraph{T}(SimpleDiGraph(elist))
end

Graphs.is_directed(::Type{<:GenericGraph}) = false
Graphs.is_directed(::Type{<:GenericDiGraph}) = true

Expand Down
103 changes: 103 additions & 0 deletions src/traversals/eulerian.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# Adapted from SimpleGraphs.jl [Copyright (c) 2014, Ed Scheinerman]:
# https://github.com/scheinerman/SimpleGraphs.jl/blob/master/src/simple_euler.jl
# Reproduced under the MIT Expat License.

"""
eulerian(g::AbstractSimpleGraph{T}[, u::T]) --> T[]
Returns a [Eulerian trail or cycle](https://en.wikipedia.org/wiki/Eulerian_path) through an
undirected graph `g`, starting at vertex `u`, returning a vector listing the vertices of `g`
in the order that they are traversed. If no such trail or cycle exists, throws an error.
A Eulerian trail or cycle is a path that visits every edge of `g` exactly once; for a
cycle, the path starts _and_ ends at vertex `u`.
## Optional arguments
- If `u` is omitted, a Eulerian trail or cycle is computed with `u = first(vertices(g))`.
"""
function eulerian(g::AbstractGraph{T}, u::T=first(vertices(g))) where {T}
is_directed(g) && error("`eulerian` is not yet implemented for directed graphs")

_check_eulerian_input(g, u) # perform basic sanity checks

g′ = SimpleGraph{T}(nv(g)) # copy `g` (mutated in `_eulerian!`)
for e in edges(g)
add_edge!(g′, src(e), dst(e))
end

return _eulerian!(g′, u)
end

@traitfn function _eulerian!(g::AG::(!IsDirected), u::T) where {T, AG<:AbstractGraph{T}}
# TODO: This uses Fleury's algorithm which is O(|E|²) in the number of edges |E|.
# Hierholzer's algorithm [https://en.wikipedia.org/wiki/Eulerian_path#Hierholzer's_algorithm]
# is presumably faster, running in O(|E|) time, but requires needing to keep track
# of visited/nonvisited sites in a doubly-linked list/deque.
trail = T[]

nverts = nv(g)
while true
# if last vertex
if nverts == 1
push!(trail, u)
return trail
end

Nu = neighbors(g, u)
if length(Nu) == 1
# if only one neighbor, delete and move on
w = first(Nu)
rem_edge!(g, u, w)
nverts -= 1
push!(trail, u)
u = w
elseif length(Nu) == 0
error("graph is not connected: a eulerian cycle/trail does not exist")
else
# otherwise, pick whichever neighbor is not a bridge/cut-edge
bs = bridges(g)
for w in Nu
if all(e -> _excludes_edge(u, w, e), bs)
# not a bridge/cut-edge; add to trail
rem_edge!(g, u, w)
push!(trail, u)
u = w
break
end
end
end
end
error("unreachable reached")
end

@inline function _excludes_edge(u, w, e::AbstractEdge)
# `true` if `e` is not `Edge(u,w)` or `Edge(w,u)`, otherwise `false`
s, d = src(e), dst(e)
return !((u == s && w == d) || (u == d && w == s))
end

function _check_eulerian_input(g, u)
if !has_vertex(g, u)
error("starting vertex is not in the graph")
end

# special case: if any vertex has degree zero
if any(x->degree(g, x) == 0, vertices(g))
error("some vertices have degree zero (are isolated) and cannot be reached")
end

# vertex degree checks
du = degree(g, u)
if iseven(du) # cycle: start (u) == stop (v) - all nodes must have even degree
if any(x -> isodd(degree(g, x)), vertices(g))
error("starting vertex has even degree but there are other vertices with odd degree: a eulerian cycle does not exist")
end
else # isodd(du) # trail: start (u) != stop (v) - all nodes, except u and v, must have even degree
if count(x -> iseven(degree(g, x)), vertices(g)) != 2
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")
end
end

# to reduce cost, the graph connectivity check is performed in `_eulerian!` rather
# than through `is_connected(g)`
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 = [
"traversals/maxadjvisit",
"traversals/randomwalks",
"traversals/diffusion",
"traversals/eulerian",
"community/cliques",
"community/core-periphery",
"community/label_propagation",
Expand Down
37 changes: 37 additions & 0 deletions test/traversals/eulerian.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
@testset "Eulerian tours/cycles" begin
# a cycle (identical start/end)
g0 = GenericGraph([Edge(1,2), Edge(2,3), Edge(3,1)])
@test eulerian(g0, 1) == eulerian(g0)
@test last(eulerian(g0, 1)) == 1 # a cycle

# a tour (different start/end)
g1 = GenericGraph([Edge(1,2), Edge(2,3), Edge(3,4)])
@test eulerian(g1, 1) == [1,2,3,4]
@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)

# a cycle with a node (vertex 2) with multiple neighbors
g2 = GenericGraph([Edge(1,2), Edge(2,3), Edge(3,4), Edge(4,1), Edge(2,5), Edge(5,6),
Edge(6,2)])
@test eulerian(g2) == eulerian(g2, 1) == [1, 2, 5, 6, 2, 3, 4, 1]

# graph with odd-degree vertices
g3 = GenericGraph([Edge(1,2), Edge(2,3), Edge(3,4), Edge(2,4), Edge(4,1), Edge(4,2)])
@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)

# start/end point not in graph
@test_throws ErrorException("starting vertex is not in the graph") eulerian(g3, 5)

# disconnected components
g4 = GenericGraph([Edge(1,2), Edge(2,3), Edge(3,1), # component 1
Edge(4,5), Edge(5,6), Edge(6,4)]) # component 2
@test_throws ErrorException("graph is not connected: a eulerian cycle/trail does not exist") eulerian(g4)

# zero-degree nodes
g5′ = SimpleGraph(4)
add_edge!(g5′, Edge(1,2)); add_edge!(g5′, Edge(2,3)); add_edge!(g5′, Edge(3,1))
g5 = GenericGraph(g5′)
@test_throws ErrorException("some vertices have degree zero (are isolated) and cannot be reached") eulerian(g5)

# not yet implemented for directed graphs
@test_broken eulerian(GenericDiGraph([Edge(1,2), Edge(2,3), Edge(3,1)]))
end

0 comments on commit 1cb789d

Please sign in to comment.