-
Notifications
You must be signed in to change notification settings - Fork 93
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Browse the repository at this point in the history
* `all_simple_paths`: update PR #20 - this updates the port of sbromberger/LightGraphs.jl#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 <[email protected]> Co-authored-by: Etienne dg <[email protected]> * fixes to tests & doctests * improve docstring * run JuliaFormatter - `format(Graphs, overwrite=true)` * bump to v1.9.1 * fix docs * address code-review * fix formatting * special-case `u in vs` input: include 0-length path `[u]` in iterates * updates after code review * Update src/traversals/all_simple_paths.jl Co-authored-by: Guillaume Dalle <[email protected]> * Update src/traversals/all_simple_paths.jl Co-authored-by: Guillaume Dalle <[email protected]> * Update src/traversals/all_simple_paths.jl Co-authored-by: Guillaume Dalle <[email protected]> * Apply suggestions from code review Co-authored-by: Guillaume Dalle <[email protected]> * more updates from code-review * format --------- Co-authored-by: akiyuki ishikawa <[email protected]> Co-authored-by: Etienne dg <[email protected]> Co-authored-by: Guillaume Dalle <[email protected]>
- Loading branch information
1 parent
9291314
commit c1cedee
Showing
6 changed files
with
296 additions
and
2 deletions.
There are no files selected for viewing
This file contains 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 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 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 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}}: | ||
[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, ']') | ||
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] | ||
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]) | ||
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 | ||
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 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 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 | ||
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 | ||
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 |