diff --git a/src/LightGraphs.jl b/src/LightGraphs.jl index 221092db4..02992ce4c 100644 --- a/src/LightGraphs.jl +++ b/src/LightGraphs.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 295e400ef..6f615cf1a 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