Skip to content
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

[Port] A proposal of an all_simple_paths function implementation #20

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all 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/Graphs.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
188 changes: 188 additions & 0 deletions src/traversals/allsimplepaths.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
using DataStructures

"""
all_simple_paths(g, source, targets, cutoff)
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
all_simple_paths(g, source, targets, cutoff)
all_simple_paths(g, source, targets; cutoff)


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::T=typemax(T)) where T <: Integer
return SimplePathIterator(g, source, Set(targets), cutoff=cutoff)
end


"""
all_simple_paths(g, source, target, cutoff)

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::T=typemax(T)) where T <: Integer
return SimplePathIterator(g, source, Set(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
Copy link
Contributor

Choose a reason for hiding this comment

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

This doesn't have to be a Set, right? Maybe just make it targets::T

Copy link
Contributor

Choose a reason for hiding this comment

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

This is designed to support multi-targets, and I wanted to be consistent with the networkx implementation.
Users can use the single target API :
all_simple_paths(g::AbstractGraph, source::T, target::T; cutoff::T=typemax(T)) where T <: Integer

cutoff::T # Max length of resulting paths

function SimplePathIterator(g::AbstractGraph, source::T, targets::Set{T}; cutoff::T=typemax(T)) where T <: Integer
new{T}(g, source, targets, cutoff)
end
end


"""
SimplePathIteratorState{T <: Integer}

SimplePathIterator's state.
"""
mutable struct SimplePathIteratorState{T <: Integer}
stack::Stack{Vector{T}} # Store information used to restore iteration of child nodes. Each vector has two elements which are a parent node and an index of children.
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, [spi.source, 1]) # Add a child node with index = 1
new{T}(stack, visited, queued_targets)
end
end

"""
function _stepback!(state)

A helper function that updates iterator state.
For internal use only.
Comment on lines +84 to +85
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
A helper function that updates iterator state.
For internal use only.
A helper function that updates the simple path iterator state.

Internal use is already implied by the underscore

"""
function _stepback!(state::SimplePathIteratorState)
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

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]
# Move child index forward.
first(state.stack)[2] += 1

if child in state.visited
# Avoid loop
continue
end

if 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, [child, 1]) # Add the child node as a parent for next iteration.
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 = Set(children[next_childe_index: end])
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 _ 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