-
Notifications
You must be signed in to change notification settings - Fork 103
All simple paths (refresh #20) #353
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
Changes from all commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
6b49e56
`all_simple_paths`: update PR #20
thchr 391cbab
fixes to tests & doctests
thchr 4e678b3
improve docstring
thchr b9cd00a
run JuliaFormatter
thchr 2ca2338
bump to v1.9.1
thchr 46602f9
fix docs
thchr 0479b6f
address code-review
thchr bbb5d98
fix formatting
thchr 0991093
special-case `u in vs` input: include 0-length path `[u]` in iterates
thchr 4d6fde5
updates after code review
thchr 596910e
Update src/traversals/all_simple_paths.jl
thchr 1719455
Update src/traversals/all_simple_paths.jl
thchr a094228
Update src/traversals/all_simple_paths.jl
thchr 236fe47
Apply suggestions from code review
thchr c9953b5
more updates from code-review
thchr 1c9a813
format
thchr File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or 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 hidden or 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 hidden or 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 hidden or 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,160 @@ | ||
""" | ||
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 | ||
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. | ||
|
||
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 `cutoff`, it is | ||
omitted. | ||
|
||
## Examples | ||
```jldoctest allsimplepaths; setup = :(using Graphs) | ||
julia> g = complete_graph(4); | ||
|
||
julia> spi = all_simple_paths(g, 1, 4) | ||
SimplePathIterator{SimpleGraph{Int64}}(1 → 4) | ||
|
||
julia> collect(spi) | ||
5-element Vector{Vector{Int64}}: | ||
thchr marked this conversation as resolved.
Show resolved
Hide resolved
|
||
[1, 2, 3, 4] | ||
[1, 2, 4] | ||
[1, 3, 2, 4] | ||
[1, 3, 4] | ||
[1, 4] | ||
``` | ||
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}}: | ||
[1, 2, 4] | ||
[1, 3, 4] | ||
[1, 4] | ||
``` | ||
""" | ||
function all_simple_paths( | ||
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) | ||
end | ||
|
||
# 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 | ||
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, ']') | ||
thchr marked this conversation as resolved.
Show resolved
Hide resolved
|
||
end | ||
print(io, ')') | ||
return nothing | ||
end | ||
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 ↩ | ||
# (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}}() | ||
visited = Stack{T}() | ||
queued = Vector{T}() | ||
push!(visited, spi.u) # add a starting vertex to the path candidate | ||
push!(stack, (spi.u, one(T))) # add a child node with index 1 | ||
return SimplePathIteratorState{T}(stack, visited, queued, false) | ||
end | ||
|
||
function _stepback!(state::SimplePathIteratorState) # updates iterator state. | ||
pop!(state.stack) | ||
pop!(state.visited) | ||
return nothing | ||
end | ||
|
||
# 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} | ||
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_child_index = first(state.stack) | ||
children = outneighbors(spi.g, parent_node) | ||
if length(children) < next_child_index | ||
_stepback!(state) # all children have been checked, step back | ||
continue | ||
end | ||
|
||
child = children[next_child_index] | ||
thchr marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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 | ||
# collect adjacent targets if more exist and add them to queue | ||
rest_children = Set(children[next_child_index:end]) | ||
thchr marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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, one(T))) # add the child node as a parent for next iteration | ||
else | ||
pop!(state.visited) # step back and explore the remaining child nodes | ||
gdalle marked this conversation as resolved.
Show resolved
Hide resolved
|
||
end | ||
|
||
if !isnothing(result) # found a new path, return it | ||
return result, state | ||
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 |
This file contains hidden or 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 hidden or 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,127 @@ | ||
@testset "All simple paths" begin | ||
# single path | ||
g = path_graph(4) | ||
paths = all_simple_paths(g, 1, 4) | ||
@test Set(paths) == Set(collect(paths)) == Set([[1, 2, 3, 4]]) | ||
|
||
# printing | ||
@test sprint(show, paths) == "SimplePathIterator{SimpleGraph{Int64}}(1 → 4)" | ||
|
||
# complete graph with cutoff | ||
g = complete_graph(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) | ||
add_vertex!(g) | ||
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]]) # check `collect` also | ||
|
||
# two paths, with one beyond a cut-off | ||
g = path_graph(4) | ||
add_vertex!(g) | ||
add_edge!(g, 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 | ||
thchr marked this conversation as resolved.
Show resolved
Hide resolved
|
||
g = path_graph(4) | ||
add_vertex!(g) | ||
paths = all_simple_paths(g, 1, [3, 4]) | ||
@test Set(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(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(paths) == Set([[1, 2, 3, 4], [1, 2, 3, 5]]) | ||
|
||
# digraph with a cycle | ||
thchr marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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(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(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]]) | ||
|
||
# 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: 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]]) | ||
|
||
paths = all_simple_paths(g, 1, 2; cutoff=2) | ||
@test Set(paths) == Set([[1, 2], [1, 3, 2], [1, 4, 2]]) | ||
|
||
# nontrivial 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(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(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(paths) == Set([[2, 3], [2, 4], [2, 3, 4]]) | ||
end |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.