In your latest homework assignment, the spec mentions that a graphical representation is convenient for representing website links, but we should stop to consider that Graphs themselves are a data type that we can explore!

Graphs are about as fun as a data type can get (if you ask me).

Definition

A graph is an abstract data type consisting of some number of connected Nodes, with fewer restrictions than Trees.

Definition

In graphical terms, we typically call Nodes vertices and the connections between them edges, which can be either directed (the edge points from one vertex to another) or not.

Such a loose definition! Let’s look at a graph…

Graph Properties

There are many, many graph-analytic properties that depend on their application, but we’ll look at some of the most common now.

Definition

A graph is said to be directed if every edge has at least one directed arrowhead.

This might even mean bidirected arrows with arrows on either end!

Definition

A graph is said to be acyclic if it contains no cycles.

A cycle exists in a graph whenever it is possible, by simply following some path of directed edges, to visit a node twice.

A very common graph representation involves the case where both of these properties are combined:

Definition

A directed, acyclic graph (also known as a DAG) is a directed graph with no directed cycles.

Example

Is the following graph a DAG?

Example

Is the following graph a DAG?

Question

In what scenarios do you think it’s important that graphs are directed and acyclic?

Graph Use-Cases

Now that we’ve seen some simple graphs and their properties, let’s think about when we might use them.

Example

Suggest some possible uses for graphs.

  • Shortest distances between geographic points.
  • Mapping social network relationships
  • Depicting links between websites / Wikipedia Articles
  • Planning for autonomous agents
  • Many other AI applications (search, probabilistic reasoning, etc.)

Graphs vs. Trees

Seeing how a graph is just any collection of vertices and edges, let’s think for a second…

Question

Are all trees also graphs?

Question

Are all graphs also trees?

Graphs vs. Trees

PropertyTreesGraphs
Notion of rootYesNot necessarily
One path from root to any nodeYesNot necessarily
Can contain cyclesNoYes
Nodes can have multiple inbound arrowsNoYes
Have methods of traversing nodesYesYes*

I put a star next to the graph capacity for traversal simply because, unlike with, say, a binary tree, in which we have defined traversal methods like left-to-right postorder, graphs can be traversed starting at any node and following a variety of different traversal schemas.

Additionally, we’re not guaranteed that we could traverse all elements of a graph, even if we have a systematic algorithm for doing so. Take the following graph, for example:

Starting at any one of these nodes, we’re not guaranteed that we’ll visit every other node in the graph! (in fact, in this case it’s not possible)

Long story short, there are a variety of niceties we had with trees that are lost with the loose constraints of graphs… but we also gain some cool stuff too.

Graph Traversals

There are a variety of graph traversals for different applications, but for our introduction, let’s just examine 1 or 2.

First, of course, we’ll need some definitions.

Definition

A traversal algorithm provides a procedural means of visiting edge-connected vertices in a graph.

Typically, we use traversal algorithms to process vertices in a graph, or are used in search operations.

Definition

While running, traversals maintain a frontier of vertices it has yet to visit, where the frontier is some DS that facilitates a traversal order.

Definition

A depth-first traversal of a graph, starting at some vertex V, maintains a Stack frontier that proceeds as follows:

create Stack frontier, Set visited
push starting vertex onto frontier
while frontier is not empty:
    pop vertex U on top of stack
    if U is in visited: continue
    visit U and add U to visited set
    for each child N of U:
        if N is not in visited:
            push N onto frontier

Question

Note the “mark V as ‘Visited’” step above; how would you go about accomplishing this efficiently and with clean programming practices?

Toolkit

Notice a couple key properties about traversals:

  • The “Depth” first part of this traversal lends itself to the fact that the frontier is a Stack (FILO) and so the most recently added children of any Node (which are “deeper” from the traversal’s origin) are visited first.
  • Traversals are not necessarily unique; although we say that we must add all unvisited children of a node to the frontier, this does not specify a particular ordering of that addition!

Example

Show a couple of depth-first traversals starting with node 1 in the following DAG:

Some viable depth-first traversals:

  • 1, 2, 4, 64, 16, 8, 32
  • 1, 16, 32, 8, 4, 64, 2

Definition

A breadth-first traversal of a graph, starting at some vertex V, maintains a Queue frontier that proceeds as follows:

create Queue frontier, Set visited
push starting vertex onto frontier
while frontier is not empty:
    pop vertex U at front of Queue
    if U is in visited: continue
    visit U and add U to visited set
    for each neighbor N of U:
        if N is not in visited:
            push N onto frontier

This is useful for tracing dependencies where we have to resolve the parents before the children!

Example

What are some viable breadth-first traversals of the following graph?

A viable breadth-first traversal: 1, 2, 16, 8, 32, 4, 64

Question

Why do you think this traversal method is called “breadth first?”

Representing Graphs

OK cool, but how do we store these graphs? What data structures can we use to represent them?

Definition

  1. The most obvious approach is to use some custom object class where we encode Nodes (vertexes) and Edges (with references to other Nodes) and can maintain data in either inner-class.

Toolkit

Benefits: Useful when the graph modeled is directed, sparse, and there is extra data to be stored in each Node (e.g., city population where Nodes are used to model cities on a geographic map).

Problem

Problems: this approach might be overkill if we have only undirected edges or lots of them, or if we’re only using the Nodes to store edges (in which case, the Node objects may be wasting memory / allocation time)!

Some alternative representations are:

Definition

  1. An adjacency matrix is a 2D array with a row / column for every node, and an indication at index [i][j] of whether node i is connected to node j.

Usually we indicate adjacencies with the number 1 to represent an edge going from node i to node j, and a 0 if there is no edge between them in an undirected graph.

However, we may also use an adjacency matrix when there are both directed and undirected edges by coding, e.g., 2 to represent an undirected connection, 1 to represent an edge from node i to j, -1 to represent an edge from node j to i.

Example

For example, an adjacency matrix denoting the graph (where each matrix index i corresponds to the node with value 2^i; so, for example, [0][1] corresponds to the edge from the node with value 1 to the node with value 2) above (with undirected edges instead) might look like:

     1  2  4  8  16 32 64
1   [0, 1, 0, 0, 1, 0, 0]
2   [0, 0, 1, 0, 0, 0, 0]
4   [0, 0, 0, 0, 0, 0, 1]
8   [0, 0, 1, 0, 0, 0, 1]
16  [0, 0, 0, 1, 0, 1, 0]
32  [0, 0, 0, 0, 0, 0, 0]
64  [0, 0, 0, 0, 0, 0, 0]

Toolkit

Benefits: Useful for large, densely connected graphs with potentially directed and undirected graphs.

Remark

Problem: this matrix can have a lot of 0s and waste space when there aren’t many edges in our graph. Also, in general, how to map the nodes to keys, or more complex data to store in each Node?

Definition

  1. An adjacency map stores each edge connection as a Map of Node keys to Lists / Sets of keys to which that Node is connected.

Example

For example, an adjacency map with integer keys (representing the ints at each node) for the above graph might look like:

keys  lists
 1    [] -> 2 -> 16
 2    [] -> 4
 4    [] -> 64
 8    [] -> 4 -> 64
16    [] -> 8 -> 32
32    []
64    []

Toolkit

Solves the index mapping problem of adjacency matrices while being more memory efficient for more sparse graphs.

Remark

Problem: this adjacency list can be wasteful when our graph has a lot of edges.


Graph Applications

Remark

We won’t have time to examine this section during the present semester, but will see it come back in Algorithms!

Graphs are popularly used to represent geographical data and distances between points (physical or otherwise, in the cases of Networking applications).

For this reason, one of the most ubiquitous graph algorithms helps us compute efficient distances between nodes in a graph.

Definition

Given a graph with vertexes = geographical locations, and edges connecting adjacent locations with some distances between them, Dijkstra’s algorithm tells us the least distance we would have to travel from a given vertex S to any other vertex in the graph.

Example

The following example graph lists locations A through E that are separated by distances indicated along the edges.

Question

What is the shorted distance from A to E? What path does it take?

Example

Goal: We’d like to have a way of automatically computing the shortest distance from a node to every other node in the network. What are some strategies you could consider to accomplish this?

Main gist: we want to calculate the minimum distance to get from the start to every vertex in the network in a modified breadth-first fashion so that we always know the shortest distance to vertex S so that we can then compute the shortest distance from the start to any neighbor of S!

Dijkstra’s Algorithm

Dijkstra’s (one of the most ubiquitous algorithms and, in my opinion, most fun to say) algorithm does precisely the above.

Dijkstra’s algorithm operates by maintaining several data structures:

  • dist: for each node, tracks the optimal distance from the start node. Each element in this vector / map starts out with value infinity, except for the start node, which starts out with value 0.
  • done: for each node, tracks whether or not we have already computed the optimal travel distance from it to its neighbors. Implemented as an initially empty Set.
  • prev: for each node, tracks the node before this one along the shortest path from the start node.
function Dijkstra(graph, start):
    initialize dist, done, prev (as stated above)
    while done not all true: // Still paths to explore
        u = min distance, not done node from start // track via priority queue
        add u to done
        for each node v in undone neighbors(u)
            d = distance(u, v) // distance between u and v from graph edge
            oldBest = dist[v]
            candidate = d + dist[u]
            if candidate < oldBest
                dist[v] = candidate // Update best distance
                prev[v] = u // Update best path
    return (dist, prev)

Let’s trace this algorithm through our example now (sorry, forgot to illustrate the prev list below, left as an exercise!).

Question

With the completed dist, prev structures returned from above, how do we find the best path from the source to any destination?

There are many variants of Dijkstra’s algorithm, some suited for different purposes than a general shortest-distance computer, but the gist is the same of each: a procedural way of finding shortest distances between nodes in a graph.

And that’s Djiksta’s algorithm!

There are variants of the complexity based on the particular implementation, but generally, most vertexes are well connected, and so we have, for |E| = number of edges in the graph and |V| = number of vertexes, O(|E| + |V|^2), which usually can be proven to be O(|V|^2).