From 1cb789d4d184b3b72132c56d6b65d2a3bc2faaec Mon Sep 17 00:00:00 2001 From: Thomas Christensen Date: Thu, 27 Jul 2023 18:41:40 +0200 Subject: [PATCH] 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 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 \ No newline at end of file diff --git a/test/runtests.jl b/test/runtests.jl index cbb8763bb..9ae319f63 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -116,6 +116,7 @@ tests = [ "traversals/maxadjvisit", "traversals/randomwalks", "traversals/diffusion", + "traversals/eulerian", "community/cliques", "community/core-periphery", "community/label_propagation", diff --git a/test/traversals/eulerian.jl b/test/traversals/eulerian.jl new file mode 100644 index 000000000..c4e5e8b97 --- /dev/null +++ b/test/traversals/eulerian.jl @@ -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 \ No newline at end of file