Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name = "Graphs"
uuid = "86223c79-3864-5bf0-83f7-82e725a168b6"
version = "1.9.0"
version = "1.9.1"

[deps]
ArnoldiMethod = "ec485272-7323-5ecc-a04f-4719b315124d"
Expand Down
1 change: 1 addition & 0 deletions docs/src/algorithms/traversals.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,6 @@ Pages = [
"traversals/maxadjvisit.jl",
"traversals/randomwalks.jl",
"traversals/eulerian.jl",
"traversals/all_simple_paths.jl",
]
```
7 changes: 6 additions & 1 deletion src/Graphs.jl
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ using DataStructures:
union!,
find_root!,
BinaryMaxHeap,
BinaryMinHeap
BinaryMinHeap,
Stack
using LinearAlgebra: I, Symmetric, diagm, eigen, eigvals, norm, rmul!, tril, triu
import LinearAlgebra: Diagonal, issymmetric, mul!
using Random:
Expand Down Expand Up @@ -197,6 +198,9 @@ export
# eulerian
eulerian,

# all simple paths
all_simple_paths,

# coloring
greedy_color,

Expand Down Expand Up @@ -496,6 +500,7 @@ include("traversals/maxadjvisit.jl")
include("traversals/randomwalks.jl")
include("traversals/diffusion.jl")
include("traversals/eulerian.jl")
include("traversals/all_simple_paths.jl")
include("connectivity.jl")
include("distance.jl")
include("editdist.jl")
Expand Down
147 changes: 147 additions & 0 deletions src/traversals/all_simple_paths.jl
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.
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
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}}:
[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):
```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, ']')
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
# two elements: a parent vertex and an index of children
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
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
end

if !isnothing(result) # found a new path, return it
return result, state
end
end
end
end
1 change: 1 addition & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ tests = [
"traversals/randomwalks",
"traversals/diffusion",
"traversals/eulerian",
"traversals/all_simple_paths",
"community/cliques",
"community/core-periphery",
"community/label_propagation",
Expand Down
108 changes: 108 additions & 0 deletions test/traversals/all_simple_paths.jl
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
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
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