Skip to content
This repository has been archived by the owner on Oct 8, 2021. It is now read-only.

A proposal of an all_simple_paths function implementation #1540

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/LightGraphs.jl
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ difference, symmetric_difference,
join, tensor_product, cartesian_product, crosspath,
induced_subgraph, egonet, merge_vertices!, merge_vertices,

# allsimplepaths
all_simple_paths,

# bfs
gdistances, gdistances!, bfs_tree, bfs_parents, has_path,

Expand Down Expand Up @@ -219,6 +222,7 @@ include("cycles/hawick-james.jl")
include("cycles/karp.jl")
include("cycles/basis.jl")
include("cycles/limited_length.jl")
include("traversals/allsimplepaths.jl")
include("traversals/bfs.jl")
include("traversals/bipartition.jl")
include("traversals/greedy_color.jl")
Expand Down
185 changes: 185 additions & 0 deletions src/traversals/allsimplepaths.jl
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
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

targets is a Vector here, but in the struct, it's a Set. Perhaps we should not type it here, and construct a Set out of it in these functions instead of in the inner constructor of the struct.

Copy link
Author

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.

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
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of a union, keep this as T with the default as typmax(T).

Copy link
Author

Choose a reason for hiding this comment

The 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
Copy link
Owner

@sbromberger sbromberger May 13, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes the struct O(|V|^2) in memory, right?

Copy link
Author

Choose a reason for hiding this comment

The 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 O(|V|^2).

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)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if it's for internal use, perhaps prefix it with an underscore.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I renamed the function name: _stepback!

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
5 changes: 3 additions & 2 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@ using Statistics: mean

const testdir = dirname(@__FILE__)

testgraphs(g) = is_directed(g) ? [g, DiGraph{UInt8}(g), DiGraph{Int16}(g)] : [g, Graph{UInt8}(g), Graph{Int16}(g)]
testgraphs(g) = is_directed(g) ? [g, DiGraph{UInt8}(g), DiGraph{Int16}(g)] : [g, Graph{UInt8}(g), Graph{Int16}(g)]
testgraphs(gs...) = vcat((testgraphs(g) for g in gs)...)
testdigraphs = testgraphs

# some operations will create a large graph from two smaller graphs. We
# might error out on very small eltypes.
testlargegraphs(g) = is_directed(g) ? [g, DiGraph{UInt16}(g), DiGraph{Int32}(g)] : [g, Graph{UInt16}(g), Graph{Int32}(g)]
testlargegraphs(g) = is_directed(g) ? [g, DiGraph{UInt16}(g), DiGraph{Int32}(g)] : [g, Graph{UInt16}(g), Graph{Int32}(g)]
testlargegraphs(gs...) = vcat((testlargegraphs(g) for g in gs)...)

tests = [
Expand Down Expand Up @@ -46,6 +46,7 @@ tests = [
"shortestpaths/floyd-warshall",
"shortestpaths/yen",
"shortestpaths/spfa",
"traversals/allsimplepaths",
"traversals/bfs",
"traversals/bipartition",
"traversals/greedy_color",
Expand Down
124 changes: 124 additions & 0 deletions test/traversals/allsimplepaths.jl
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