An ImplicitGraph is a graph in which the vertices and edges are implicitly defined by two functions: one that tests for vertex membership and one that returns a list of the (out) neighbors of a vertex.
The vertex set of an ImplicitGraph may be finite or (implicitly) infinite. The (out) degrees, however, must be finite.
An ImplicitGraph is defined as follows:
ImplicitGraph{T}(has_vertex, out_neighbors)
where
Tis the data type of the vertices.has_vertex(v::T)::Boolis a function that takes objects of typeTas input and returnstrueifvis a vertex of the graph.out_neighbors(v::T)::Vector{T}is a function that takes objects of typeTas input and returns a list of the (out) neighbors ofv.
For example, the following creates an (essentially) infinite path whose vertices are integers (see the iPath function):
yes(v::Int)::Bool = true
N(v::Int)::Vector{Int} = [v-1, v+1]
G = ImplicitGraph{Int}(yes, N)The yes function always returns true for any Int. The N function returns the two neighbors of a vertex v. (For a truly infinite path, use BigInt in place of Int.)
Note that if v is an element of its own neighbor set, that represents a loop at vertex v.
The user-supplied out_neighbors function can be used to create both undirected and directed graphs. If an undirected graph is intended, be sure that if {v,w} is an edge of the graph, then w will be in the list returned by out_neighbors(v) and v will be in the list returned by out_neighbors(w).
To create an infinite directed path, the earlier example can be modified like this:
yes(v::Int)::Bool = true
N(v::Int)::Vector{Int} = [v+1]
G = ImplicitGraph{Int}(yes, N)We provide a few basic graphs that can be created using the following methods:
-
iCycle(n::Int)creates an undirected cycle with vertex set{1,2,...,n};iCycle(n,false)creates a directedn-cycle. -
iPath()creates an (essentially) infinite undirected path whose vertex set contains all integers (objects of typeInt);iPath(false)creates a one-way infinite path⋯ → -2 → -1 → 0 → 1 → 2 → ⋯. -
iGrid()creates an (essentially) infinite grid whose vertices are ordered pairs of integers (objects of typeInt). -
iCube(d::Int)creates ad-dimensional cube graph. The vertices are alld-long strings of0s and1s. Two vertices are adjacent iff they differ in exactly one bit. -
iKnight()creates the Knight's move graph on an (essentially) infinite chessboard. The vertices are pairs of integers (objects of typeInt). -
iShift(alphabet, n::Int)creates the shift digraph whose vertices aren-tuples of elements ofalphabet.
-
To test if
vis a vertex of anImplicitGraphG, usehas(G). Note that the data type ofvmust match the element type ofG. (The functioneltypereturns the data type of the vertices of theImplicitGraph.) -
To test if
{v,w}is an edge ofGuseG[v,w]orhas(G,v,w). Note thatvandwmust both be vertices ofGor an error is thrown. -
To get a list of the (out) neighbors of a vertex
v, useG[v]. -
To get the degree of a vertex in a graph, use
deg(G,v).
julia> G = iGrid()
ImplicitGraph{Tuple{Int64,Int64}}
julia> has_vertex(G,(1,2))
true
julia> G[(1,2)]
4-element Array{Tuple{Int64,Int64},1}:
(1, 1)
(1, 3)
(0, 2)
(2, 2)
julia> G[(1,2),(1,3)]
true
julia> deg(G,(5,0))
4
The function find_path finds a shortest path between vertices of a graph. This function may run without returning if the graph is infinite and disconnected.
julia> G = iGrid()
ImplicitGraph{Tuple{Int64, Int64}}
julia> find_path(G, (0, 0), (3, 5))
9-element Array{Tuple{Int64, Int64}, 1}:
(0, 0)
(0, 1)
(0, 2)
(0, 3)
(0, 4)
(0, 5)
(1, 5)
(2, 5)
(3, 5)
The function dist returns the length of a shortest path between vertices in the graph.
julia> dist(G, (0, 0), (3, 5))
8
For large undirected graphs, you may find that find_path_undirected runs faster.
It uses bidirectional search to reduce memory usage and runtime. It does not support
directed graphs, however.
Optionally, instead of a single target vertex whose type is the same as other vertices of the ImplicitGraph, we can call find_path with this signature:
find_path(G::ImplicitGraph{T}, s::T, is_target::Function) where {T}
The function is_target is expected to take a vertex of G as its only argument and return a Bool which is true if the vertex is a target. In this way, we can search for a path from the source vertex to one of many target vertices, or a vertex with a specified property.
Path finding can consume an amout of memory that is exponential in the length of the path, which can crash Julia. To avoid this, we can call find_path with this signature:
find_path(G::ImplicitGraph{T}, s::T, t::T, cutoff_depth::Int=0) where {T}
Paths with length at most cutoff_depth will be found, but attempting to find a longer path results in an empty output (as if the path did not exist):
julia> G = iGrid()
ImplicitGraph{Tuple{Int64,Int64}}
julia> find_path(G, (0, 0), (3, 5), 8)
9-element Array{Tuple{Int64 ,Int64}, 1}:
(0, 0)
(0, 1)
(0, 2)
(0, 3)
(0, 4)
(0, 5)
(1, 5)
(2, 5)
(3, 5)
julia> IG.find_path(G, (0, 0), (3, 5), 7)
Tuple{Int64, Int64}[]
Note: Setting
cutoff_depthto0allows a search for paths of unlimited length; only positive values limit the length.
The function guided_path_finder employs a score function to try to find a
path between vertices. It may be faster than find_path, but might not give a shortest path.
This function is called as follows: guided_path_finder(G,s,t,score=sc, depth=d, verbose=0) where
Gis anImplicitGraph,sis the starting vertex of the desired path,tis the ending vertex of the desired path,scis a score function that mapping vertices to integers and should get smaller as vertices get closer tot(and should minimize att),dcontrols amount of look ahead (default is1), andverbosesets how often to print progess information (or0for no diagnostics).
julia> G = iKnight();
julia> s = (9,9); t = (0,0);
julia> sc(v) = sum(abs.(v)); # score of (a,b) is |a| + |b|
julia> guided_path_finder(G,s,t,score=sc,depth=1)
9-element Vector{Tuple{Int64, Int64}}:
(9, 9)
(8, 7)
(7, 5)
(6, 3)
(5, 1)
(3, 0)
(1, -1)
(-1, -2)
(0, 0)
# With better look-ahead we find a shorter path
julia> guided_path_finder(G,s,t,score=sc,depth=3)
7-element Vector{Tuple{Int64, Int64}}:
(9, 9)
(8, 7)
(7, 5)
(6, 3)
(4, 2)
(2, 1)
(0, 0)
Greater depth can find a shorter path, but that comes at a cost:
julia> using BenchmarkTools
julia> @btime guided_path_finder(G,s,t,score=sc,depth=1);
52.361 μs (1308 allocations: 81.05 KiB)
julia> @btime guided_path_finder(G,s,t,score=sc,depth=3);
407.546 μs (8691 allocations: 696.47 KiB)
The extras directory contains additional code and examples that may be
useful in conjunction with the ImplicitGraph type. See the README
in that directory.