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

Commit

Permalink
Merge 5a9952f into 8576861
Browse files Browse the repository at this point in the history
  • Loading branch information
i-aki-y authored May 13, 2021
2 parents 8576861 + 5a9952f commit 2f13330
Show file tree
Hide file tree
Showing 4 changed files with 316 additions and 2 deletions.
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
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

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
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)
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

0 comments on commit 2f13330

Please sign in to comment.