-
Notifications
You must be signed in to change notification settings - Fork 115
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 6 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,147 @@ | ||
| """ | ||
| all_simple_paths(g, u, v; cutoff=nv(g)) --> 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. | ||
thchr marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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 | ||
thchr marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| 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 paths of length less than a specified cut-off (here, 2 edges): | ||
thchr marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| ```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)) 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`. | ||
| 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{Vector{T}} # used to restore iteration of child vertices; each vector has | ||
thchr marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| # two elements: a parent vertex and an index of children | ||
thchr marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| 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 | ||
| return SimplePathIteratorState{T}(stack, visited, queued) | ||
| end | ||
|
|
||
| function _stepback!(state::SimplePathIteratorState) # updates iterator state. | ||
| pop!(state.stack) | ||
| pop!(state.visited) | ||
| return nothing | ||
| end | ||
|
|
||
| # Returns the next simple path in `spi`, according to a depth-first search | ||
gdalle marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| 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 | ||
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 | ||
| 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,108 @@ | ||
| @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]]) | ||
|
|
||
| # single path with cutoff | ||
thchr marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| g = complete_graph(4) | ||
| @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]]) | ||
|
|
||
| # two paths with cutoff | ||
thchr marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| g = path_graph(4) | ||
| add_vertex!(g) | ||
| add_edge!(g, 3, 5) | ||
| paths = all_simple_paths(g, 1, [4, 5]; cutoff=3) | ||
thchr marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| @test Set(p for p in paths) == Set([[1, 2, 3, 4], [1, 2, 3, 5]]) | ||
|
|
||
| # 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(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 | ||
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(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([]) | ||
thchr marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| # cutoff prones paths | ||
thchr marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| # Note, a path lenght is node - 1 | ||
thchr marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| 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 | ||
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.