From eb24d67669d23b9b1dfaf2b1cbcb80e091569138 Mon Sep 17 00:00:00 2001 From: akiyuki ishikawa Date: Sat, 20 Feb 2021 14:29:59 +0900 Subject: [PATCH 1/3] Add all_simple_paths function Add a function that finds all simple paths between two nodes in a graph. --- src/Graphs.jl | 4 + src/traversals/allsimplepaths.jl | 185 ++++++++++++++++++++++++++++++ test/runtests.jl | 5 +- test/traversals/allsimplepaths.jl | 124 ++++++++++++++++++++ 4 files changed, 316 insertions(+), 2 deletions(-) create mode 100644 src/traversals/allsimplepaths.jl create mode 100644 test/traversals/allsimplepaths.jl diff --git a/src/Graphs.jl b/src/Graphs.jl index ca07d57c0..fb63bfbf2 100644 --- a/src/Graphs.jl +++ b/src/Graphs.jl @@ -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, @@ -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") diff --git a/src/traversals/allsimplepaths.jl b/src/traversals/allsimplepaths.jl new file mode 100644 index 000000000..e57b8ad38 --- /dev/null +++ b/src/traversals/allsimplepaths.jl @@ -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 diff --git a/test/runtests.jl b/test/runtests.jl index 303ae1584..1a615c1f2 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -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 = [ @@ -46,6 +46,7 @@ tests = [ "shortestpaths/floyd-warshall", "shortestpaths/yen", "shortestpaths/spfa", + "traversals/allsimplepaths", "traversals/bfs", "traversals/bipartition", "traversals/greedy_color", diff --git a/test/traversals/allsimplepaths.jl b/test/traversals/allsimplepaths.jl new file mode 100644 index 000000000..94bc2b919 --- /dev/null +++ b/test/traversals/allsimplepaths.jl @@ -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 From a59781ea8320c5ecb4a5f9241e32dd43e903dafc Mon Sep 17 00:00:00 2001 From: akiyuki ishikawa Date: Mon, 17 May 2021 09:51:54 +0900 Subject: [PATCH 2/3] Apply review results --- src/traversals/allsimplepaths.jl | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/traversals/allsimplepaths.jl b/src/traversals/allsimplepaths.jl index e57b8ad38..1b2a3dd38 100644 --- a/src/traversals/allsimplepaths.jl +++ b/src/traversals/allsimplepaths.jl @@ -1,7 +1,7 @@ using DataStructures """ - all_simple_paths(g, source, targets, cutoff=nothing) + 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`. @@ -23,21 +23,21 @@ julia> collect(all_simple_paths(g, 1, [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) +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=nothing) + 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::Union{Int,Nothing}=nothing) where T <: Integer - return SimplePathIterator(g, source, [target], cutoff=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 @@ -51,10 +51,10 @@ struct SimplePathIterator{T <: Integer} g::AbstractGraph source::T # Starting node targets::Set{T} # Target nodes - cutoff::Union{Int,Nothing} # Max length of resulting paths + cutoff::T # 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) + function SimplePathIterator(g::AbstractGraph, source::T, targets::Set{T}; cutoff::T=typemax(T)) where T <: Integer + new{T}(g, source, targets, cutoff) end end @@ -79,12 +79,12 @@ mutable struct SimplePathIteratorState{T <: Integer} end """ - function stepback!(state) + function _stepback!(state) A helper function that updates iterator state. For internal use only. """ -function stepback!(state::SimplePathIteratorState) +function _stepback!(state::SimplePathIteratorState) pop!(state.stack) pop!(state.visited) end @@ -107,7 +107,7 @@ function Base.iterate(spi::SimplePathIterator{T}, state::Union{SimplePathIterato target = pop!(state.queued_targets) result = vcat(reverse(collect(state.visited)), target) if isempty(state.queued_targets) - stepback!(state) + _stepback!(state) end return result, state end @@ -116,7 +116,7 @@ function Base.iterate(spi::SimplePathIterator{T}, state::Union{SimplePathIterato if isempty(children) # Now leaf node, step back. - stepback!(state) + _stepback!(state) continue end @@ -126,7 +126,7 @@ function Base.iterate(spi::SimplePathIterator{T}, state::Union{SimplePathIterato continue end - if isnothing(spi.cutoff) || length(state.visited) < spi.cutoff + if length(state.visited) < spi.cutoff result = (child in spi.targets) ? vcat(reverse(collect(state.visited)), [child]) : nothing # Update state variables @@ -148,7 +148,7 @@ function Base.iterate(spi::SimplePathIterator{T}, state::Union{SimplePathIterato state.queued_targets = collect(setdiff(intersect(spi.targets, rest_children), Set(state.visited))) if isempty(state.queued_targets) - stepback!(state) + _stepback!(state) end end end @@ -178,7 +178,7 @@ 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 + for _ in spi c += 1 end return c From acf0d5ab75c4c7d86e0c0208b75cd80ef23bd9a6 Mon Sep 17 00:00:00 2001 From: akiyuki ishikawa Date: Tue, 18 May 2021 09:33:17 +0900 Subject: [PATCH 3/3] Fix stack usage To improve memory effeciency, make the stack store only parent node and index. --- src/traversals/allsimplepaths.jl | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/traversals/allsimplepaths.jl b/src/traversals/allsimplepaths.jl index 1b2a3dd38..deb436b16 100644 --- a/src/traversals/allsimplepaths.jl +++ b/src/traversals/allsimplepaths.jl @@ -65,7 +65,7 @@ end SimplePathIterator's state. """ mutable struct SimplePathIteratorState{T <: Integer} - stack::Stack{Vector{T}} # Store child nodes + 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 @@ -73,7 +73,7 @@ mutable struct SimplePathIteratorState{T <: Integer} 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 + push!(stack, [spi.source, 1]) # Add a child node with index = 1 new{T}(stack, visited, queued_targets) end end @@ -94,7 +94,7 @@ 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` +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 @@ -112,15 +112,18 @@ function Base.iterate(spi::SimplePathIterator{T}, state::Union{SimplePathIterato return result, state end - children = first(state.stack) - - if isempty(children) - # Now leaf node, step back. + 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 = pop!(children) + child = children[next_childe_index] + # Move child index forward. + first(state.stack)[2] += 1 + if child in state.visited # Avoid loop continue @@ -132,7 +135,7 @@ function Base.iterate(spi::SimplePathIterator{T}, state::Union{SimplePathIterato # 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 + 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 @@ -144,7 +147,7 @@ function Base.iterate(spi::SimplePathIterator{T}, state::Union{SimplePathIterato else # Now length(visited) == cutoff # Collect adjacent targets if exist and add them to queue. - rest_children = union(Set(children), Set(child)) + 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)