Graph Theory

Preliminaries

What is a graph?

  • A set of vertices $V$, connected by a series of edges $E$
  • Sometimes edges are "one-way", and we call such a graph directed
  • Sometimes edges have an associated weight, and we call those graphs weighted
4 5 5 5 A B C D

Can be used to represent lot's of stuff!

  • A series of cities and roads between them (think google maps)
  • The internet (might want to minimize latency)
  • A series of task dependencies (Makefiles, build scripts, etc.)
  • Maybe friendships/social connections. (LinkedIn "second degree" suggestions)

Implementation

Adjacency List Implementations

  • Set of lists for each vertex
  • Each list contains all vertices connected to the vertex associated with that list
  • Using math: $G = \{ L_A, L_B, ... \}$, where $A, B, ...$ are the nodes of the graph, and $L_A = \{ B, D, E, ... \}$ is a list of vertices that $A$ is connected to.
  • So, the fact that $B$ is in $L_A$ indicates that $A$ is connected to $B$ (but necessary the other way around!)

Python Implementation (using hash tables)


class Graph:
    def __init__(self):
        # Initial graph has nothing connected
        self.adjacency = {}

    def connect(self, vertex1, vertex2, weight=None):
        # If the vertex has never been added to the graph, add it, but with no connects
        if vertex1 not in self.adjacency:
            self.adjacency[vertex1] = {}

        # now connect the edges, by adding a vertex2 key to the vertex1 adjanceny map with value of the weight
        self.adjacency[vertex1][vertex2] = weight
        # if the graph is not directed, you'll also need a self.adjacency[vertex2][vertex1] = weight

    def is_vertex(self, vertex):
        return vertex in self.adjacency

    def are_connected(self, vertex1, vertex2):
        return self.is_vertex(vertex1) and vertex2 in self.adjacency[vertex1]

    def weight(self, vertex1, vertex2):
        return self.adjacency[vertex1][vertex2]

    def neighbors(self, vertex1):
        return self.adjacency[vertex1].keys()

# Test Code
G = Graph()

G.connect('A', 'B')
G.connect('A', 'C')
G.connect('B', 'C')
G.connect('C', 'D')

print(G.are_connected('A', 'B')) # True
print(G.are_connected('B', 'A')) # False, because the graph is directed!
print(G.are_connected('A', 'C')) # False, because there is no direct connection
print(G.neighbors('A')) # ['C', 'B']
                        

Java Implementation

  • I'm lazy
  • It's practically the same thing, but use a
    Hashtable<String, Hashtable<String, String>>
    to store adjacency

Graph-related problems

Is node B reachable from A?

If my graph is a set of task dependencies, what order should I do my tasks

What's the shortest way to get from A to B?

  • Edges have weights, so some edges are more costly to traverse
  • We want to find the path to take that is least costly (think Google Maps directions)
  • Dijkstra's Algorithm is an appropriate algorithm
  • Sometimes, you need all shortest from every node to every other node. This is called "All-Pairs Shortest Path", and you can use Floyd Warshall to solve it.

What's the set edges with the lowest weight so that the graph remains connected

If my edges were pipes with capacity, what's the most water I could pump from vertex to another

Example Problems

Electric Car Rally

  • Can be modelled as a shortest-path problem, so Dijkstra's is an appropriate choice
  • The biggest challege is actually building the graph!

The 8-Puzzle

  • Essentially asks you to solve the 8-puzzle
  • You don't have to actually store the entire graph, but at least be able to enumerate all possible moves that you've taken before
  • To solve this, Breadth-First Search is the most appropriate