Graphs

A graph is a pair \((V, E)\) where \(V\) is the set of vertices (nodes) and \(E \subseteq V \times V\) is the set of edges (i.e. pairs \((u, v)\) where \(u, v \in V\)).

_images/graphs1.png

Note

The image above is a directed graph (digraph), where the direction of the nodes matter. In an undirected graph, the direction doesn’t matter, so \(\forall u,v \in V, (u, v) \in E \iff (v, u) \in E\).

For a vertex \(v \in V\), its outdegree is the number of edges going out from it, and its indegree is the number of edges going into it.

In an undirected graph, this is simplified to degree: the number of edges connected to an edge.

A path in a graph \(G = (V, E)\) is a sequence of vertices \(v_1, v_2,... ,v_k \in V\) where for all \(i < k, (v_i, v_{i+1}) \in E\) (i.e. there is an edge from each vertex in the path to the next).

_images/graphs2.png

A path is simple if it contains no repeated vertices.

A cycle is a nontrivial path starting and ending at the same vertex. A graph is cyclic if it contains a cycle, or acyclic if not.

Note

For a digraph, nontrivial means a path of at least 2 nodes. For an undirected graph, it’s at least 3 (cannot use the same edge to go back and forth)

Ex. This digraph is acyclic: \((x, y, z, w, x)\) is not a path, since there is no path from w to x.

_images/graphs3.png

Some more examples:

_images/graphs4.png

A graph is connected if \(\forall u, v \in V\), there is a path from u to v or vice versa.

A digraph is strongly connected if \(\forall u, v \in V\), there is a path from u to v. (For undirected graphs, connected = strongly connected.)

Note

For a directed graph mith more than one vertex, being strongly connected implies that the graph is cyclic, since a path from u to v and vice versa exists.

_images/graphs5.png

Algorithms

S-T Connectivity Problem

Given vertices \(s, t \in V\), is there a path from s to t? (Later: what’s the shortest such path?)

_images/graphs6.png

BFS

https://imgs.xkcd.com/comics/depth_and_breadth_2x.png

Traverse graph starting from a given vertex, processing vertices in the order they can be reached. Only visit each vertex once, keeping track of the set of already visited vertices. Often implemented using a FIFO queue.

The traversal can be visualized as a tree, where we visit nodes in top-down, left-right order.

_images/graphs7.png
BFS(v):
    Initialize a FIFO queue with a single item v
    Initialize a set of seen vertices with a single item v
    while the queue is not empty:
        u = pop the next element in the queue
        Visit vertex u
        for each edge (u, w):
            if w is not already seen:
                add it to the queue
                add it to set of seen vertices

Note

When we first add a vertex to the queue, we mark it as seen, so it can’t be added to the queue again. Since there are only finitely-many vertices, BFS will eventually terminate.

Each edge gets checked at most once (in each direction), so if there are n vertices and m edges, the runtime is \(\Theta(n+m)\) (assuming you can get the edges out from a vertices in constant time).

DFS

Traverse like BFS, except recursively visit all descendants of a vertex before moving on to the next one (at the same depth). This can be implemented without a queue, only using the call stack.

_images/graphs8.png
DFS(v):
    mark v as visited
    for each edge (v, u):
        if u is not visited, DFS(u)

Note

As with DFS, we visit each vertex at most once, and process each edge at most once in each direction, so runtime is \(\Theta(n+m)\).

We call \(\Theta(n+m)\) linear time for graphs with n vertices and m edges.

Representing Graphs

  • Adjacency lists: for each vertex, have a list of outgoing edges
    • Allows for linear iteration over all edges from a vertex

    • Checking if a particular edge exists is expensive

    • Size for n vertices, m edges = \(\Theta(n + m)\)

  • Adjancency matrix: a matrix with entry \((i, j)\) indicating an edge from i to j
    • Constant time to check if a particular edge exists

    • but the size based on number of vertices is \(\Theta(n^2)\)

_images/graphs9.png

A graph with n vertices has \(\leq {n \choose 2}\) edges if undirected, or \(\leq n^2\) if directed and we allow self-loops (or \(\leq n(n-1)\) if no self loops).

In all cases, \(m=O(n^2)\). This makes the size of an adjacency list \(O(n^2)\).

If we really have a ton of edges, the two representations are the same; but if you have a sparse graph (\(m << n^2\)), then the adjacency list is more memory-efficient.

This means that a \(\Theta(n^2)\) algorithm can run in linear time (\(\Theta(n+m)\)) on dense graphs because \(m = \Theta(n^2)\).

So \(\Theta(n^2)\) is optimal for dense graphs, but not for sparse graphs.

More Problems

Cyclic Test

testing if a graph is cyclic

  • use DFS to traverse, keep track of vertices visited along the way

  • if we find an edge back to any such vertex, we have a cycle

Topological Order

  • Finding a topological ordering of a DAG (directed acyclic graph)
    • an ordering \(v_1, v_2, v_n\) of the vertices such that if \((v_i, v_j) \in E\), then \(i < j\)

    • we can find topological order using postorder traversal in DFS
      • after DFS has visited all descendants of a vertex, prepend the vertex to the topological order

    • make sure to iterate all connected components of the graph, not just one

_images/graphs10.png
def Topo-Sort(G):
    for v in V:
        if v is not visited:
            Visit(v)

def Visit(v):
    mark v as visited
    mark v as in progress // temporarily
    for edges (v, u) in E:
        if u is in progress, G is cyclic // there is no topological order
        if u is not visited:
            Visit(u)
    mark v as not in progress
    prepend v to the topological order
_images/graphs11.png

Minimum Spanning Tree

Suppose you need to wire all houses in a town together (represented as a graph, and the edges represent where power lines can be built), Build a power line to use the minimal amount of wire.

_images/graphs12.png

Given a weighted graph, (i.e. each edge has a numerical weight), we want to find a minimum spanning tree: a selection of edges of G that connects every vertex, is a tree (any cycles are unnecessary), and has least total weight.

Kruskal’s Algorithm

Main idea: add edges to the tree 1 at a time, in order from least weight to largest weight, skipping edges that would create a cycle

_images/graphs13.png

This is an example of a greedy algorithm, since it picks the best available option at each step.

We use a disjoint-set forest to keep track of sets of connected components.

_images/graphs14.png

Initially, all vertices are in their own sets. When we add an edge, we Union the sets of its two vertices together. If we would add an edge that would connect two vertices in the same set, we skip it (it would cause a cycle).

1
2
3
4
5
6
7
8
def Kruskal(G, weights w):
    initialize a disjoint-set forest on V
    sort E in order of increasing w(e)
    while all vertices are not connected:  // i.e. until n-1 edges are added
        take the next edge from E, (u, v)
        if Find(u) != Find(v):
            add (u, v) to the tree
            Union(u, v)

Runtime:

  • L2: \(\Theta(n)\)

  • L3: \(\Theta(m \log m)\)

  • L4: \(m\) iterations of the loop worst case
    • Note that this lets us use the amortized runtime of Find and Union

    • L6: two finds (\(\Theta(\alpha(n))\) each)

    • L8: and a union (\(\Theta(\alpha(n))\))

  • Note that \(m \alpha(n) < m \log m\) since \(n \leq m\)

So the total runtime is \(\Theta(m \log m) = \Theta(m \log n)\) (since \(m = \Omega(n)\) since G is connected).

This assumes G is represented by an adjacency list, so that we can construct the list E in \(\Theta(m)\) time.

Prim’s Algorithm

Main idea: build the tree one vertex at a time, always picking the cheapest vertex

_images/graphs15.png

Need to maintain a set of vertices S which have already been connected to the root; also need to maintain the cost of all vertices which could be added next (the “frontier”).

Use a priority queue to store these costs: the key of vertex \(v \notin S\) will be \(\min w((u, v) \in E), u \in S\)

_images/graphs16.png

After selecting a vertex v of least cost and adding it to S, some vertices may now be reachable via cheaper edges, so we need to decrease their keys in the queue.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
def Prim(G, weights w, root r):
    initialize a PQ Q with items V  // all keys are infinity except r = 0
    initialize an array T[v] to None for all vertices v in V
    while Q is not empty:
        v = Delete-Min(Q)  // assume delete returns the deleted elem
        for each edge (v, u):
            if u is in Q and w(v, u) < Key(u):
                Decrease-Key(Q, u, w(v, u))
                T[u] = (v, u)  // keep track of the edge used to reach u
    return T
_images/graphs17.png

Runtime:

  • n iterations of the main loop (1 per vertex), with 1 Delete-Min per iteration

  • every edge considered at most once in each direction, so at most m Decrease-Key

  • we use a Fibonacci heap, (Delete-Min = \(\Theta(\log n)\), Decrease-Key = \(\Theta(1)\) amortized)

  • so the total runtime is \(\Theta(m + n \log n)\).

Note

We can compare the runtime of these two algorithms on sparse and dense graphs:

On a sparse graph, i.e. \(m = \Theta(n)\), the two have the same runtime: \(\Theta(n \log n)\).

On a dense graph, i.e. \(m = \Theta(n^2)\), Kruskal’s ends up with \(\Theta(n^2 \log n)\) and Prim’s with \(\Theta(n^2)\).

Proofs

Need to prove that the greedy choices (picking the cheapest edge at each step) leads to an overall optimal tree.

Key technique: loop invariants: A property P that:

  1. is true at the beginning of the loop

  2. is preserved by one iteration of the loop (i.e. if P is true at the start of the loop, it is true at the end)

By induction on the number of iterations, a loop invariant will be true when the loop terminates. For our MST algorithms, our invariant will be: The set of edges added to the tree so far is a subset of some MST.

If we can show that this is an invariant, then since the tree we finally return connects all vertices, it must be a MST.

Lemma (cut property): Let \(G=(V,E)\) be a connected undirected graph where all edges have distinct weights. For any \(S\subseteq V\), if \(e \in E\) is the cheapest edge from \(S\) to \(V-S\), every MST of \(G\) contains \(e\).

_images/graphs18.png

Kruskal’s

Thm: Kruskal’s alg is correct.

Pf: We’ll establish the loop invariant above.

Base case: before adding any edges, the edges added so far is \(\emptyset\), which is a subset of some MST.

Inductive case: suppose the edges added so far are contained in an MST \(T\), and we’re about to add the edge \((u, v)\).

  • Let \(S\) be all vertices already connected to \(u\).

  • Since Kruskal’s doesn’t add edges between already connected vertices, \(v \notin S\)

  • Then \((u, v)\) must be the cheapest edge from \(S\) to \(V-S\), since otherwise a cheaper edge would have been added earlier in the algorithm, and then \(v \in S\)

  • Then by the cut property, every MST of \(G\) contains \((u, v)\)

  • In particular, \(T\) contains \((u, v)\), so the set of edges added up to and including \((u, v)\) is also contained in \(T\), which is a MST

  • So the loop invariant is actually an invariant, and by induction it holds at the end of the algorithm

So all edges added are in an MST, and since they are a spanning tree, they are a MST. Since the algorithm doesn’t terminate until we have a spanning tree, the returned tree must be a MST.

Prim’s

Idea: Let \(S\) be the set of all vertices connected so far. Prim’s then adds the cheapest edge from \(S\) to \(V - S\).

So by the cut property, the newly added edge is part of every MST. The proof follows as above.

Cut Property

Proof of the following lemma, restated from above:

Let \(G=(V,E)\) be a connected undirected graph where all edges have distinct weights. For any \(S\subseteq V\), if \(e \in E\) is the cheapest edge from \(S\) to \(V-S\), every MST of \(G\) contains \(e\).

_images/graphs19.png

Suppose there was some MST \(T\) which doesn’t contain \(e=(u, v)\). Since \(T\) is a spanning tree, it contains a path \(P = (u, ..., v)\). Since \(u \in S\) and \(v \notin S\), \(P\) contains some edge \(e' = (u', v')\) with \(u' \in S, v' \notin S\). We claim that exchanging \(e\) for \(e'\) will give a spanning tree \(T'\) of lower weight than \(T\), which contradicts \(T\) being an MST.

First, \(T'\) has smaller weight than \(T\), since \(e\) is cheaper than \(e'\) by definition.

Next, \(T'\) connects all vertices, since \(T\) does, and any path that went through \(e'\) can be rerouted to use \(e\) instead: follow \(P\) backwards from \(u'\) to \(u\), take \(e\) from:math:u to \(v\), then follow \(P\) backwards from \(v\) to \(v'\), and then continue the old path normally. (e.g. \((a, ..., u', v', ..., b) \to (a, ..., u', ..., u, v, ..., v', ..., b)\))

Finally, \(T'\) is also acyclic, since adding \(e\) to \(T\) produces a single cycle, which is then broken by removing \(e'\).

So \(T'\) is a spanning tree of lower weight than \(T\) - contradiction! Therefore every MST of G must contain the edge e.

Note

To relax the assumption of distinct edge weights, imagine adding tiny extra weights to break any ties in the MST algorithm; if the changes in the weights are sufficiently small, then any MST of the new graph is also an MST of the original (see hw4).

Single-Source Shortest Paths

Given a directed graph with nonnegative edge weights (lengths) and a start vertex \(s\), find the shortest paths from \(s\) to every other vertex (shortest = minimum total length/weight). (Assume every vertex is reachable from s.)

To represent the shortest paths, we use a shortest-path tree: each vertex stores the edge that should be used to reach it from s along the shortest path.

_images/graphs20.png

Ex. For this graph, starting from vertex z, the shortest-path tree T is:

_images/graphs22.png
T[x] = (z, x)
T[y] = (w, y)
T[w] = (z, w)

Dijkstra’s Algorithm

Greedy algorithm for building the shortest-path tree. Very similar to Prim’s, but the cost of a vertex is the length of the shortest known path to reach it from s

Idea: Maintain a set of vertices S already added to the shortest-path tree; for these vertices, we know their distance d(v) from s. At each step, add a vertex \(v \notin S\) which minimizes \(d(u) + w(u, v)\) over all \(u \in S\) with \((u, v) \in E\)

_images/graphs21.png
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
def Dijkstra(G, weights w, source s in V):
    Initialize a PQ Q with items V; all keys d(v) = inf except d(s) = 0
    Initialize an array T[v] to None for all v in V
    while Q is not empty:
        u = Delete-Min(Q)
        for each edge (u, v) in E:
            if v in Q and d(v) > d(u) + w(u, v):
                Decrease-Key(Q, v, d(u) + w(u, v))
                T[v] = u
    return T

Runtime: Same as Prim’s algorithm - \(\Theta(m + n \log n)\) using a Fibonacci heap

Proof

To prove correctness, we’ll use the following loop invariant I:

for all vertices v popped off the queue so far:

  • \(d(v)\) is the distance from s to v and

  • there is a path \(P_v\) from s to v in T which is a shortest path
    • \(P_v=(v, T[v], T[T[v]], ..., s)\) reversed

To show I is an invariant, need to prove it is true initially and it preserved by one iteration. If S is the set of popped-off vertices, then initially \(S = \emptyset\) so I holds vacuously.

Suppose I holds at the beginning of an iteration. If we pop vertex v, and \(T[v]=(u, v)\), then \(u \in S\), so \(P_u\) is a shortest path from s to u and \(d(u)\) is the distance from s to u. Also \(d(v) = d(u) + w(u, v)\) by line 8, which is the length of the path \(P_v\).

Now suppose there were a shorter path P from s to v. Since \(v \notin S\), P must leave S at some point: let the first edge leaving S be \((x, y)\). Since \(x \in S\), we previously considered the edge \((x, y)\) and therefore \(d(y) \leq d(x) + w(x, y)\). Also, since \(y \notin S\), the algorithm selected v to pop before y, so \(d(v) \leq d(y)\). Then \(d(v) \leq d(x) + w(x, y)\) but the length of P is \(d(x) + w(x, y)\), and \(P_v\) has length \(d(v)\), so P cannot be shorter than \(P_v\). This is a contradiction, so \(P_v\) is in fact a shortest path from s to v.

_images/graphs23.png

Then \(d(v)\) is the distance from s to v, so I holds after the end of the loop. Therefore I is an invariant, and by induction it holds when the algorithm terminates. Since all vertices are popped off when the algorithm terminates, T must then be a shortest path’s tree (and \(d(v)\) is the distance from s to v for all v)