Graph Classifications and Representations

A graph \(G=(V,E)\) is made of nodes (\(V\), or “vertices”) and edges (\(E\)). Before we discuss graph algorithms such as shortest-path, we will first need to understand different types of graphs and how to represent them in a computer.

Classifications

There are many ways (i.e., dimensions or angles) to classify graphs into different categories, and here we discuss the five most important dimensions:

undirected graphs:
  connected:   1 -- 2 -- 3
  disconnected with two connected components: 1--2  3

directed graphs:
  weakly connected:     1 -> 2 -> 3
  strongly connected:   1 -> 2 -> 3
                         ^       /
                          \-----/
                     
  weakly connected with 4 strongly connected components (1-2-3, 4-5, 6, 7):
                   1  --> 2 --> 4 <--> 5 
                    ^    /      |
                     \  /       v
                      3<        6 <--- 7
A weakly connected graph with 3 strongly connected components

For any directed graph, after you shrink each strongly connected component (SCC) into one node, you get a DAG, called SCC-DAG: SCC-DAG

Representations

Adjacency Matrix

We use a \(|V|\times |V|\) matrix \(A\) to represent the graph \(G=(V,E)\), where each \(A_{i,j} = 1\) if \((i, j)\in E\) (for unweighted graph) or each \(A_{i,j} = c(i,j)\) (for weighted graphs). Note that

Adjacency List

As discussed above, any (natural) large graph must be sparse, such as road/flight networks and social networks. So by default, graphs should be represented as an adjacency list, which works for both sparse and dense graphs. For example:

 0 --> 2 --> 3 --> 5 --> 6
    ^    \   |  ^    \
   /      \  v /      \
 1         > 4         > 7

adjacency list:   |  adjacency set:
0 -> [2]          |  0 -> {2}
1 -> [2]          |  1 -> {2}
2 -> [3, 4]       |  2 -> {3, 4}
3 -> [4, 5]       |  3 -> {4, 5} 
4 -> [5]          |  4 -> {5}
5 -> [6, 7]       |  5 -> {6, 7}
6 -> []           |  6 -> {}
7 -> []           |  7 -> {}

Adjacency Set/Map

Note that adjacency list is very handy for iterating over neighbors of a particular node (e.g., if you are at node 3, the adjacency list will let you loop over its connections [4, 5]). However, if you want to query whether a random edge \((i,j)\) is in the graph (which is trivial in an adjacency matrix), you would need to convert each (Python) list like [4,5] into a Python set (implemented as identity hashmap) like {4, 5} instead. This way you can both loop over the connections and do random access on edges. To summarize, the best way to implement the adjacency list is “adjacency set” (see above), which has the advantages of both adjacency list and adjacency matrix.

What about weighted graphs? For adjacency list, for each node \(u\), we use a list of pairs \((v, w)\) to represent edges like \(u \overset{w}{\rightarrow} v\); for adjacency set, for each node \(u\), we use a hashmap, i.e., Python dict instead of Python set, to map \(v\) to \(w\) (if \(u \overset{w}{\rightarrow} v \in E\)). In fact, this becomes a two-stage hashmap (like a two-dimensional array, but suitable for sparse graphs), e.g., \(h[u][v] = w\). For example:

   5      6
A ---> B ---> D
  \3      4/ 
   --> C -/
   
adjacency list      | adjacency map
A -> [(B,5), (C,3)] | A: {B:5, C:3}
B -> [(D,6)]        | B: {D:6}
C -> [(D,4)]        | C: {D:4}
D -> []             | D: {}

In practice, if you don’t need random edge access, you can use adjacency list, otherwise you should use adjacency set (for unweighted graph) or adjacency map (for weighted graph). All of these are very handy in Python.

Here are code snippets for converting the list of edges (input format) to adjacency list/set/map (internal representation).

    adjlist = defaultdict(list) # adjacency list for unweighted
    for u, v in edges: # u->v
        adjlist[u].append(v)

    adjset = defaultdict(set)   # adjacency set for unweighted
    for u, v in edges: # u->v
        adjset[u].add(v)
        

    adjlist = defaultdict(list) # adjacency list for weighted
    for u, v, w in edges: # u->v with cost w
        adjlist[u].append((v, w))

    adjmap = defaultdict(dict)  # adjacency map for weighted
    for u, v, w in edges: # u->v with cost w
        adjmap[u][v] = w