-
Notifications
You must be signed in to change notification settings - Fork 103
Eulerian cycles/trails for undirected graphs #232
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
Changes from all commits
6d6aa56
9b8f5ea
61ff3dc
b3ff87a
9325759
fed450f
c562fa3
05656f3
322bed2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 | ||
thchr marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How do I know whether the result will be a trail or a cycle? What does it depend on? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Only on the degree of the starting vertex? |
||
|
||
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|. | ||
thchr marked this conversation as resolved.
Show resolved
Hide resolved
|
||
# 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) | ||
thchr marked this conversation as resolved.
Show resolved
Hide resolved
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add a TODO here for complexity |
||
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") | ||
thchr marked this conversation as resolved.
Show resolved
Hide resolved
|
||
end | ||
|
||
@inline function _excludes_edge(u, w, e::AbstractEdge) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this belongs in the core utilities somewhere |
||
# `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 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
@testset "Eulerian tours/cycles" begin | ||
thchr marked this conversation as resolved.
Show resolved
Hide resolved
|
||
# 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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@thchr I spotted this too late - I think we should not implement any method (this includes constructors) that specialize on a
GenericGraph
orGenericDiGraph
. The reason is, that we want to useGenericGraph
as a placeholder for an arbitraryAbstractGraph
to verify functions that take anAbstractGraph
work correctly.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
wdym by specialize on a GenericGraph?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do you want to revert that part?
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I mean the issue I see here is, if someone writes a function like
then this function might fail for an arbitrary
AbstractGraph
but not for aGenericGraph
. I don't think it will break anything soon though, so there is no rush, but I can make a PR that works around that.I would propose, if we need a workaround for constructors so that it will be easier to write tests, we just implement the functions
generic_graph
andgeneric_digraph
- maybe I already did such a thing in one of my PRs but I don't remember exactly.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good to me :)