Skip to content

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

Merged
merged 9 commits into from
Jul 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Comment on lines +39 to +41
Copy link
Member

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 or GenericDiGraph. The reason is, that we want to use GenericGraph as a placeholder for an arbitrary AbstractGraph to verify functions that take an AbstractGraph work correctly.

Copy link
Member

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?

Copy link
Member

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?

Copy link
Member

@simonschoelly simonschoelly Jul 27, 2023

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

function f(g::AbstractGraph)
    T = typeof(g)
    return T([Edge(1, 2)])
end

then this function might fail for an arbitrary AbstractGraph but not for a GenericGraph. 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 and generic_digraph - maybe I already did such a thing in one of my PRs but I don't remember exactly.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good to me :)


"""
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.
Copy link
Member

Choose a reason for hiding this comment

The 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?

Copy link
Member

Choose a reason for hiding this comment

The 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|.
# 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)
Copy link
Member

Choose a reason for hiding this comment

The 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")
end

@inline function _excludes_edge(u, w, e::AbstractEdge)
Copy link
Member

Choose a reason for hiding this comment

The 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
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