-
Notifications
You must be signed in to change notification settings - Fork 91
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
- Loading branch information
Showing
6 changed files
with
154 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |