-
Notifications
You must be signed in to change notification settings - Fork 183
A proposal of an all_simple_paths function implementation #1540
base: master
Are you sure you want to change the base?
Changes from 2 commits
e153441
5a9952f
e055166
95005fb
ead91c4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,185 @@ | ||
using DataStructures | ||
|
||
""" | ||
all_simple_paths(g, source, targets, cutoff=nothing) | ||
|
||
Returns an iterator that generates all simple paths in the graph `g` from `source` to `targets`. | ||
If `cutoff` is given, the paths' lengths are limited to equal or less than `cutoff`. | ||
Note that the length of a path is defined as the number of edges, not the number of elements. | ||
ex. the path length of `[1, 2, 3]` is two. | ||
Internally, a DFS algorithm is used to search paths. | ||
|
||
# Examples | ||
|
||
```jldoctest | ||
julia> using LightGraphs | ||
julia> g = complete_graph(4) | ||
julia> collect(all_simple_paths(g, 1, [4])) | ||
5-element Array{Array{Int64,1},1}: | ||
[1, 4] | ||
[1, 3, 4] | ||
[1, 3, 2, 4] | ||
[1, 2, 4] | ||
[1, 2, 3, 4] | ||
``` | ||
""" | ||
function all_simple_paths(g::AbstractGraph, source::T, targets::Vector{T}; cutoff::Union{Int,Nothing}=nothing) where T <: Integer | ||
return SimplePathIterator(g, source, targets, cutoff=cutoff) | ||
end | ||
|
||
|
||
""" | ||
all_simple_paths(g, source, target, cutoff=nothing) | ||
|
||
This function is equivalent to `all_simple_paths(g, source, [target], cutoff)`. | ||
This is provided for convenience. | ||
|
||
See also `all_simple_paths(g, source, targets, cutoff)`. | ||
""" | ||
function all_simple_paths(g::AbstractGraph, source::T, target::T; cutoff::Union{Int,Nothing}=nothing) where T <: Integer | ||
return SimplePathIterator(g, source, [target], cutoff=cutoff) | ||
end | ||
|
||
|
||
""" | ||
SimplePathIterator{T <: Integer} | ||
|
||
Iterator that generates all simple paths. | ||
The iterator holds the condition specified in `all_simple_path` function. | ||
""" | ||
struct SimplePathIterator{T <: Integer} | ||
g::AbstractGraph | ||
source::T # Starting node | ||
targets::Set{T} # Target nodes | ||
cutoff::Union{Int,Nothing} # Max length of resulting paths | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Instead of a union, keep this as T with the default as There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see, I hadn't thought of that fix. |
||
|
||
function SimplePathIterator(g::AbstractGraph, source::T, targets::Vector{T}; cutoff::Union{Int,Nothing}=nothing) where T <: Integer | ||
new{T}(g, source, Set(targets), cutoff) | ||
end | ||
end | ||
|
||
|
||
""" | ||
SimplePathIteratorState{T <: Integer} | ||
|
||
SimplePathIterator's state. | ||
""" | ||
mutable struct SimplePathIteratorState{T <: Integer} | ||
stack::Stack{Vector{T}} # Store child nodes | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This makes the struct There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, if the graph is dense and the path is long, the stack can grow |
||
visited::Stack{T} # Store current path candidate | ||
queued_targets::Vector{T} # Store rest targets if path length reached cutoff. | ||
function SimplePathIteratorState(spi::SimplePathIterator{T}) where T <: Integer | ||
stack = Stack{Vector{T}}() | ||
visited = Stack{T}() | ||
queued_targets = Vector{T}() | ||
push!(visited, spi.source) # Add a starting node to the path candidate | ||
push!(stack, copy(outneighbors(spi.g, spi.source))) # Add child nodes from the start | ||
new{T}(stack, visited, queued_targets) | ||
end | ||
end | ||
|
||
""" | ||
function stepback!(state) | ||
|
||
A helper function that updates iterator state. | ||
For internal use only. | ||
""" | ||
function stepback!(state::SimplePathIteratorState) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if it's for internal use, perhaps prefix it with an underscore. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I renamed the function name: |
||
pop!(state.stack) | ||
pop!(state.visited) | ||
end | ||
|
||
|
||
""" | ||
Base.iterate(spi::SimplePathIterator{T}, state=nothing) | ||
|
||
Returns a next simple path based on DFS. | ||
If `cutoff` is specified in `SimplePathIterator`, the path length is limited up to `cutoff` | ||
""" | ||
function Base.iterate(spi::SimplePathIterator{T}, state::Union{SimplePathIteratorState,Nothing}=nothing) where T <: Integer | ||
|
||
state = isnothing(state) ? SimplePathIteratorState(spi) : state | ||
|
||
while !isempty(state.stack) | ||
|
||
if !isempty(state.queued_targets) | ||
# Consumes queueed targets | ||
target = pop!(state.queued_targets) | ||
result = vcat(reverse(collect(state.visited)), target) | ||
if isempty(state.queued_targets) | ||
stepback!(state) | ||
end | ||
return result, state | ||
end | ||
|
||
children = first(state.stack) | ||
|
||
if isempty(children) | ||
# Now leaf node, step back. | ||
stepback!(state) | ||
continue | ||
end | ||
|
||
child = pop!(children) | ||
if child in state.visited | ||
# Avoid loop | ||
continue | ||
end | ||
|
||
if isnothing(spi.cutoff) || length(state.visited) < spi.cutoff | ||
result = (child in spi.targets) ? vcat(reverse(collect(state.visited)), [child]) : nothing | ||
|
||
# Update state variables | ||
push!(state.visited, child) # Move to child node | ||
if !isempty(setdiff(spi.targets, state.visited)) # Expand stack until find all targets | ||
push!(state.stack, copy(outneighbors(spi.g, child))) # Add child nodes and step forward | ||
else | ||
pop!(state.visited) # Step back and explore the remaining child nodes | ||
end | ||
|
||
# If found a new path, returns it. | ||
if !isnothing(result) | ||
return result, state | ||
end | ||
else | ||
# Now length(visited) == cutoff | ||
# Collect adjacent targets if exist and add them to queue. | ||
rest_children = union(Set(children), Set(child)) | ||
state.queued_targets = collect(setdiff(intersect(spi.targets, rest_children), Set(state.visited))) | ||
|
||
if isempty(state.queued_targets) | ||
stepback!(state) | ||
end | ||
end | ||
end | ||
end | ||
|
||
|
||
""" | ||
Base.collect(spi::SimplePathIterator{T}) | ||
|
||
Makes an array of paths from iterator. | ||
Note that this can take much memory space and cpu time when the graph is dense. | ||
""" | ||
function Base.collect(spi::SimplePathIterator{T}) where T <: Integer | ||
res = Vector{Vector{T}}() | ||
for x in spi | ||
push!(res, x) | ||
end | ||
return res | ||
end | ||
|
||
|
||
""" | ||
Base.length(spi::SimplePathIterator{T}) | ||
|
||
Returns searched paths count. | ||
Note that this can take much cpu time when the graph is dense. | ||
""" | ||
function Base.length(spi::SimplePathIterator{T}) where T <: Integer | ||
c = 0 | ||
for x in spi | ||
c += 1 | ||
end | ||
return c | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,124 @@ | ||
@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) | ||
|
||
# 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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
targets
is aVector
here, but in the struct, it's aSet
. Perhaps we should not type it here, and construct aSet
out of it in these functions instead of in the inner constructor of the struct.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I moved the typecasting from Structure to
all_simple_paths
function. If I misunderstood your point let me know.