Skip to content

Commit c1cedee

Browse files
thchri-aki-yetiennedeggdalle
authored
All simple paths (refresh #20) (#353)
* `all_simple_paths`: update PR #20 - this updates the port of sbromberger/LightGraphs.jl#1540 from #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: akiyuki ishikawa <aki.y.ishikawa@gmail.com> Co-authored-by: Etienne dg <etienne.mace_de_gastines@insa-rouen.fr> * fixes to tests & doctests * improve docstring * run JuliaFormatter - `format(Graphs, overwrite=true)` * bump to v1.9.1 * fix docs * address code-review * fix formatting * special-case `u in vs` input: include 0-length path `[u]` in iterates * updates after code review * Update src/traversals/all_simple_paths.jl Co-authored-by: Guillaume Dalle <22795598+gdalle@users.noreply.github.com> * Update src/traversals/all_simple_paths.jl Co-authored-by: Guillaume Dalle <22795598+gdalle@users.noreply.github.com> * Update src/traversals/all_simple_paths.jl Co-authored-by: Guillaume Dalle <22795598+gdalle@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: Guillaume Dalle <22795598+gdalle@users.noreply.github.com> * more updates from code-review * format --------- Co-authored-by: akiyuki ishikawa <aki.y.ishikawa@gmail.com> Co-authored-by: Etienne dg <etienne.mace_de_gastines@insa-rouen.fr> Co-authored-by: Guillaume Dalle <22795598+gdalle@users.noreply.github.com>
1 parent 9291314 commit c1cedee

File tree

6 files changed

+296
-2
lines changed

6 files changed

+296
-2
lines changed

Project.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name = "Graphs"
22
uuid = "86223c79-3864-5bf0-83f7-82e725a168b6"
3-
version = "1.9.0"
3+
version = "1.10.0"
44

55
[deps]
66
ArnoldiMethod = "ec485272-7323-5ecc-a04f-4719b315124d"

docs/src/algorithms/traversals.md

+1
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,6 @@ Pages = [
2121
"traversals/maxadjvisit.jl",
2222
"traversals/randomwalks.jl",
2323
"traversals/eulerian.jl",
24+
"traversals/all_simple_paths.jl",
2425
]
2526
```

src/Graphs.jl

+6-1
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:
@@ -197,6 +198,9 @@ export
197198
# eulerian
198199
eulerian,
199200

201+
# all simple paths
202+
all_simple_paths,
203+
200204
# coloring
201205
greedy_color,
202206

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

+160
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
"""
2+
all_simple_paths(g, u, v; cutoff) --> Graphs.SimplePathIterator
3+
all_simple_paths(g, u, vs; cutoff) --> Graphs.SimplePathIterator
4+
5+
Returns an iterator that generates all
6+
[simple paths](https://en.wikipedia.org/wiki/Path_(graph_theory)#Walk,_trail,_and_path) in
7+
the graph `g` from a source vertex `u` to a target vertex `v` or iterable of target vertices
8+
`vs`. A simple path has no repeated vertices.
9+
10+
The iterator's elements (i.e., the paths) can be materialized via `collect` or `iterate`.
11+
Paths are iterated in the order of a depth-first search.
12+
13+
If the requested path has identical source and target vertices, i.e., if `u = v`, a
14+
zero-length path `[u]` is included among the iterates.
15+
16+
## Keyword arguments
17+
The maximum path length (i.e., number of edges) is limited by the keyword argument `cutoff`
18+
(default, `nv(g)-1`). If a path's path length is greater than `cutoff`, it is
19+
omitted.
20+
21+
## Examples
22+
```jldoctest allsimplepaths; setup = :(using Graphs)
23+
julia> g = complete_graph(4);
24+
25+
julia> spi = all_simple_paths(g, 1, 4)
26+
SimplePathIterator{SimpleGraph{Int64}}(1 → 4)
27+
28+
julia> collect(spi)
29+
5-element Vector{Vector{Int64}}:
30+
[1, 2, 3, 4]
31+
[1, 2, 4]
32+
[1, 3, 2, 4]
33+
[1, 3, 4]
34+
[1, 4]
35+
```
36+
We can restrict the search to path lengths less than or equal to a specified cut-off (here,
37+
2 edges):
38+
```jldoctest allsimplepaths; setup = :(using Graphs)
39+
julia> collect(all_simple_paths(g, 1, 4; cutoff=2))
40+
3-element Vector{Vector{Int64}}:
41+
[1, 2, 4]
42+
[1, 3, 4]
43+
[1, 4]
44+
```
45+
"""
46+
function all_simple_paths(
47+
g::AbstractGraph{T}, u::T, vs; cutoff::T=nv(g) - one(T)
48+
) where {T<:Integer}
49+
vs = vs isa Set{T} ? vs : Set{T}(vs)
50+
return SimplePathIterator(g, u, vs, cutoff)
51+
end
52+
53+
# iterator over all simple paths from `u` to `vs` in `g` of length less than `cutoff`
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+
return nothing
72+
end
73+
Base.IteratorSize(::Type{<:SimplePathIterator}) = Base.SizeUnknown()
74+
Base.eltype(::SimplePathIterator{T}) where {T} = Vector{T}
75+
76+
mutable struct SimplePathIteratorState{T<:Integer}
77+
stack::Stack{Tuple{T,T}} # used to restore iteration of child vertices: elements are ↩
78+
# (parent vertex, index of children)
79+
visited::Stack{T} # current path candidate
80+
queued::Vector{T} # remaining targets if path length reached cutoff
81+
self_visited::Bool # in case `u ∈ vs`, we want to return a `[u]` path once only
82+
end
83+
function SimplePathIteratorState(spi::SimplePathIterator{T}) where {T<:Integer}
84+
stack = Stack{Tuple{T,T}}()
85+
visited = Stack{T}()
86+
queued = Vector{T}()
87+
push!(visited, spi.u) # add a starting vertex to the path candidate
88+
push!(stack, (spi.u, one(T))) # add a child node with index 1
89+
return SimplePathIteratorState{T}(stack, visited, queued, false)
90+
end
91+
92+
function _stepback!(state::SimplePathIteratorState) # updates iterator state.
93+
pop!(state.stack)
94+
pop!(state.visited)
95+
return nothing
96+
end
97+
98+
# iterates to the next simple path in `spi`, according to a depth-first search
99+
function Base.iterate(
100+
spi::SimplePathIterator{T}, state::SimplePathIteratorState=SimplePathIteratorState(spi)
101+
) where {T<:Integer}
102+
while !isempty(state.stack)
103+
if !isempty(state.queued) # consume queued targets
104+
target = pop!(state.queued)
105+
result = vcat(reverse(collect(state.visited)), target)
106+
if isempty(state.queued)
107+
_stepback!(state)
108+
end
109+
return result, state
110+
end
111+
112+
parent_node, next_child_index = first(state.stack)
113+
children = outneighbors(spi.g, parent_node)
114+
if length(children) < next_child_index
115+
_stepback!(state) # all children have been checked, step back
116+
continue
117+
end
118+
119+
child = children[next_child_index]
120+
next_child_index_tmp = pop!(state.stack)[2] # move child ↩
121+
push!(state.stack, (parent_node, next_child_index_tmp + one(T))) # index forward
122+
child in state.visited && continue
123+
124+
if length(state.visited) == spi.cutoff
125+
# collect adjacent targets if more exist and add them to queue
126+
rest_children = Set(children[next_child_index:end])
127+
state.queued = collect(
128+
setdiff(intersect(spi.vs, rest_children), Set(state.visited))
129+
)
130+
131+
if isempty(state.queued)
132+
_stepback!(state)
133+
end
134+
else
135+
result = if child in spi.vs
136+
vcat(reverse(collect(state.visited)), child)
137+
else
138+
nothing
139+
end
140+
141+
# update state variables
142+
push!(state.visited, child) # move to child vertex
143+
if !isempty(setdiff(spi.vs, state.visited)) # expand stack until all targets are found
144+
push!(state.stack, (child, one(T))) # add the child node as a parent for next iteration
145+
else
146+
pop!(state.visited) # step back and explore the remaining child nodes
147+
end
148+
149+
if !isnothing(result) # found a new path, return it
150+
return result, state
151+
end
152+
end
153+
end
154+
155+
# special-case: when `vs` includes `u`, return also a 1-vertex, 0-length path `[u]`
156+
if spi.u in spi.vs && !state.self_visited
157+
state.self_visited = true
158+
return [spi.u], state
159+
end
160+
end

test/runtests.jl

+1
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

+127
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(paths) == Set(collect(paths)) == Set([[1, 2, 3, 4]])
6+
7+
# printing
8+
@test sprint(show, paths) == "SimplePathIterator{SimpleGraph{Int64}}(1 → 4)"
9+
10+
# complete graph with cutoff
11+
g = complete_graph(4)
12+
@test Set(all_simple_paths(g, 1, 4; cutoff=2)) == Set([[1, 2, 4], [1, 3, 4], [1, 4]])
13+
14+
# two paths
15+
g = path_graph(4)
16+
add_vertex!(g)
17+
add_edge!(g, 3, 5)
18+
paths = all_simple_paths(g, 1, [4, 5])
19+
@test Set(paths) == Set([[1, 2, 3, 4], [1, 2, 3, 5]])
20+
@test Set(collect(paths)) == Set([[1, 2, 3, 4], [1, 2, 3, 5]]) # check `collect` also
21+
22+
# two paths, with one beyond a cut-off
23+
g = path_graph(4)
24+
add_vertex!(g)
25+
add_edge!(g, 3, 5)
26+
add_vertex!(g)
27+
add_edge!(g, 5, 6)
28+
paths = all_simple_paths(g, 1, [4, 6])
29+
@test Set(paths) == Set([[1, 2, 3, 4], [1, 2, 3, 5, 6]])
30+
paths = all_simple_paths(g, 1, [4, 6]; cutoff=3)
31+
@test Set(paths) == Set([[1, 2, 3, 4]])
32+
33+
# two targets in line emits two paths
34+
g = path_graph(4)
35+
add_vertex!(g)
36+
paths = all_simple_paths(g, 1, [3, 4])
37+
@test Set(paths) == Set([[1, 2, 3], [1, 2, 3, 4]])
38+
39+
# two paths digraph
40+
g = SimpleDiGraph(5)
41+
add_edge!(g, 1, 2)
42+
add_edge!(g, 2, 3)
43+
add_edge!(g, 3, 4)
44+
add_edge!(g, 3, 5)
45+
paths = all_simple_paths(g, 1, [4, 5])
46+
@test Set(paths) == Set([[1, 2, 3, 4], [1, 2, 3, 5]])
47+
48+
# two paths digraph with cutoff
49+
g = SimpleDiGraph(5)
50+
add_edge!(g, 1, 2)
51+
add_edge!(g, 2, 3)
52+
add_edge!(g, 3, 4)
53+
add_edge!(g, 3, 5)
54+
paths = all_simple_paths(g, 1, [4, 5]; cutoff=3)
55+
@test Set(paths) == Set([[1, 2, 3, 4], [1, 2, 3, 5]])
56+
57+
# digraph with a cycle
58+
g = SimpleDiGraph(4)
59+
add_edge!(g, 1, 2)
60+
add_edge!(g, 2, 3)
61+
add_edge!(g, 3, 1)
62+
add_edge!(g, 2, 4)
63+
paths = all_simple_paths(g, 1, 4)
64+
@test Set(paths) == Set([[1, 2, 4]])
65+
66+
# digraph with a cycle; paths with two targets share a node in the cycle
67+
g = SimpleDiGraph(4)
68+
add_edge!(g, 1, 2)
69+
add_edge!(g, 2, 3)
70+
add_edge!(g, 3, 1)
71+
add_edge!(g, 2, 4)
72+
paths = all_simple_paths(g, 1, [3, 4])
73+
@test Set(paths) == Set([[1, 2, 3], [1, 2, 4]])
74+
75+
# another digraph with a cycle; check cycles are excluded, regardless of cutoff
76+
g = SimpleDiGraph(6)
77+
add_edge!(g, 1, 2)
78+
add_edge!(g, 2, 3)
79+
add_edge!(g, 3, 4)
80+
add_edge!(g, 4, 5)
81+
add_edge!(g, 5, 2)
82+
add_edge!(g, 5, 6)
83+
paths = all_simple_paths(g, 1, 6)
84+
paths′ = all_simple_paths(g, 1, 6; cutoff=typemax(Int))
85+
@test Set(paths) == Set(paths′) == Set([[1, 2, 3, 4, 5, 6]])
86+
87+
# same source and target vertex
88+
g = path_graph(4)
89+
@test Set(all_simple_paths(g, 1, 1)) == Set([[1]])
90+
@test Set(all_simple_paths(g, 3, 3)) == Set([[3]])
91+
@test Set(all_simple_paths(g, 1, [1, 1])) == Set([[1]])
92+
@test Set(all_simple_paths(g, 1, [1, 4])) == Set([[1], [1, 2, 3, 4]])
93+
94+
# cutoff prunes paths (note: maximum path length below is `nv(g) - 1`)
95+
g = complete_graph(4)
96+
paths = all_simple_paths(g, 1, 2; cutoff=1)
97+
@test Set(paths) == Set([[1, 2]])
98+
99+
paths = all_simple_paths(g, 1, 2; cutoff=2)
100+
@test Set(paths) == Set([[1, 2], [1, 3, 2], [1, 4, 2]])
101+
102+
# nontrivial graph
103+
g = SimpleDiGraph(6)
104+
add_edge!(g, 1, 2)
105+
add_edge!(g, 2, 3)
106+
add_edge!(g, 3, 4)
107+
add_edge!(g, 4, 5)
108+
109+
add_edge!(g, 1, 6)
110+
add_edge!(g, 2, 6)
111+
add_edge!(g, 2, 4)
112+
add_edge!(g, 6, 5)
113+
add_edge!(g, 5, 3)
114+
add_edge!(g, 5, 4)
115+
116+
paths = all_simple_paths(g, 2, [3, 4])
117+
@test Set(paths) == Set([
118+
[2, 3], [2, 4, 5, 3], [2, 6, 5, 3], [2, 4], [2, 3, 4], [2, 6, 5, 4], [2, 6, 5, 3, 4]
119+
])
120+
121+
paths = all_simple_paths(g, 2, [3, 4]; cutoff=3)
122+
@test Set(paths) ==
123+
Set([[2, 3], [2, 4, 5, 3], [2, 6, 5, 3], [2, 4], [2, 3, 4], [2, 6, 5, 4]])
124+
125+
paths = all_simple_paths(g, 2, [3, 4]; cutoff=2)
126+
@test Set(paths) == Set([[2, 3], [2, 4], [2, 3, 4]])
127+
end

0 commit comments

Comments
 (0)