Skip to content

Commit e9fa195

Browse files
committed
all_simple_paths: update PR JuliaGraphs#20
- this updates the port of sbromberger/LightGraphs.jl#1540 from JuliaGraphs#20 - has a number of simplifications relative to original implementation - original implementation by @i_aki_y - cutoff now defaults to `nv(g)` Co-authored-by: i_aki_y Co-authored-by: etiennedeg
1 parent e773bce commit e9fa195

File tree

4 files changed

+290
-1
lines changed

4 files changed

+290
-1
lines changed

src/Graphs.jl

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ using DataStructures:
2323
union!,
2424
find_root!,
2525
BinaryMaxHeap,
26-
BinaryMinHeap
26+
BinaryMinHeap,
27+
Stack
2728
using LinearAlgebra: I, Symmetric, diagm, eigen, eigvals, norm, rmul!, tril, triu
2829
import LinearAlgebra: Diagonal, issymmetric, mul!
2930
using Random:
@@ -196,6 +197,9 @@ export
196197

197198
# eulerian
198199
eulerian,
200+
201+
# all simple paths
202+
all_simple_paths,
199203

200204
# coloring
201205
greedy_color,
@@ -496,6 +500,7 @@ include("traversals/maxadjvisit.jl")
496500
include("traversals/randomwalks.jl")
497501
include("traversals/diffusion.jl")
498502
include("traversals/eulerian.jl")
503+
include("traversals/all_simple_paths.jl")
499504
include("connectivity.jl")
500505
include("distance.jl")
501506
include("editdist.jl")

src/traversals/all_simple_paths.jl

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
"""
2+
all_simple_paths(g, u, v; cutoff=nv(g)) --> Graphs.SimplePathIterator
3+
4+
Returns an iterator that generates all simple paths in the graph `g` from a source vertex
5+
`u` to a target vertex `v` or iterable of target vertices `vs`.
6+
7+
The iterator's elements (i.e., the paths) can be materialized via `collect` or `iterate`.
8+
Paths are iterated in the order of a depth-first search.
9+
10+
## Keyword arguments
11+
The maximum path length (i.e., number of edges) is limited by the keyword argument `cutoff`
12+
(default, `nv(g)`). If a path's path length is greater than or equal to `cutoff`, it is
13+
omitted.
14+
15+
## Examples
16+
```jldoctest
17+
julia> using Graphs
18+
julia> g = complete_graph(4)
19+
julia> spi = all_simple_paths(g, 1, 4)
20+
Graphs.SimplePathIterator(1 → 4)
21+
julia> collect(spi)
22+
5-element Vector{Vector{Int64}}:
23+
[1, 4]
24+
[1, 3, 4]
25+
[1, 3, 2, 4]
26+
[1, 2, 4]
27+
[1, 2, 3, 4]
28+
```
29+
We can restrict the search to paths of length less than a specified cut-off (here, 2 edges):
30+
```jldoctest
31+
julia> collect(all_simple_paths(g, 1, 4; cutoff=2))
32+
[1, 2, 4]
33+
[1, 3, 4]
34+
[1, 4]
35+
```
36+
"""
37+
function all_simple_paths(
38+
g::AbstractGraph{T},
39+
u::T,
40+
vs;
41+
cutoff::T=nv(g)
42+
) where T <: Integer
43+
44+
vs = vs isa Set{T} ? vs : Set{T}(vs)
45+
return SimplePathIterator(g, u, vs, cutoff)
46+
end
47+
48+
"""
49+
SimplePathIterator{T <: Integer}
50+
51+
Iterator that generates all simple paths in `g` from `u` to `vs` of a length at most
52+
`cutoff`.
53+
"""
54+
struct SimplePathIterator{T <: Integer, G <: AbstractGraph{T}}
55+
g::G
56+
u::T # start vertex
57+
vs::Set{T} # target vertices
58+
cutoff::T # max length of resulting paths
59+
end
60+
61+
function Base.show(io::IO, spi::SimplePathIterator)
62+
print(io, "SimplePathIterator{", typeof(spi.g), "}(", spi.u, "")
63+
if length(spi.vs) == 1
64+
print(io, only(spi.vs))
65+
else
66+
print(io, '[')
67+
join(io, spi.vs, ", ")
68+
print(io, ']')
69+
end
70+
print(io, ')')
71+
end
72+
Base.IteratorSize(::Type{<:SimplePathIterator}) = Base.SizeUnknown()
73+
Base.eltype(::SimplePathIterator{T}) where T = Vector{T}
74+
75+
mutable struct SimplePathIteratorState{T <: Integer}
76+
stack::Stack{Vector{T}} # used to restore iteration of child vertices; each vector has
77+
# two elements: a parent vertex and an index of children
78+
visited::Stack{T} # current path candidate
79+
queued::Vector{T} # remaining targets if path length reached cutoff
80+
end
81+
function SimplePathIteratorState(spi::SimplePathIterator{T}) where T <: Integer
82+
stack = Stack{Vector{T}}()
83+
visited = Stack{T}()
84+
queued = Vector{T}()
85+
push!(visited, spi.u) # add a starting vertex to the path candidate
86+
push!(stack, [spi.u, 1]) # add a child node with index 1
87+
SimplePathIteratorState{T}(stack, visited, queued)
88+
end
89+
90+
function _stepback!(state::SimplePathIteratorState) # updates iterator state.
91+
pop!(state.stack)
92+
pop!(state.visited)
93+
end
94+
95+
96+
"""
97+
Base.iterate(spi::SimplePathIterator{T}, state=nothing)
98+
99+
Returns the next simple path in `spi`, according to a depth-first search.
100+
"""
101+
function Base.iterate(
102+
spi::SimplePathIterator{T},
103+
state::SimplePathIteratorState=SimplePathIteratorState(spi)
104+
) where T <: Integer
105+
106+
while !isempty(state.stack)
107+
if !isempty(state.queued) # consume queued targets
108+
target = pop!(state.queued)
109+
result = vcat(reverse(collect(state.visited)), target)
110+
if isempty(state.queued)
111+
_stepback!(state)
112+
end
113+
return result, state
114+
end
115+
116+
parent_node, next_childe_index = first(state.stack)
117+
children = outneighbors(spi.g, parent_node)
118+
if length(children) < next_childe_index
119+
# all children have been checked, step back.
120+
_stepback!(state)
121+
continue
122+
end
123+
124+
child = children[next_childe_index]
125+
first(state.stack)[2] += 1 # move child index forward
126+
child in state.visited && continue
127+
128+
if length(state.visited) == spi.cutoff
129+
# collect adjacent targets if more exist and add them to queue
130+
rest_children = Set(children[next_childe_index: end])
131+
state.queued = collect(setdiff(intersect(spi.vs, rest_children), Set(state.visited)))
132+
133+
if isempty(state.queued)
134+
_stepback!(state)
135+
end
136+
else
137+
result = if child in spi.vs
138+
vcat(reverse(collect(state.visited)), child)
139+
else
140+
nothing
141+
end
142+
143+
# update state variables
144+
push!(state.visited, child) # move to child vertex
145+
if !isempty(setdiff(spi.vs, state.visited)) # expand stack until all targets are found
146+
push!(state.stack, [child, 1]) # add the child node as a parent for next iteration
147+
else
148+
pop!(state.visited) # step back and explore the remaining child nodes
149+
end
150+
151+
if !isnothing(result) # found a new path, return it
152+
return result, state
153+
end
154+
end
155+
end
156+
end

test/runtests.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ tests = [
109109
"traversals/randomwalks",
110110
"traversals/diffusion",
111111
"traversals/eulerian",
112+
"traversals/all_simple_paths",
112113
"community/cliques",
113114
"community/core-periphery",
114115
"community/label_propagation",

test/traversals/all_simple_paths.jl

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
@testset "All simple paths" begin
2+
# single path
3+
g = path_graph(4)
4+
paths = all_simple_paths(g, 1, 4)
5+
@test Set(p for p in paths) == Set([[1, 2, 3, 4]])
6+
@test Set(collect(paths)) == Set([[1, 2, 3, 4]])
7+
@test 1 == length(paths)
8+
9+
10+
# single path with cutoff
11+
@test collect(all_simple_paths(g, 1, 4; cutoff=2)) == [[1, 2, 4], [1, 3, 4], [1, 4]]
12+
13+
# two paths
14+
g = path_graph(4)
15+
add_vertex!(g)
16+
add_edge!(g, 3, 5)
17+
paths = all_simple_paths(g, 1, [4, 5])
18+
@test Set(p for p in paths) == Set([[1, 2, 3, 4], [1, 2, 3, 5]])
19+
@test Set(collect(paths)) == Set([[1, 2, 3, 4], [1, 2, 3, 5]])
20+
@test 2 == length(paths)
21+
22+
# two paths with cutoff
23+
g = path_graph(4)
24+
add_vertex!(g)
25+
add_edge!(g, 3, 5)
26+
paths = all_simple_paths(g, 1, [4, 5], cutoff=3)
27+
@test Set(p for p in paths) == Set([[1, 2, 3, 4], [1, 2, 3, 5]])
28+
29+
# two targets in line emits two paths
30+
g = path_graph(4)
31+
add_vertex!(g)
32+
paths = all_simple_paths(g, 1, [3, 4])
33+
@test Set(p for p in paths) == Set([[1, 2, 3], [1, 2, 3, 4]])
34+
35+
# two paths digraph
36+
g = SimpleDiGraph(5)
37+
add_edge!(g, 1, 2)
38+
add_edge!(g, 2, 3)
39+
add_edge!(g, 3, 4)
40+
add_edge!(g, 3, 5)
41+
paths = all_simple_paths(g, 1, [4, 5])
42+
@test Set(p for p in paths) == Set([[1, 2, 3, 4], [1, 2, 3, 5]])
43+
44+
# two paths digraph with cutoff
45+
g = SimpleDiGraph(5)
46+
add_edge!(g, 1, 2)
47+
add_edge!(g, 2, 3)
48+
add_edge!(g, 3, 4)
49+
add_edge!(g, 3, 5)
50+
paths = all_simple_paths(g, 1, [4, 5], cutoff=3)
51+
@test Set(p for p in paths) == Set([[1, 2, 3, 4], [1, 2, 3, 5]])
52+
53+
# digraph with a cycle
54+
g = SimpleDiGraph(4)
55+
add_edge!(g, 1, 2)
56+
add_edge!(g, 2, 3)
57+
add_edge!(g, 3, 1)
58+
add_edge!(g, 2, 4)
59+
paths = all_simple_paths(g, 1, 4)
60+
@test Set(p for p in paths) == Set([[1, 2, 4]])
61+
62+
# digraph with a cycle. paths with two targets share a node in the cycle.
63+
g = SimpleDiGraph(4)
64+
add_edge!(g, 1, 2)
65+
add_edge!(g, 2, 3)
66+
add_edge!(g, 3, 1)
67+
add_edge!(g, 2, 4)
68+
paths = all_simple_paths(g, 1, [3, 4])
69+
@test Set(p for p in paths) == Set([[1, 2, 3], [1, 2, 4]])
70+
71+
# source equals targets
72+
g = SimpleGraph(4)
73+
paths = all_simple_paths(g, 1, 1)
74+
@test Set(p for p in paths) == Set([])
75+
76+
# cutoff prones paths
77+
# Note, a path lenght is node - 1
78+
g = complete_graph(4)
79+
paths = all_simple_paths(g, 1, 2; cutoff=1)
80+
@test Set(p for p in paths) == Set([[1, 2]])
81+
82+
paths = all_simple_paths(g, 1, 2; cutoff=2)
83+
@test Set(p for p in paths) == Set([[1, 2], [1, 3, 2], [1, 4, 2]])
84+
85+
# non trivial graph
86+
g = SimpleDiGraph(6)
87+
add_edge!(g, 1, 2)
88+
add_edge!(g, 2, 3)
89+
add_edge!(g, 3, 4)
90+
add_edge!(g, 4, 5)
91+
92+
add_edge!(g, 1, 6)
93+
add_edge!(g, 2, 6)
94+
add_edge!(g, 2, 4)
95+
add_edge!(g, 6, 5)
96+
add_edge!(g, 5, 3)
97+
add_edge!(g, 5, 4)
98+
99+
paths = all_simple_paths(g, 2, [3, 4])
100+
@test Set(p for p in paths) == Set([
101+
[2, 3],
102+
[2, 4, 5, 3],
103+
[2, 6, 5, 3],
104+
[2, 4],
105+
[2, 3, 4],
106+
[2, 6, 5, 4],
107+
[2, 6, 5, 3, 4],
108+
])
109+
110+
paths = all_simple_paths(g, 2, [3, 4], cutoff=3)
111+
@test Set(p for p in paths) == Set([
112+
[2, 3],
113+
[2, 4, 5, 3],
114+
[2, 6, 5, 3],
115+
[2, 4],
116+
[2, 3, 4],
117+
[2, 6, 5, 4],
118+
])
119+
120+
paths = all_simple_paths(g, 2, [3, 4], cutoff=2)
121+
@test Set(p for p in paths) == Set([
122+
[2, 3],
123+
[2, 4],
124+
[2, 3, 4],
125+
])
126+
127+
end

0 commit comments

Comments
 (0)