16.1. Graph Basics#
using PyPlot # Packages used
16.1.1. Graphs#
A graph is a set of objects, represented as vertices, which may be connected (or related) to other vertices through edges. In a directed graph, there connections are ordered (that is, a vertex can be connected to another vertex, but not the opposite). In undirected graphs, all edges are simply seen as a connected between the two vertices.
There are many ways to represent a graph, and here we will use an adjancency list. The vertices will be numbered \(1,2,\ldots\) and represented in an array. Each vertex contains an integer arrays, which lists their “neighbors” - that is, whether the two vertices have a (directed) edge between them. For undirected graphs, we simply ensure that the edge appears in both vertices neighbor lists.
To begin with, we define a new type to represent a vertex. The most important field is
neighbors
, which is a vector of integers. We also include a coordinates
field which
is optional, but useful for plotting the graph. The Base.show
function is overloaded
to make the screen output of a vertex look good.
struct Vertex
neighbors::Vector{Int} # Indices of neighbors of this Vertex
coordinates::Vector{Float64} # 2D coordinates of this Vertex - only for plotting
Vertex(neighbors; coordinates=[0,0]) = new(neighbors, coordinates)
end
function Base.show(io::IO, v::Vertex)
print(io, "Neighbors = ", v.neighbors)
end
The entire graph can not be represented as a simple list of Vertex
elements. Again we implement
a simple custom Base.show
function for viewing the graph.
struct Graph
vertices::Vector{Vertex}
end
function Base.show(io::IO, g::Graph)
for i = 1:length(g.vertices)
println(io, "Vertex $i, ", g.vertices[i])
end
end
As an example, the code below creates a graph with 4 vertices, connected as follows:
Vertex 1 -> Vertex 4
Vertex 2 -> Vertices 1,4
Vertex 3 -> No vertices
Vertex 4 -> Vertex 2
We also include some \(x,y\) coordinates for plotting later.
v1 = Vertex([4], coordinates=[1,0.5])
v2 = Vertex([1,4], coordinates=[0,2])
v3 = Vertex([], coordinates=[-1,1])
v4 = Vertex([2], coordinates=[2,2])
g = Graph([v1,v2,v3,v4])
Vertex 1, Neighbors = [4]
Vertex 2, Neighbors = [1, 4]
Vertex 3, Neighbors = Int64[]
Vertex 4, Neighbors = [2]
16.1.2. Plotting a graph#
If \(x,y\) coordinates are provided for all vertices, we can plot graphs with circles
connected by arrows. The code below is not perfect but should work for many of our
examples. If the items appear in wrong sizes, the scale
argument can be used to
tweak it.
function PyPlot.plot(g::Graph; scale=1.0)
fig, ax = subplots()
ax.set_aspect("equal")
xmin = minimum(v.coordinates[1] for v in g.vertices)
xmax = maximum(v.coordinates[1] for v in g.vertices)
ymin = minimum(v.coordinates[2] for v in g.vertices)
ymax = maximum(v.coordinates[2] for v in g.vertices)
sz = max(xmax-xmin, ymax-ymin)
cr = scale*0.05sz
hw = cr/2
axis([xmin-2cr,xmax+2cr,ymin-2cr,ymax+2cr])
axis("off")
for i in 1:length(g.vertices)
c = g.vertices[i].coordinates
ax.add_artist(matplotlib.patches.Circle(c, cr, facecolor="none", edgecolor="k"))
ax.text(c[1], c[2], string(i),
horizontalalignment="center", verticalalignment="center", fontsize=round(Int, 14*scale))
for nb in g.vertices[i].neighbors
cnb = g.vertices[nb].coordinates
dc = cnb .- c
L = sqrt(sum(dc.^2))
c1 = c .+ cr/L * dc
c2 = cnb .- cr/L * dc
arrow(c1[1], c1[2], c2[1]-c1[1], c2[2]-c1[2],
head_width=hw, length_includes_head=true, facecolor="k")
end
end
end
plot(g)
16.1.3. Creating graphs#
We can create graphs using standard Julia array functions. For example, the functin below creates an undirected cycle graph with vertices positioned along the unit circle:
function circle_graph(nv=8)
g = Graph([])
for i = 1:nv
th = 2π*i/nv
v = Vertex([mod(i,nv)+1, mod(i-2,nv)+1], coordinates=[cos(th), sin(th)])
push!(g.vertices, v)
end
return g
end
circle_graph (generic function with 2 methods)
g = circle_graph(10)
plot(g)
The graph can be modified in a number of ways, see examples below:
g = circle_graph(10)
# Add edges by growing the neighbors array
push!(g.vertices[1].neighbors, 6); # Add edge 1->6
append!(g.vertices[4].neighbors, [2,7]); # Add 2 new edges
# Change edges by modifying the neighbors array
g.vertices[3].neighbors[1] = 1 # Change edge, 3->1 instead of 3->4
pop!(g.vertices[5].neighbors) # Remove edge
# Add a new vertex by growing the vertices array, and if needed
# updating some of the existing neighbors array
# Note that the new vertex should be numbered last, otherwise
# all node numbers must be reordered
newv = Vertex([1,4,9], coordinates=[-.1,.3])
push!(g.vertices, newv)
push!(g.vertices[2].neighbors, 11)
# Deleting a vertex is difficult using adjancency lists
# (unless it is the last vertex), since it requires renumbering
# of the vertices. But as a compromise, we can remove all edges
# to/from a vertex:
resize!(g.vertices[8].neighbors,0)
for v in g.vertices
filter!(i -> i != 8, v.neighbors)
end
plot(g)