Link

Graph Implementations Reading

Table of contents

  1. Graph APIs
  2. Graph data structures
    1. Adjacency matrix
    2. Adjacency list

So far, we’ve been introduced to one particular type of graph: simple, undirected, and unweighted.

What problems arise when using BFS for navigation directions in HuskyMaps if vertices are intersections and edges are roads?

Breadth-first search solves s–t shortest paths in an unweighted graph. This is an appropriate model only if the length of each edge (modeling either distance or travel time) is roughly the same. In reality, roads come in many different varieties: some are longer than others, some allow for higher or lower speed limits, some get congested during certains times of the day, and some roads are one-way only.

In this lesson, we’ll explore some fundamental graph design choices that allow us to better model a wider variety of problems.

Weighted graphs
Edges are weighted (assigned a number or cost).
Directed graphs (digraphs)
Edges are directed (one-way).

Graph APIs

Application programming interfaces (APIs) define the contract between clients and implementers. In Java programs, APIs define available method signatures and their expected behaviors for all implementing classes.

Graphs represent a class of abstract data types. The exact API for a graph depends on the particular problem and the solution devised by the implementer. Nonetheless, there are several common design patterns employed by graph implementers. Consider the following (simplified) graph API for HuskyMaps. StreetMapGraph is a weighted graph with Long vertices and WeightedEdge<Long> edges.

class StreetMapGraph {
    // Each vertex refers to a specific, real-world Location.
    private Map<Long, Location> nodes = new HashMap<>();
    // Each vertex has weighted edges to its neighbors.
    private Map<Long, Set<WeightedEdge<Long>>> neighbors = new HashMap<>();

    // Initialize the graph from a real dataset.
    StreetMapGraph(String filename) { ... }

    // Nearest neighbor search and autocomplete.
    long closest(Location target) { ... }
    List<String> getLocationsByPrefix(String prefix) { ... }
    List<Location> getLocations(String locationName) { ... }

    // Getter methods for graph algorithms.
    List<WeightedEdge<Long>> neighbors(long v) { ... }
    Set<Long> vertices() { ... }
    Location location(long id) { ... }
}

class WeightedEdge<Vertex> {
    private Vertex v
    private Vertex w;
    private double weight;
    ...
}

Note that this graph API does not contain any graph algorithms. Instead, graph algorithms are implemented in separate classes and query the graph through the public methods. As a consequence, the methods we provide can have a significant impact on our graph algorithm implementations. For example, we might implement breadth-first search on a StreetMapGraph as follows.

class BreadthFirstPaths {
    private Set<Long> marked = new HashSet<>();
    private Map<Long, Long> edgeTo = new HashMap<>();
    private Map<Long, Double> distTo = new HashMap<>();

    // Run BFS on the StreetMapGraph g from vertex s.
    BreadthFirstPaths(StreetMapGraph g, long s) {
        Queue<Long> fringe = new LinkedList<>();
        fringe.add(s);
        marked.add(s);
        edgeTo.put(s, s);
        distTo.put(s, 0);

        while (!fringe.isEmpty()) {
            long v = fringe.remove();
            for (WeightedEdge<Long> edge : g.neighbors(v)) {
                assert v == edge.from();
                long w = edge.to();
                if (!marked.contains(w)) {
                    marked.add(w);
                    edgeTo.put(w, v);
                    distTo.put(w, distTo.get(v) + 1);
                    fringe.add(w);
                }
            }
        }
    }

    // Returns true if there is a path to t.
    boolean hasPathTo(long t) {
        return marked.contains(t);
    }

    // Get the s-t shortest path.
    List<Long> pathTo(long t) { ... }

    // Get the number of edges on the s-t shortest path.
    double distTo(long t) {
        return distTo.getOrDefault(t, Double.POSITIVE_INFINITY);
    }
}

This idea of separating data structure StreetMapGraph from the processing algorithm BreadthFirstPaths is an example of a typical graph solver design pattern. After the HuskyMaps server starts up and creates the global StreetMapGraph instance, we can use BreadthFirstPaths to answer a query.

  1. Create a new BreadthFirstPaths instance and pass the StreetMapGraph to the constructor.
  2. The constructor for BreadthFirstPaths runs BFS from the given vertex and stores the shortest paths in its internal data structures.
  3. Query BreadthFirstPaths using hasPathTo, pathTo, and distTo for navigation directions.

Graph data structures

Just as there are several data structures for sets, maps, and priority queues, so too are there different data structures for graphs. To evaluate the runtime of these data structures, consider the following method that prints out all of the edges in a graph with int IDs.

void printEdges(Graph g) {
    for (int v : g.vertices()) {
        for (int w : g.neighbors(v)) {
            System.out.println(v + "-" + w);
        }
    }
}

Adjacency matrix

An adjacency matrix maintains a matrix indexed by vertex ID, i.e. boolean[][]. There is an edge between v and w if and only if the corresponding cell contains true. In undirected graphs, the adjacency matrix is symmetric along its diagonal.

If there are V vertices in a simple graph, up to how many edges can be in the graph?

Up to V2 - V edges since all the values along the main diagonal must be false.

Edges vs. Vertices in Simple Graphs

Adjacency list

An adjacency list maintains an array of lists indexed by vertex ID. Adjacency lists are the most popular approach for representing graphs because most problems are sparse.

Sparse graph
E grows slowly, i.e. proportional to V.
Dense graph
E grows quickly, i.e. proportional to V2.
Give a tight asymptotic runtime bound for BreadthFirstPaths in terms of V and E assuming an adjacency list representation.

O(V + E). In a directed graph, each vertex v is visited at most once and each edge is considered at most once.

Graph traversal algorithms are affected by the time it takes to evaluate g.neighbors(v). In an adjacency list, only the incident edges (edges to true neighbors) need to be considered. In an adjacency matrix on the other hand, it’s necessary to iterate over all V - 1 vertices since we don’t know which edges are incident to a given vertex.

The runtime for DFS follows from our analysis of BFS.

Graph ProblemAlgorithmAdj. matrixAdj. list
s–t pathsDepth-first searchO(V2)O(V + E)
s–t shortest pathsBreadth-first searchO(V2)O(V + E)
Does StreetMapGraph use adjacency matrix or adjacency list representation?

StreetMapGraph uses an adjacency list representation. Each Long vertex is mapped to a list of neighbors, implemented as a Set<WeightedEdge<Long>>.