From 6b49e56fd279b3bc97dd98aad21f854ca44ebcaf Mon Sep 17 00:00:00 2001 From: Thomas Christensen Date: Mon, 18 Mar 2024 16:45:52 +0100 Subject: [PATCH 01/16] `all_simple_paths`: update PR #20 - this updates the port of https://github.com/sbromberger/LightGraphs.jl/pull/1540 from #20 - has a number of simplifications relative to original implementation - original implementation by @i-aki-y - cutoff now defaults to `nv(g)` Co-authored-by: akiyuki ishikawa Co-authored-by: Etienne dg --- src/Graphs.jl | 7 +- src/traversals/all_simple_paths.jl | 156 ++++++++++++++++++++++++++++ test/runtests.jl | 1 + test/traversals/all_simple_paths.jl | 127 ++++++++++++++++++++++ 4 files changed, 290 insertions(+), 1 deletion(-) create mode 100644 src/traversals/all_simple_paths.jl create mode 100644 test/traversals/all_simple_paths.jl diff --git a/src/Graphs.jl b/src/Graphs.jl index 675db9eef..73b0a18c0 100644 --- a/src/Graphs.jl +++ b/src/Graphs.jl @@ -23,7 +23,8 @@ using DataStructures: union!, find_root!, BinaryMaxHeap, - BinaryMinHeap + BinaryMinHeap, + Stack using LinearAlgebra: I, Symmetric, diagm, eigen, eigvals, norm, rmul!, tril, triu import LinearAlgebra: Diagonal, issymmetric, mul! using Random: @@ -196,6 +197,9 @@ export # eulerian eulerian, + + # all simple paths + all_simple_paths, # coloring greedy_color, @@ -496,6 +500,7 @@ include("traversals/maxadjvisit.jl") include("traversals/randomwalks.jl") include("traversals/diffusion.jl") include("traversals/eulerian.jl") +include("traversals/all_simple_paths.jl") include("connectivity.jl") include("distance.jl") include("editdist.jl") diff --git a/src/traversals/all_simple_paths.jl b/src/traversals/all_simple_paths.jl new file mode 100644 index 000000000..9a1c42a8f --- /dev/null +++ b/src/traversals/all_simple_paths.jl @@ -0,0 +1,156 @@ +""" + all_simple_paths(g, u, v; cutoff=nv(g)) --> Graphs.SimplePathIterator + +Returns an iterator that generates all simple paths in the graph `g` from a source vertex +`u` to a target vertex `v` or iterable of target vertices `vs`. + +The iterator's elements (i.e., the paths) can be materialized via `collect` or `iterate`. +Paths are iterated in the order of a depth-first search. + +## Keyword arguments +The maximum path length (i.e., number of edges) is limited by the keyword argument `cutoff` +(default, `nv(g)`). If a path's path length is greater than or equal to `cutoff`, it is +omitted. + +## Examples +```jldoctest +julia> using Graphs +julia> g = complete_graph(4) +julia> spi = all_simple_paths(g, 1, 4) + Graphs.SimplePathIterator(1 → 4) +julia> collect(spi) +5-element Vector{Vector{Int64}}: + [1, 4] + [1, 3, 4] + [1, 3, 2, 4] + [1, 2, 4] + [1, 2, 3, 4] +``` +We can restrict the search to paths of length less than a specified cut-off (here, 2 edges): +```jldoctest +julia> collect(all_simple_paths(g, 1, 4; cutoff=2)) + [1, 2, 4] + [1, 3, 4] + [1, 4] +``` +""" +function all_simple_paths( + g::AbstractGraph{T}, + u::T, + vs; + cutoff::T=nv(g) + ) where T <: Integer + + vs = vs isa Set{T} ? vs : Set{T}(vs) + return SimplePathIterator(g, u, vs, cutoff) +end + +""" + SimplePathIterator{T <: Integer} + +Iterator that generates all simple paths in `g` from `u` to `vs` of a length at most +`cutoff`. +""" +struct SimplePathIterator{T <: Integer, G <: AbstractGraph{T}} + g::G + u::T # start vertex + vs::Set{T} # target vertices + cutoff::T # max length of resulting paths +end + +function Base.show(io::IO, spi::SimplePathIterator) + print(io, "SimplePathIterator{", typeof(spi.g), "}(", spi.u, " → ") + if length(spi.vs) == 1 + print(io, only(spi.vs)) + else + print(io, '[') + join(io, spi.vs, ", ") + print(io, ']') + end + print(io, ')') +end +Base.IteratorSize(::Type{<:SimplePathIterator}) = Base.SizeUnknown() +Base.eltype(::SimplePathIterator{T}) where T = Vector{T} + +mutable struct SimplePathIteratorState{T <: Integer} + stack::Stack{Vector{T}} # used to restore iteration of child vertices; each vector has + # two elements: a parent vertex and an index of children + visited::Stack{T} # current path candidate + queued::Vector{T} # remaining targets if path length reached cutoff +end +function SimplePathIteratorState(spi::SimplePathIterator{T}) where T <: Integer + stack = Stack{Vector{T}}() + visited = Stack{T}() + queued = Vector{T}() + push!(visited, spi.u) # add a starting vertex to the path candidate + push!(stack, [spi.u, 1]) # add a child node with index 1 + SimplePathIteratorState{T}(stack, visited, queued) +end + +function _stepback!(state::SimplePathIteratorState) # updates iterator state. + pop!(state.stack) + pop!(state.visited) +end + + +""" + Base.iterate(spi::SimplePathIterator{T}, state=nothing) + +Returns the next simple path in `spi`, according to a depth-first search. +""" +function Base.iterate( + spi::SimplePathIterator{T}, + state::SimplePathIteratorState=SimplePathIteratorState(spi) + ) where T <: Integer + + while !isempty(state.stack) + if !isempty(state.queued) # consume queued targets + target = pop!(state.queued) + result = vcat(reverse(collect(state.visited)), target) + if isempty(state.queued) + _stepback!(state) + end + return result, state + end + + parent_node, next_childe_index = first(state.stack) + children = outneighbors(spi.g, parent_node) + if length(children) < next_childe_index + # all children have been checked, step back. + _stepback!(state) + continue + end + + child = children[next_childe_index] + first(state.stack)[2] += 1 # move child index forward + child in state.visited && continue + + if length(state.visited) == spi.cutoff + # collect adjacent targets if more exist and add them to queue + rest_children = Set(children[next_childe_index: end]) + state.queued = collect(setdiff(intersect(spi.vs, rest_children), Set(state.visited))) + + if isempty(state.queued) + _stepback!(state) + end + else + result = if child in spi.vs + vcat(reverse(collect(state.visited)), child) + else + nothing + end + + # update state variables + push!(state.visited, child) # move to child vertex + if !isempty(setdiff(spi.vs, state.visited)) # expand stack until all targets are found + push!(state.stack, [child, 1]) # add the child node as a parent for next iteration + else + pop!(state.visited) # step back and explore the remaining child nodes + end + + if !isnothing(result) # found a new path, return it + return result, state + end + end + end +end \ No newline at end of file diff --git a/test/runtests.jl b/test/runtests.jl index df304b74f..1764fe537 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -109,6 +109,7 @@ tests = [ "traversals/randomwalks", "traversals/diffusion", "traversals/eulerian", + "traversals/all_simple_paths", "community/cliques", "community/core-periphery", "community/label_propagation", diff --git a/test/traversals/all_simple_paths.jl b/test/traversals/all_simple_paths.jl new file mode 100644 index 000000000..3e851743b --- /dev/null +++ b/test/traversals/all_simple_paths.jl @@ -0,0 +1,127 @@ +@testset "All simple paths" begin + # single path + g = path_graph(4) + paths = all_simple_paths(g, 1, 4) + @test Set(p for p in paths) == Set([[1, 2, 3, 4]]) + @test Set(collect(paths)) == Set([[1, 2, 3, 4]]) + @test 1 == length(paths) + + + # single path with cutoff + @test collect(all_simple_paths(g, 1, 4; cutoff=2)) == [[1, 2, 4], [1, 3, 4], [1, 4]] + + # two paths + g = path_graph(4) + add_vertex!(g) + add_edge!(g, 3, 5) + paths = all_simple_paths(g, 1, [4, 5]) + @test Set(p for p in paths) == Set([[1, 2, 3, 4], [1, 2, 3, 5]]) + @test Set(collect(paths)) == Set([[1, 2, 3, 4], [1, 2, 3, 5]]) + @test 2 == length(paths) + + # two paths with cutoff + g = path_graph(4) + add_vertex!(g) + add_edge!(g, 3, 5) + paths = all_simple_paths(g, 1, [4, 5], cutoff=3) + @test Set(p for p in paths) == Set([[1, 2, 3, 4], [1, 2, 3, 5]]) + + # two targets in line emits two paths + g = path_graph(4) + add_vertex!(g) + paths = all_simple_paths(g, 1, [3, 4]) + @test Set(p for p in paths) == Set([[1, 2, 3], [1, 2, 3, 4]]) + + # two paths digraph + g = SimpleDiGraph(5) + add_edge!(g, 1, 2) + add_edge!(g, 2, 3) + add_edge!(g, 3, 4) + add_edge!(g, 3, 5) + paths = all_simple_paths(g, 1, [4, 5]) + @test Set(p for p in paths) == Set([[1, 2, 3, 4], [1, 2, 3, 5]]) + + # two paths digraph with cutoff + g = SimpleDiGraph(5) + add_edge!(g, 1, 2) + add_edge!(g, 2, 3) + add_edge!(g, 3, 4) + add_edge!(g, 3, 5) + paths = all_simple_paths(g, 1, [4, 5], cutoff=3) + @test Set(p for p in paths) == Set([[1, 2, 3, 4], [1, 2, 3, 5]]) + + # digraph with a cycle + g = SimpleDiGraph(4) + add_edge!(g, 1, 2) + add_edge!(g, 2, 3) + add_edge!(g, 3, 1) + add_edge!(g, 2, 4) + paths = all_simple_paths(g, 1, 4) + @test Set(p for p in paths) == Set([[1, 2, 4]]) + + # digraph with a cycle. paths with two targets share a node in the cycle. + g = SimpleDiGraph(4) + add_edge!(g, 1, 2) + add_edge!(g, 2, 3) + add_edge!(g, 3, 1) + add_edge!(g, 2, 4) + paths = all_simple_paths(g, 1, [3, 4]) + @test Set(p for p in paths) == Set([[1, 2, 3], [1, 2, 4]]) + + # source equals targets + g = SimpleGraph(4) + paths = all_simple_paths(g, 1, 1) + @test Set(p for p in paths) == Set([]) + + # cutoff prones paths + # Note, a path lenght is node - 1 + g = complete_graph(4) + paths = all_simple_paths(g, 1, 2; cutoff=1) + @test Set(p for p in paths) == Set([[1, 2]]) + + paths = all_simple_paths(g, 1, 2; cutoff=2) + @test Set(p for p in paths) == Set([[1, 2], [1, 3, 2], [1, 4, 2]]) + + # non trivial graph + g = SimpleDiGraph(6) + add_edge!(g, 1, 2) + add_edge!(g, 2, 3) + add_edge!(g, 3, 4) + add_edge!(g, 4, 5) + + add_edge!(g, 1, 6) + add_edge!(g, 2, 6) + add_edge!(g, 2, 4) + add_edge!(g, 6, 5) + add_edge!(g, 5, 3) + add_edge!(g, 5, 4) + + paths = all_simple_paths(g, 2, [3, 4]) + @test Set(p for p in paths) == Set([ + [2, 3], + [2, 4, 5, 3], + [2, 6, 5, 3], + [2, 4], + [2, 3, 4], + [2, 6, 5, 4], + [2, 6, 5, 3, 4], + ]) + + paths = all_simple_paths(g, 2, [3, 4], cutoff=3) + @test Set(p for p in paths) == Set([ + [2, 3], + [2, 4, 5, 3], + [2, 6, 5, 3], + [2, 4], + [2, 3, 4], + [2, 6, 5, 4], + ]) + + paths = all_simple_paths(g, 2, [3, 4], cutoff=2) + @test Set(p for p in paths) == Set([ + [2, 3], + [2, 4], + [2, 3, 4], + ]) + +end \ No newline at end of file From 391cbab57a0a7b0a2027eaf9f5f44f6a40f88d59 Mon Sep 17 00:00:00 2001 From: Thomas Christensen Date: Mon, 18 Mar 2024 17:18:02 +0100 Subject: [PATCH 02/16] fixes to tests & doctests --- src/traversals/all_simple_paths.jl | 30 +++++++++++++++-------------- test/traversals/all_simple_paths.jl | 12 +++++------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/traversals/all_simple_paths.jl b/src/traversals/all_simple_paths.jl index 9a1c42a8f..c9d5545eb 100644 --- a/src/traversals/all_simple_paths.jl +++ b/src/traversals/all_simple_paths.jl @@ -1,6 +1,6 @@ """ all_simple_paths(g, u, v; cutoff=nv(g)) --> Graphs.SimplePathIterator - + Returns an iterator that generates all simple paths in the graph `g` from a source vertex `u` to a target vertex `v` or iterable of target vertices `vs`. @@ -13,30 +13,32 @@ The maximum path length (i.e., number of edges) is limited by the keyword argume omitted. ## Examples -```jldoctest -julia> using Graphs -julia> g = complete_graph(4) +```jldoctest allsimplepaths; setup = :(using Graphs) +julia> g = complete_graph(4); + julia> spi = all_simple_paths(g, 1, 4) - Graphs.SimplePathIterator(1 → 4) +SimplePathIterator{SimpleGraph{Int64}}(1 → 4) + julia> collect(spi) 5-element Vector{Vector{Int64}}: - [1, 4] - [1, 3, 4] - [1, 3, 2, 4] - [1, 2, 4] [1, 2, 3, 4] + [1, 2, 4] + [1, 3, 2, 4] + [1, 3, 4] + [1, 4] ``` We can restrict the search to paths of length less than a specified cut-off (here, 2 edges): -```jldoctest +```jldoctest allsimplepaths; setup = :(using Graphs) julia> collect(all_simple_paths(g, 1, 4; cutoff=2)) +3-element Vector{Vector{Int64}}: [1, 2, 4] [1, 3, 4] [1, 4] ``` """ function all_simple_paths( - g::AbstractGraph{T}, - u::T, + g::AbstractGraph{T}, + u::T, vs; cutoff::T=nv(g) ) where T <: Integer @@ -99,7 +101,7 @@ end Returns the next simple path in `spi`, according to a depth-first search. """ function Base.iterate( - spi::SimplePathIterator{T}, + spi::SimplePathIterator{T}, state::SimplePathIteratorState=SimplePathIteratorState(spi) ) where T <: Integer @@ -121,7 +123,7 @@ function Base.iterate( continue end - child = children[next_childe_index] + child = children[next_childe_index] first(state.stack)[2] += 1 # move child index forward child in state.visited && continue diff --git a/test/traversals/all_simple_paths.jl b/test/traversals/all_simple_paths.jl index 3e851743b..fdc9e2139 100644 --- a/test/traversals/all_simple_paths.jl +++ b/test/traversals/all_simple_paths.jl @@ -4,10 +4,9 @@ paths = all_simple_paths(g, 1, 4) @test Set(p for p in paths) == Set([[1, 2, 3, 4]]) @test Set(collect(paths)) == Set([[1, 2, 3, 4]]) - @test 1 == length(paths) - # single path with cutoff + g = complete_graph(4) @test collect(all_simple_paths(g, 1, 4; cutoff=2)) == [[1, 2, 4], [1, 3, 4], [1, 4]] # two paths @@ -17,13 +16,12 @@ paths = all_simple_paths(g, 1, [4, 5]) @test Set(p for p in paths) == Set([[1, 2, 3, 4], [1, 2, 3, 5]]) @test Set(collect(paths)) == Set([[1, 2, 3, 4], [1, 2, 3, 5]]) - @test 2 == length(paths) # two paths with cutoff g = path_graph(4) add_vertex!(g) add_edge!(g, 3, 5) - paths = all_simple_paths(g, 1, [4, 5], cutoff=3) + paths = all_simple_paths(g, 1, [4, 5]; cutoff=3) @test Set(p for p in paths) == Set([[1, 2, 3, 4], [1, 2, 3, 5]]) # two targets in line emits two paths @@ -47,7 +45,7 @@ add_edge!(g, 2, 3) add_edge!(g, 3, 4) add_edge!(g, 3, 5) - paths = all_simple_paths(g, 1, [4, 5], cutoff=3) + paths = all_simple_paths(g, 1, [4, 5]; cutoff=3) @test Set(p for p in paths) == Set([[1, 2, 3, 4], [1, 2, 3, 5]]) # digraph with a cycle @@ -107,7 +105,7 @@ [2, 6, 5, 3, 4], ]) - paths = all_simple_paths(g, 2, [3, 4], cutoff=3) + paths = all_simple_paths(g, 2, [3, 4]; cutoff=3) @test Set(p for p in paths) == Set([ [2, 3], [2, 4, 5, 3], @@ -117,7 +115,7 @@ [2, 6, 5, 4], ]) - paths = all_simple_paths(g, 2, [3, 4], cutoff=2) + paths = all_simple_paths(g, 2, [3, 4]; cutoff=2) @test Set(p for p in paths) == Set([ [2, 3], [2, 4], From 4e678b3d29128d75e93d2aef70ea5000c528dcdc Mon Sep 17 00:00:00 2001 From: Thomas Christensen Date: Mon, 18 Mar 2024 17:37:41 +0100 Subject: [PATCH 03/16] improve docstring --- src/traversals/all_simple_paths.jl | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/traversals/all_simple_paths.jl b/src/traversals/all_simple_paths.jl index c9d5545eb..062a7c87a 100644 --- a/src/traversals/all_simple_paths.jl +++ b/src/traversals/all_simple_paths.jl @@ -1,8 +1,10 @@ """ all_simple_paths(g, u, v; cutoff=nv(g)) --> Graphs.SimplePathIterator -Returns an iterator that generates all simple paths in the graph `g` from a source vertex -`u` to a target vertex `v` or iterable of target vertices `vs`. +Returns an iterator that generates all +[simple paths](https://en.wikipedia.org/wiki/Path_(graph_theory)#Walk,_trail,_and_path) in +the graph `g` from a source vertex `u` to a target vertex `v` or iterable of target vertices +`vs`. A simple path has no repeated vertices. The iterator's elements (i.e., the paths) can be materialized via `collect` or `iterate`. Paths are iterated in the order of a depth-first search. From b9cd00a5e13f15d78ac76e9aeedca18dbe6535bf Mon Sep 17 00:00:00 2001 From: Thomas Christensen Date: Mon, 18 Mar 2024 17:46:11 +0100 Subject: [PATCH 04/16] run JuliaFormatter - `format(Graphs, overwrite=true)` --- src/Graphs.jl | 2 +- src/traversals/all_simple_paths.jl | 37 +++++++++++++---------------- test/traversals/all_simple_paths.jl | 31 ++++++------------------ 3 files changed, 24 insertions(+), 46 deletions(-) diff --git a/src/Graphs.jl b/src/Graphs.jl index 73b0a18c0..7df4cc832 100644 --- a/src/Graphs.jl +++ b/src/Graphs.jl @@ -197,7 +197,7 @@ export # eulerian eulerian, - + # all simple paths all_simple_paths, diff --git a/src/traversals/all_simple_paths.jl b/src/traversals/all_simple_paths.jl index 062a7c87a..e41d4b1bf 100644 --- a/src/traversals/all_simple_paths.jl +++ b/src/traversals/all_simple_paths.jl @@ -38,13 +38,7 @@ julia> collect(all_simple_paths(g, 1, 4; cutoff=2)) [1, 4] ``` """ -function all_simple_paths( - g::AbstractGraph{T}, - u::T, - vs; - cutoff::T=nv(g) - ) where T <: Integer - +function all_simple_paths(g::AbstractGraph{T}, u::T, vs; cutoff::T=nv(g)) where {T<:Integer} vs = vs isa Set{T} ? vs : Set{T}(vs) return SimplePathIterator(g, u, vs, cutoff) end @@ -55,7 +49,7 @@ end Iterator that generates all simple paths in `g` from `u` to `vs` of a length at most `cutoff`. """ -struct SimplePathIterator{T <: Integer, G <: AbstractGraph{T}} +struct SimplePathIterator{T<:Integer,G<:AbstractGraph{T}} g::G u::T # start vertex vs::Set{T} # target vertices @@ -72,41 +66,40 @@ function Base.show(io::IO, spi::SimplePathIterator) print(io, ']') end print(io, ')') + return nothing end Base.IteratorSize(::Type{<:SimplePathIterator}) = Base.SizeUnknown() -Base.eltype(::SimplePathIterator{T}) where T = Vector{T} +Base.eltype(::SimplePathIterator{T}) where {T} = Vector{T} -mutable struct SimplePathIteratorState{T <: Integer} +mutable struct SimplePathIteratorState{T<:Integer} stack::Stack{Vector{T}} # used to restore iteration of child vertices; each vector has - # two elements: a parent vertex and an index of children + # two elements: a parent vertex and an index of children visited::Stack{T} # current path candidate queued::Vector{T} # remaining targets if path length reached cutoff end -function SimplePathIteratorState(spi::SimplePathIterator{T}) where T <: Integer +function SimplePathIteratorState(spi::SimplePathIterator{T}) where {T<:Integer} stack = Stack{Vector{T}}() visited = Stack{T}() queued = Vector{T}() push!(visited, spi.u) # add a starting vertex to the path candidate push!(stack, [spi.u, 1]) # add a child node with index 1 - SimplePathIteratorState{T}(stack, visited, queued) + return SimplePathIteratorState{T}(stack, visited, queued) end function _stepback!(state::SimplePathIteratorState) # updates iterator state. pop!(state.stack) pop!(state.visited) + return nothing end - """ Base.iterate(spi::SimplePathIterator{T}, state=nothing) Returns the next simple path in `spi`, according to a depth-first search. """ function Base.iterate( - spi::SimplePathIterator{T}, - state::SimplePathIteratorState=SimplePathIteratorState(spi) - ) where T <: Integer - + spi::SimplePathIterator{T}, state::SimplePathIteratorState=SimplePathIteratorState(spi) +) where {T<:Integer} while !isempty(state.stack) if !isempty(state.queued) # consume queued targets target = pop!(state.queued) @@ -131,8 +124,10 @@ function Base.iterate( if length(state.visited) == spi.cutoff # collect adjacent targets if more exist and add them to queue - rest_children = Set(children[next_childe_index: end]) - state.queued = collect(setdiff(intersect(spi.vs, rest_children), Set(state.visited))) + rest_children = Set(children[next_childe_index:end]) + state.queued = collect( + setdiff(intersect(spi.vs, rest_children), Set(state.visited)) + ) if isempty(state.queued) _stepback!(state) @@ -157,4 +152,4 @@ function Base.iterate( end end end -end \ No newline at end of file +end diff --git a/test/traversals/all_simple_paths.jl b/test/traversals/all_simple_paths.jl index fdc9e2139..b1481b09b 100644 --- a/test/traversals/all_simple_paths.jl +++ b/test/traversals/all_simple_paths.jl @@ -35,7 +35,7 @@ add_edge!(g, 1, 2) add_edge!(g, 2, 3) add_edge!(g, 3, 4) - add_edge!(g, 3, 5) + add_edge!(g, 3, 5) paths = all_simple_paths(g, 1, [4, 5]) @test Set(p for p in paths) == Set([[1, 2, 3, 4], [1, 2, 3, 5]]) @@ -44,7 +44,7 @@ add_edge!(g, 1, 2) add_edge!(g, 2, 3) add_edge!(g, 3, 4) - add_edge!(g, 3, 5) + add_edge!(g, 3, 5) paths = all_simple_paths(g, 1, [4, 5]; cutoff=3) @test Set(p for p in paths) == Set([[1, 2, 3, 4], [1, 2, 3, 5]]) @@ -96,30 +96,13 @@ paths = all_simple_paths(g, 2, [3, 4]) @test Set(p for p in paths) == Set([ - [2, 3], - [2, 4, 5, 3], - [2, 6, 5, 3], - [2, 4], - [2, 3, 4], - [2, 6, 5, 4], - [2, 6, 5, 3, 4], + [2, 3], [2, 4, 5, 3], [2, 6, 5, 3], [2, 4], [2, 3, 4], [2, 6, 5, 4], [2, 6, 5, 3, 4] ]) paths = all_simple_paths(g, 2, [3, 4]; cutoff=3) - @test Set(p for p in paths) == Set([ - [2, 3], - [2, 4, 5, 3], - [2, 6, 5, 3], - [2, 4], - [2, 3, 4], - [2, 6, 5, 4], - ]) + @test Set(p for p in paths) == + Set([[2, 3], [2, 4, 5, 3], [2, 6, 5, 3], [2, 4], [2, 3, 4], [2, 6, 5, 4]]) paths = all_simple_paths(g, 2, [3, 4]; cutoff=2) - @test Set(p for p in paths) == Set([ - [2, 3], - [2, 4], - [2, 3, 4], - ]) - -end \ No newline at end of file + @test Set(p for p in paths) == Set([[2, 3], [2, 4], [2, 3, 4]]) +end From 2ca23386f97af897a95b767e3b1abb7fa16599c1 Mon Sep 17 00:00:00 2001 From: Thomas Christensen Date: Mon, 18 Mar 2024 17:47:22 +0100 Subject: [PATCH 05/16] bump to v1.9.1 --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index a9fd907c9..42b04ed00 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "Graphs" uuid = "86223c79-3864-5bf0-83f7-82e725a168b6" -version = "1.9.0" +version = "1.9.1" [deps] ArnoldiMethod = "ec485272-7323-5ecc-a04f-4719b315124d" From 46602f9e116bf132e82c3e549e6799a6809b7900 Mon Sep 17 00:00:00 2001 From: Thomas Christensen Date: Mon, 18 Mar 2024 18:15:30 +0100 Subject: [PATCH 06/16] fix docs --- docs/src/algorithms/traversals.md | 1 + src/traversals/all_simple_paths.jl | 14 +++----------- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/docs/src/algorithms/traversals.md b/docs/src/algorithms/traversals.md index 866f0bad1..f638cab04 100644 --- a/docs/src/algorithms/traversals.md +++ b/docs/src/algorithms/traversals.md @@ -21,5 +21,6 @@ Pages = [ "traversals/maxadjvisit.jl", "traversals/randomwalks.jl", "traversals/eulerian.jl", + "traversals/all_simple_paths.jl", ] ``` diff --git a/src/traversals/all_simple_paths.jl b/src/traversals/all_simple_paths.jl index e41d4b1bf..f345312d3 100644 --- a/src/traversals/all_simple_paths.jl +++ b/src/traversals/all_simple_paths.jl @@ -43,12 +43,8 @@ function all_simple_paths(g::AbstractGraph{T}, u::T, vs; cutoff::T=nv(g)) where return SimplePathIterator(g, u, vs, cutoff) end -""" - SimplePathIterator{T <: Integer} - -Iterator that generates all simple paths in `g` from `u` to `vs` of a length at most -`cutoff`. -""" +# Iterator that generates all simple paths in `g` from `u` to `vs` of a length at most +# `cutoff`. struct SimplePathIterator{T<:Integer,G<:AbstractGraph{T}} g::G u::T # start vertex @@ -92,11 +88,7 @@ function _stepback!(state::SimplePathIteratorState) # updates iterator state. return nothing end -""" - Base.iterate(spi::SimplePathIterator{T}, state=nothing) - -Returns the next simple path in `spi`, according to a depth-first search. -""" +# Returns the next simple path in `spi`, according to a depth-first search function Base.iterate( spi::SimplePathIterator{T}, state::SimplePathIteratorState=SimplePathIteratorState(spi) ) where {T<:Integer} From 0479b6f295b7823206e66207339228b2f50de1bd Mon Sep 17 00:00:00 2001 From: Thomas Christensen Date: Fri, 22 Mar 2024 14:36:10 +0100 Subject: [PATCH 07/16] address code-review --- Project.toml | 2 +- src/traversals/all_simple_paths.jl | 30 +++++++++-------- test/traversals/all_simple_paths.jl | 51 +++++++++++++++++++---------- 3 files changed, 51 insertions(+), 32 deletions(-) diff --git a/Project.toml b/Project.toml index 42b04ed00..dd5c761d4 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "Graphs" uuid = "86223c79-3864-5bf0-83f7-82e725a168b6" -version = "1.9.1" +version = "1.10.0" [deps] ArnoldiMethod = "ec485272-7323-5ecc-a04f-4719b315124d" diff --git a/src/traversals/all_simple_paths.jl b/src/traversals/all_simple_paths.jl index f345312d3..2e2b1ea32 100644 --- a/src/traversals/all_simple_paths.jl +++ b/src/traversals/all_simple_paths.jl @@ -1,5 +1,6 @@ """ - all_simple_paths(g, u, v; cutoff=nv(g)) --> Graphs.SimplePathIterator + all_simple_paths(g, u, v; cutoff) --> Graphs.SimplePathIterator + all_simple_paths(g, u, vs; cutoff) --> Graphs.SimplePathIterator Returns an iterator that generates all [simple paths](https://en.wikipedia.org/wiki/Path_(graph_theory)#Walk,_trail,_and_path) in @@ -11,7 +12,7 @@ Paths are iterated in the order of a depth-first search. ## Keyword arguments The maximum path length (i.e., number of edges) is limited by the keyword argument `cutoff` -(default, `nv(g)`). If a path's path length is greater than or equal to `cutoff`, it is +(default, `nv(g)-1`). If a path's path length is greater than or equal to `cutoff`, it is omitted. ## Examples @@ -38,7 +39,9 @@ julia> collect(all_simple_paths(g, 1, 4; cutoff=2)) [1, 4] ``` """ -function all_simple_paths(g::AbstractGraph{T}, u::T, vs; cutoff::T=nv(g)) where {T<:Integer} +function all_simple_paths( + g::AbstractGraph{T}, u::T, vs; cutoff::T=nv(g)-1 +) where {T<:Integer} vs = vs isa Set{T} ? vs : Set{T}(vs) return SimplePathIterator(g, u, vs, cutoff) end @@ -68,17 +71,17 @@ Base.IteratorSize(::Type{<:SimplePathIterator}) = Base.SizeUnknown() Base.eltype(::SimplePathIterator{T}) where {T} = Vector{T} mutable struct SimplePathIteratorState{T<:Integer} - stack::Stack{Vector{T}} # used to restore iteration of child vertices; each vector has - # two elements: a parent vertex and an index of children + stack::Stack{Tuple{T, T}} # used to restore iteration of child vertices: elements are + # (parent vertex, index of children) visited::Stack{T} # current path candidate queued::Vector{T} # remaining targets if path length reached cutoff end function SimplePathIteratorState(spi::SimplePathIterator{T}) where {T<:Integer} - stack = Stack{Vector{T}}() + stack = Stack{Tuple{T, T}}() visited = Stack{T}() queued = Vector{T}() push!(visited, spi.u) # add a starting vertex to the path candidate - push!(stack, [spi.u, 1]) # add a child node with index 1 + push!(stack, (spi.u, 1)) # add a child node with index 1 return SimplePathIteratorState{T}(stack, visited, queued) end @@ -102,21 +105,22 @@ function Base.iterate( return result, state end - parent_node, next_childe_index = first(state.stack) + parent_node, next_child_index = first(state.stack) children = outneighbors(spi.g, parent_node) - if length(children) < next_childe_index + if length(children) < next_child_index # all children have been checked, step back. _stepback!(state) continue end - child = children[next_childe_index] - first(state.stack)[2] += 1 # move child index forward + child = children[next_child_index] + next_child_index′ = pop!(state.stack)[2] # move child index forward + push!(state.stack, (parent_node, next_child_index′+1)) # ↩ child in state.visited && continue if length(state.visited) == spi.cutoff # collect adjacent targets if more exist and add them to queue - rest_children = Set(children[next_childe_index:end]) + rest_children = Set(children[next_child_index:end]) state.queued = collect( setdiff(intersect(spi.vs, rest_children), Set(state.visited)) ) @@ -134,7 +138,7 @@ function Base.iterate( # update state variables push!(state.visited, child) # move to child vertex if !isempty(setdiff(spi.vs, state.visited)) # expand stack until all targets are found - push!(state.stack, [child, 1]) # add the child node as a parent for next iteration + push!(state.stack, (child, 1)) # add the child node as a parent for next iteration else pop!(state.visited) # step back and explore the remaining child nodes end diff --git a/test/traversals/all_simple_paths.jl b/test/traversals/all_simple_paths.jl index b1481b09b..8932cfee6 100644 --- a/test/traversals/all_simple_paths.jl +++ b/test/traversals/all_simple_paths.jl @@ -2,9 +2,13 @@ # single path g = path_graph(4) paths = all_simple_paths(g, 1, 4) - @test Set(p for p in paths) == Set([[1, 2, 3, 4]]) + @test Set(paths) == Set([[1, 2, 3, 4]]) @test Set(collect(paths)) == Set([[1, 2, 3, 4]]) + # printing + @test sprint(show, paths) == "SimplePathIterator{SimpleGraph{Int64}}(1 → 4)" + + # single path with cutoff g = complete_graph(4) @test collect(all_simple_paths(g, 1, 4; cutoff=2)) == [[1, 2, 4], [1, 3, 4], [1, 4]] @@ -14,7 +18,7 @@ add_vertex!(g) add_edge!(g, 3, 5) paths = all_simple_paths(g, 1, [4, 5]) - @test Set(p for p in paths) == Set([[1, 2, 3, 4], [1, 2, 3, 5]]) + @test Set(paths) == Set([[1, 2, 3, 4], [1, 2, 3, 5]]) @test Set(collect(paths)) == Set([[1, 2, 3, 4], [1, 2, 3, 5]]) # two paths with cutoff @@ -22,13 +26,13 @@ add_vertex!(g) add_edge!(g, 3, 5) paths = all_simple_paths(g, 1, [4, 5]; cutoff=3) - @test Set(p for p in paths) == Set([[1, 2, 3, 4], [1, 2, 3, 5]]) + @test Set(paths) == Set([[1, 2, 3, 4], [1, 2, 3, 5]]) # two targets in line emits two paths g = path_graph(4) add_vertex!(g) paths = all_simple_paths(g, 1, [3, 4]) - @test Set(p for p in paths) == Set([[1, 2, 3], [1, 2, 3, 4]]) + @test Set(paths) == Set([[1, 2, 3], [1, 2, 3, 4]]) # two paths digraph g = SimpleDiGraph(5) @@ -37,7 +41,7 @@ add_edge!(g, 3, 4) add_edge!(g, 3, 5) paths = all_simple_paths(g, 1, [4, 5]) - @test Set(p for p in paths) == Set([[1, 2, 3, 4], [1, 2, 3, 5]]) + @test Set(paths) == Set([[1, 2, 3, 4], [1, 2, 3, 5]]) # two paths digraph with cutoff g = SimpleDiGraph(5) @@ -46,7 +50,7 @@ add_edge!(g, 3, 4) add_edge!(g, 3, 5) paths = all_simple_paths(g, 1, [4, 5]; cutoff=3) - @test Set(p for p in paths) == Set([[1, 2, 3, 4], [1, 2, 3, 5]]) + @test Set(paths) == Set([[1, 2, 3, 4], [1, 2, 3, 5]]) # digraph with a cycle g = SimpleDiGraph(4) @@ -55,32 +59,43 @@ add_edge!(g, 3, 1) add_edge!(g, 2, 4) paths = all_simple_paths(g, 1, 4) - @test Set(p for p in paths) == Set([[1, 2, 4]]) + @test Set(paths) == Set([[1, 2, 4]]) - # digraph with a cycle. paths with two targets share a node in the cycle. + # digraph with a cycle; paths with two targets share a node in the cycle g = SimpleDiGraph(4) add_edge!(g, 1, 2) add_edge!(g, 2, 3) add_edge!(g, 3, 1) add_edge!(g, 2, 4) paths = all_simple_paths(g, 1, [3, 4]) - @test Set(p for p in paths) == Set([[1, 2, 3], [1, 2, 4]]) + @test Set(paths) == Set([[1, 2, 3], [1, 2, 4]]) + + # another digraph with a cycle; check cycles are excluded, regardless of cutoff + g = SimpleDiGraph(6) + add_edge!(g, 1, 2) + add_edge!(g, 2, 3) + add_edge!(g, 3, 4) + add_edge!(g, 4, 5) + add_edge!(g, 5, 2) + add_edge!(g, 5, 6) + paths = all_simple_paths(g, 1, 6) + paths′ = all_simple_paths(g, 1, 6; cutoff=typemax(Int)) + @test Set(paths) == Set(paths′) == Set([[1, 2, 3, 4, 5, 6]]) # source equals targets g = SimpleGraph(4) paths = all_simple_paths(g, 1, 1) - @test Set(p for p in paths) == Set([]) + @test Set(paths) == Set{Int}() - # cutoff prones paths - # Note, a path lenght is node - 1 + # cutoff prunes paths (note: path length is node - 1) g = complete_graph(4) paths = all_simple_paths(g, 1, 2; cutoff=1) - @test Set(p for p in paths) == Set([[1, 2]]) + @test Set(paths) == Set([[1, 2]]) paths = all_simple_paths(g, 1, 2; cutoff=2) - @test Set(p for p in paths) == Set([[1, 2], [1, 3, 2], [1, 4, 2]]) + @test Set(paths) == Set([[1, 2], [1, 3, 2], [1, 4, 2]]) - # non trivial graph + # nontrivial graph g = SimpleDiGraph(6) add_edge!(g, 1, 2) add_edge!(g, 2, 3) @@ -95,14 +110,14 @@ add_edge!(g, 5, 4) paths = all_simple_paths(g, 2, [3, 4]) - @test Set(p for p in paths) == Set([ + @test Set(paths) == Set([ [2, 3], [2, 4, 5, 3], [2, 6, 5, 3], [2, 4], [2, 3, 4], [2, 6, 5, 4], [2, 6, 5, 3, 4] ]) paths = all_simple_paths(g, 2, [3, 4]; cutoff=3) - @test Set(p for p in paths) == + @test Set(paths) == Set([[2, 3], [2, 4, 5, 3], [2, 6, 5, 3], [2, 4], [2, 3, 4], [2, 6, 5, 4]]) paths = all_simple_paths(g, 2, [3, 4]; cutoff=2) - @test Set(p for p in paths) == Set([[2, 3], [2, 4], [2, 3, 4]]) + @test Set(paths) == Set([[2, 3], [2, 4], [2, 3, 4]]) end From bbb5d985d9ff0467ba6d246c7c1a1ccabec32a8e Mon Sep 17 00:00:00 2001 From: Thomas Christensen Date: Fri, 22 Mar 2024 15:03:42 +0100 Subject: [PATCH 08/16] fix formatting --- src/traversals/all_simple_paths.jl | 22 ++++++++++------------ test/traversals/all_simple_paths.jl | 4 ++-- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/src/traversals/all_simple_paths.jl b/src/traversals/all_simple_paths.jl index 2e2b1ea32..153e93689 100644 --- a/src/traversals/all_simple_paths.jl +++ b/src/traversals/all_simple_paths.jl @@ -40,14 +40,13 @@ julia> collect(all_simple_paths(g, 1, 4; cutoff=2)) ``` """ function all_simple_paths( - g::AbstractGraph{T}, u::T, vs; cutoff::T=nv(g)-1 + g::AbstractGraph{T}, u::T, vs; cutoff::T=nv(g) - 1 ) where {T<:Integer} vs = vs isa Set{T} ? vs : Set{T}(vs) return SimplePathIterator(g, u, vs, cutoff) end -# Iterator that generates all simple paths in `g` from `u` to `vs` of a length at most -# `cutoff`. +# iterator over all simple paths from `u` to `vs` in `g` of length less than `cutoff` struct SimplePathIterator{T<:Integer,G<:AbstractGraph{T}} g::G u::T # start vertex @@ -71,13 +70,13 @@ Base.IteratorSize(::Type{<:SimplePathIterator}) = Base.SizeUnknown() Base.eltype(::SimplePathIterator{T}) where {T} = Vector{T} mutable struct SimplePathIteratorState{T<:Integer} - stack::Stack{Tuple{T, T}} # used to restore iteration of child vertices: elements are + stack::Stack{Tuple{T,T}} # used to restore iteration of child vertices: elements are # (parent vertex, index of children) - visited::Stack{T} # current path candidate - queued::Vector{T} # remaining targets if path length reached cutoff + visited::Stack{T} # current path candidate + queued::Vector{T} # remaining targets if path length reached cutoff end function SimplePathIteratorState(spi::SimplePathIterator{T}) where {T<:Integer} - stack = Stack{Tuple{T, T}}() + stack = Stack{Tuple{T,T}}() visited = Stack{T}() queued = Vector{T}() push!(visited, spi.u) # add a starting vertex to the path candidate @@ -91,7 +90,7 @@ function _stepback!(state::SimplePathIteratorState) # updates iterator state. return nothing end -# Returns the next simple path in `spi`, according to a depth-first search +# iterates to the next simple path in `spi`, according to a depth-first search function Base.iterate( spi::SimplePathIterator{T}, state::SimplePathIteratorState=SimplePathIteratorState(spi) ) where {T<:Integer} @@ -108,14 +107,13 @@ function Base.iterate( parent_node, next_child_index = first(state.stack) children = outneighbors(spi.g, parent_node) if length(children) < next_child_index - # all children have been checked, step back. - _stepback!(state) + _stepback!(state) # all children have been checked, step back continue end child = children[next_child_index] - next_child_index′ = pop!(state.stack)[2] # move child index forward - push!(state.stack, (parent_node, next_child_index′+1)) # ↩ + next_child_index′ = pop!(state.stack)[2] # move child index forward + push!(state.stack, (parent_node, next_child_index′ + 1)) # ↩ child in state.visited && continue if length(state.visited) == spi.cutoff diff --git a/test/traversals/all_simple_paths.jl b/test/traversals/all_simple_paths.jl index 8932cfee6..ba5c599de 100644 --- a/test/traversals/all_simple_paths.jl +++ b/test/traversals/all_simple_paths.jl @@ -69,7 +69,7 @@ add_edge!(g, 2, 4) paths = all_simple_paths(g, 1, [3, 4]) @test Set(paths) == Set([[1, 2, 3], [1, 2, 4]]) - + # another digraph with a cycle; check cycles are excluded, regardless of cutoff g = SimpleDiGraph(6) add_edge!(g, 1, 2) @@ -78,7 +78,7 @@ add_edge!(g, 4, 5) add_edge!(g, 5, 2) add_edge!(g, 5, 6) - paths = all_simple_paths(g, 1, 6) + paths = all_simple_paths(g, 1, 6) paths′ = all_simple_paths(g, 1, 6; cutoff=typemax(Int)) @test Set(paths) == Set(paths′) == Set([[1, 2, 3, 4, 5, 6]]) From 0991093ba1a06bf4f7c2bf014e9c7fdfdb122150 Mon Sep 17 00:00:00 2001 From: Thomas Christensen Date: Fri, 22 Mar 2024 17:32:50 +0100 Subject: [PATCH 09/16] special-case `u in vs` input: include 0-length path `[u]` in iterates --- src/traversals/all_simple_paths.jl | 14 ++++++++++++-- test/traversals/all_simple_paths.jl | 13 +++++++------ 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/src/traversals/all_simple_paths.jl b/src/traversals/all_simple_paths.jl index 153e93689..0194cc6f8 100644 --- a/src/traversals/all_simple_paths.jl +++ b/src/traversals/all_simple_paths.jl @@ -10,6 +10,9 @@ the graph `g` from a source vertex `u` to a target vertex `v` or iterable of tar The iterator's elements (i.e., the paths) can be materialized via `collect` or `iterate`. Paths are iterated in the order of a depth-first search. +If the requested path has identical source and target vertices, i.e., if `u = v`, a +zero-length path `[u]` is included among the iterates. + ## Keyword arguments The maximum path length (i.e., number of edges) is limited by the keyword argument `cutoff` (default, `nv(g)-1`). If a path's path length is greater than or equal to `cutoff`, it is @@ -74,6 +77,7 @@ mutable struct SimplePathIteratorState{T<:Integer} # (parent vertex, index of children) visited::Stack{T} # current path candidate queued::Vector{T} # remaining targets if path length reached cutoff + self_visited::Bool # in case `u ∈ vs`, we want to return a `[u]` path once only end function SimplePathIteratorState(spi::SimplePathIterator{T}) where {T<:Integer} stack = Stack{Tuple{T,T}}() @@ -81,7 +85,7 @@ function SimplePathIteratorState(spi::SimplePathIterator{T}) where {T<:Integer} queued = Vector{T}() push!(visited, spi.u) # add a starting vertex to the path candidate push!(stack, (spi.u, 1)) # add a child node with index 1 - return SimplePathIteratorState{T}(stack, visited, queued) + return SimplePathIteratorState{T}(stack, visited, queued, false) end function _stepback!(state::SimplePathIteratorState) # updates iterator state. @@ -112,7 +116,7 @@ function Base.iterate( end child = children[next_child_index] - next_child_index′ = pop!(state.stack)[2] # move child index forward + next_child_index′ = pop!(state.stack)[2] # move child index forward push!(state.stack, (parent_node, next_child_index′ + 1)) # ↩ child in state.visited && continue @@ -146,4 +150,10 @@ function Base.iterate( end end end + + # special-case: when `vs` includes `u`, return also a 1-vertex, 0-length path `[u]` + if spi.u in spi.vs && !state.self_visited + state.self_visited = true + return [spi.u], state + end end diff --git a/test/traversals/all_simple_paths.jl b/test/traversals/all_simple_paths.jl index ba5c599de..efca6f11f 100644 --- a/test/traversals/all_simple_paths.jl +++ b/test/traversals/all_simple_paths.jl @@ -8,7 +8,6 @@ # printing @test sprint(show, paths) == "SimplePathIterator{SimpleGraph{Int64}}(1 → 4)" - # single path with cutoff g = complete_graph(4) @test collect(all_simple_paths(g, 1, 4; cutoff=2)) == [[1, 2, 4], [1, 3, 4], [1, 4]] @@ -82,12 +81,14 @@ paths′ = all_simple_paths(g, 1, 6; cutoff=typemax(Int)) @test Set(paths) == Set(paths′) == Set([[1, 2, 3, 4, 5, 6]]) - # source equals targets - g = SimpleGraph(4) - paths = all_simple_paths(g, 1, 1) - @test Set(paths) == Set{Int}() + # same source and target vertex + g = path_graph(4) + @test Set(all_simple_paths(g, 1, 1)) == Set([[1]]) + @test Set(all_simple_paths(g, 3, 3)) == Set([[3]]) + @test Set(all_simple_paths(g, 1, [1, 1])) == Set([[1]]) + @test Set(all_simple_paths(g, 1, [1, 4])) == Set([[1], [1, 2, 3, 4]]) - # cutoff prunes paths (note: path length is node - 1) + # cutoff prunes paths (note: maximum path length below is `nv(g) - 1`) g = complete_graph(4) paths = all_simple_paths(g, 1, 2; cutoff=1) @test Set(paths) == Set([[1, 2]]) From 4d6fde5a935cba1c097074cd13d3f7ba4e21c6c4 Mon Sep 17 00:00:00 2001 From: Thomas Christensen Date: Fri, 5 Apr 2024 17:03:33 +0200 Subject: [PATCH 10/16] updates after code review --- test/traversals/all_simple_paths.jl | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/test/traversals/all_simple_paths.jl b/test/traversals/all_simple_paths.jl index efca6f11f..6f1d9fdbd 100644 --- a/test/traversals/all_simple_paths.jl +++ b/test/traversals/all_simple_paths.jl @@ -2,15 +2,14 @@ # single path g = path_graph(4) paths = all_simple_paths(g, 1, 4) - @test Set(paths) == Set([[1, 2, 3, 4]]) - @test Set(collect(paths)) == Set([[1, 2, 3, 4]]) + @test Set(paths) == Set(collect(paths)) == Set([[1, 2, 3, 4]]) # printing @test sprint(show, paths) == "SimplePathIterator{SimpleGraph{Int64}}(1 → 4)" - # single path with cutoff + # complete graph with cutoff g = complete_graph(4) - @test collect(all_simple_paths(g, 1, 4; cutoff=2)) == [[1, 2, 4], [1, 3, 4], [1, 4]] + @test Set(all_simple_paths(g, 1, 4; cutoff=2)) == Set([[1, 2, 4], [1, 3, 4], [1, 4]]) # two paths g = path_graph(4) @@ -18,14 +17,18 @@ add_edge!(g, 3, 5) paths = all_simple_paths(g, 1, [4, 5]) @test Set(paths) == Set([[1, 2, 3, 4], [1, 2, 3, 5]]) - @test Set(collect(paths)) == Set([[1, 2, 3, 4], [1, 2, 3, 5]]) + @test Set(collect(paths)) == Set([[1, 2, 3, 4], [1, 2, 3, 5]]) # check `collect` also - # two paths with cutoff + # two paths, with one beyond a cut-off g = path_graph(4) add_vertex!(g) add_edge!(g, 3, 5) - paths = all_simple_paths(g, 1, [4, 5]; cutoff=3) - @test Set(paths) == Set([[1, 2, 3, 4], [1, 2, 3, 5]]) + add_vertex!(g) + add_edge!(g, 5, 6) + paths = all_simple_paths(g, 1, [4, 6]) + @test Set(paths) == Set([[1, 2, 3, 4], [1, 2, 3, 5, 6]]) + paths = all_simple_paths(g, 1, [4, 6]; cutoff=3) + @test Set(paths) == Set([[1, 2, 3, 4]]) # two targets in line emits two paths g = path_graph(4) From 596910e15b0580e5c93788983e8e6505017bbbd6 Mon Sep 17 00:00:00 2001 From: Thomas Christensen Date: Fri, 5 Apr 2024 17:04:18 +0200 Subject: [PATCH 11/16] Update src/traversals/all_simple_paths.jl Co-authored-by: Guillaume Dalle <22795598+gdalle@users.noreply.github.com> --- src/traversals/all_simple_paths.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/traversals/all_simple_paths.jl b/src/traversals/all_simple_paths.jl index 0194cc6f8..ba78a697c 100644 --- a/src/traversals/all_simple_paths.jl +++ b/src/traversals/all_simple_paths.jl @@ -140,7 +140,7 @@ function Base.iterate( # update state variables push!(state.visited, child) # move to child vertex if !isempty(setdiff(spi.vs, state.visited)) # expand stack until all targets are found - push!(state.stack, (child, 1)) # add the child node as a parent for next iteration + push!(state.stack, (child, one(T))) # add the child node as a parent for next iteration else pop!(state.visited) # step back and explore the remaining child nodes end From 17194551eff32998fd3e44669bec7b6eff30a959 Mon Sep 17 00:00:00 2001 From: Thomas Christensen Date: Fri, 5 Apr 2024 17:04:25 +0200 Subject: [PATCH 12/16] Update src/traversals/all_simple_paths.jl Co-authored-by: Guillaume Dalle <22795598+gdalle@users.noreply.github.com> --- src/traversals/all_simple_paths.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/traversals/all_simple_paths.jl b/src/traversals/all_simple_paths.jl index ba78a697c..8ed8d2c98 100644 --- a/src/traversals/all_simple_paths.jl +++ b/src/traversals/all_simple_paths.jl @@ -117,7 +117,7 @@ function Base.iterate( child = children[next_child_index] next_child_index′ = pop!(state.stack)[2] # move child index forward - push!(state.stack, (parent_node, next_child_index′ + 1)) # ↩ + push!(state.stack, (parent_node, next_child_index′ + one(T))) # ↩ child in state.visited && continue if length(state.visited) == spi.cutoff From a0942284130f30e7d8122ae8635c22778cdd0e81 Mon Sep 17 00:00:00 2001 From: Thomas Christensen Date: Fri, 5 Apr 2024 17:04:32 +0200 Subject: [PATCH 13/16] Update src/traversals/all_simple_paths.jl Co-authored-by: Guillaume Dalle <22795598+gdalle@users.noreply.github.com> --- src/traversals/all_simple_paths.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/traversals/all_simple_paths.jl b/src/traversals/all_simple_paths.jl index 8ed8d2c98..aa6537f20 100644 --- a/src/traversals/all_simple_paths.jl +++ b/src/traversals/all_simple_paths.jl @@ -84,7 +84,7 @@ function SimplePathIteratorState(spi::SimplePathIterator{T}) where {T<:Integer} visited = Stack{T}() queued = Vector{T}() push!(visited, spi.u) # add a starting vertex to the path candidate - push!(stack, (spi.u, 1)) # add a child node with index 1 + push!(stack, (spi.u, one(T))) # add a child node with index 1 return SimplePathIteratorState{T}(stack, visited, queued, false) end From 236fe477d8180955e79bf31154d96e62106071fb Mon Sep 17 00:00:00 2001 From: Thomas Christensen Date: Fri, 5 Apr 2024 17:05:17 +0200 Subject: [PATCH 14/16] Apply suggestions from code review Co-authored-by: Guillaume Dalle <22795598+gdalle@users.noreply.github.com> --- src/traversals/all_simple_paths.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/traversals/all_simple_paths.jl b/src/traversals/all_simple_paths.jl index aa6537f20..d6235d8a7 100644 --- a/src/traversals/all_simple_paths.jl +++ b/src/traversals/all_simple_paths.jl @@ -15,7 +15,7 @@ zero-length path `[u]` is included among the iterates. ## Keyword arguments The maximum path length (i.e., number of edges) is limited by the keyword argument `cutoff` -(default, `nv(g)-1`). If a path's path length is greater than or equal to `cutoff`, it is +(default, `nv(g)-1`). If a path's path length is greater than `cutoff`, it is omitted. ## Examples @@ -33,7 +33,7 @@ julia> collect(spi) [1, 3, 4] [1, 4] ``` -We can restrict the search to paths of length less than a specified cut-off (here, 2 edges): +We can restrict the search to paths of length less than or equal to a specified cut-off (here, 2 edges): ```jldoctest allsimplepaths; setup = :(using Graphs) julia> collect(all_simple_paths(g, 1, 4; cutoff=2)) 3-element Vector{Vector{Int64}}: From c9953b5a98654fc668a102c921a1c39abce20858 Mon Sep 17 00:00:00 2001 From: Thomas Christensen Date: Fri, 5 Apr 2024 17:11:26 +0200 Subject: [PATCH 15/16] more updates from code-review --- src/traversals/all_simple_paths.jl | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/traversals/all_simple_paths.jl b/src/traversals/all_simple_paths.jl index d6235d8a7..15120a764 100644 --- a/src/traversals/all_simple_paths.jl +++ b/src/traversals/all_simple_paths.jl @@ -33,7 +33,8 @@ julia> collect(spi) [1, 3, 4] [1, 4] ``` -We can restrict the search to paths of length less than or equal to a specified cut-off (here, 2 edges): +We can restrict the search to path lengths less than or equal to a specified cut-off (here, +2 edges): ```jldoctest allsimplepaths; setup = :(using Graphs) julia> collect(all_simple_paths(g, 1, 4; cutoff=2)) 3-element Vector{Vector{Int64}}: @@ -43,7 +44,7 @@ julia> collect(all_simple_paths(g, 1, 4; cutoff=2)) ``` """ function all_simple_paths( - g::AbstractGraph{T}, u::T, vs; cutoff::T=nv(g) - 1 + g::AbstractGraph{T}, u::T, vs; cutoff::T=nv(g) - one(T) ) where {T<:Integer} vs = vs isa Set{T} ? vs : Set{T}(vs) return SimplePathIterator(g, u, vs, cutoff) @@ -73,7 +74,7 @@ Base.IteratorSize(::Type{<:SimplePathIterator}) = Base.SizeUnknown() Base.eltype(::SimplePathIterator{T}) where {T} = Vector{T} mutable struct SimplePathIteratorState{T<:Integer} - stack::Stack{Tuple{T,T}} # used to restore iteration of child vertices: elements are + stack::Stack{Tuple{T,T}} # used to restore iteration of child vertices: elements are ↩ # (parent vertex, index of children) visited::Stack{T} # current path candidate queued::Vector{T} # remaining targets if path length reached cutoff @@ -116,8 +117,8 @@ function Base.iterate( end child = children[next_child_index] - next_child_index′ = pop!(state.stack)[2] # move child index forward - push!(state.stack, (parent_node, next_child_index′ + one(T))) # ↩ + next_child_index_tmp = pop!(state.stack)[2] # move child ↩ + push!(state.stack, (parent_node, next_child_index_tmp + one(T))) # index forward child in state.visited && continue if length(state.visited) == spi.cutoff From 1c9a813ba92dfe8071212bf130678459f84df7b6 Mon Sep 17 00:00:00 2001 From: Thomas Christensen Date: Fri, 5 Apr 2024 17:25:15 +0200 Subject: [PATCH 16/16] format --- test/traversals/all_simple_paths.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/traversals/all_simple_paths.jl b/test/traversals/all_simple_paths.jl index 6f1d9fdbd..25eb76580 100644 --- a/test/traversals/all_simple_paths.jl +++ b/test/traversals/all_simple_paths.jl @@ -2,7 +2,7 @@ # single path g = path_graph(4) paths = all_simple_paths(g, 1, 4) - @test Set(paths) == Set(collect(paths)) == Set([[1, 2, 3, 4]]) + @test Set(paths) == Set(collect(paths)) == Set([[1, 2, 3, 4]]) # printing @test sprint(show, paths) == "SimplePathIterator{SimpleGraph{Int64}}(1 → 4)"