External memory graph traversal

External memory graph traversal is a type of graph traversal optimized for accessing externally stored memory.

Background
Graph traversal is a subroutine in most graph algorithms. The goal of a graph traversal algorithm is to visit (and / or process) every node of a graph. Graph traversal algorithms, like breadth-first search and depth-first search, are analyzed using the von Neumann model, which assumes uniform memory access cost. This view neglects the fact, that for huge instances part of the graph resides on disk rather than internal memory. Since accessing the disk is magnitudes slower than accessing internal memory, the need for efficient traversal of external memory exists.

External memory model
For external memory algorithms the external memory model by Aggarwal and Vitter is used for analysis. A machine is specified by three parameters: M, B and D. M is the size of the internal memory, B is the block size of a disk and D is the number of parallel disks. The measure of performance for an external memory algorithm is the number of I/Os it performs.

External memory breadth-first search
The breadth-first search algorithm starts at a root node and traverses every node with depth one. If there are no more unvisited nodes at the current depth, nodes at a higher depth are traversed. Eventually, every node of the graph has been visited.

Munagala and Ranade
For an undirected graph $$G$$, Munagala and Ranade proposed the following external memory algorithm:

Let $$L(t)$$ denote the nodes in breadth-first search level t and let $$A(t):=N(L(t-1))$$ be the multi-set of neighbors of level t-1. For every t, $$L(t)$$ can be constructed from $$A(t)$$ by transforming it into a set and excluding previously visited nodes from it.


 * 1) Create $$A(t)$$ by accessing the adjacency list of every vertex in $$L(t-1)$$. This step requires $$O(|L(t-1)|+|A(t)|/(D\cdot B))$$ I/Os.
 * 2) Next $$A'(t)$$ is created from $$A(t)$$ by removing duplicates. This can be achieved via sorting of $$A(t)$$, followed by a scan and compaction phase needing $$O(\operatorname{sort}(|A|))$$ I/Os.
 * 3) $$L(t):=A'(t)\backslash\{L(t-1)\cup L(t-2)\}$$ is calculated by a parallel scan over $$L(t-1)$$ and $$L(t-2)$$ and requires $$O((|A(t)|+|L(t-1)|+|L(t-2)|)/(D\cdot B))$$ I/Os.

The overall number of I/Os of this algorithm follows with consideration that $$\sum_t |A(t)|=O(m)$$ and $$\sum_t |L(t)|=O(n)$$ and is $$O(n+\operatorname{sort}(n+m))$$.

A visualization of the three described steps necessary to compute L(t) is depicted in the figure on the right.

Mehlhorn and Meyer
Mehlhorn and Meyer proposed an algorithm that is based on the algorithm of Munagala and Ranade (MR) and improves their result.

It consists of two phases. In the first phase the graph is preprocessed, the second phase performs a breadth-first search using the information gathered in phase one.

During the preprocessing phase the graph is partitioned into disjointed subgraphs $$S_i,\,0\leq i\leq K$$ with small diameter. It further partitions the adjacency lists accordingly, by constructing an external file $$F=F_0F_1\dots F_{K-1}$$, where $$F_i$$ contains the adjacency list for all nodes in $$S_i$$.

The breadth-first search phase is similar to the MR algorithm. In addition the algorithm maintains a sorted external file $H$. This file is initialized with $$F_0$$. Further, the nodes of any created breadth-first search level carry identifiers for the files $$F_i$$ of their respective subgraphs $$S_i$$. Instead of using random accesses to construct $$L(t)$$ the file $H$ is used.


 * 1) Perform a parallel scan of sorted list $$L(t-1)$$ and $H$. Extract the adjacency lists for nodes $$v\in L(t-1)$$, that can be found in $H$.
 * 2) The adjacency lists for the remaining nodes that could not be found in $H$ need to be fetched. A scan over $$L(t-1)$$ yields the partition identifiers. After sorting and deletion of duplicates, the respective files $$F_i$$ can be concatenated into a temporary file $F'$.
 * 3) The missing adjacency lists can be extracted from $F'$ with a scan. Next, the remaining adjacency lists are merged into $H$ with a single pass.
 * 4) $$A(t)$$ is created by a simple scan. The partition information is attached to each node in $$A(t)$$.
 * 5) The algorithm proceeds like the MR algorithm.

Edges might be scanned more often in $H$, but unstructured I/Os in order to fetch adjacency lists are reduced.

The overall number of I/Os for this algorithm is $$O\left(\sqrt\frac{n\cdot(n+m)}{D\cdot B}+\operatorname{sort}(n+m)\right)$$

External memory depth-first search
The depth-first search algorithm explores a graph along each branch as deep as possible, before backtracing.

For directed graphs Buchsbaum, Goldwasser, Venkatasubramanian and Westbrook proposed an algorithm with $$O((V+E/B)\log_2 (V/B)+\operatorname{sort}(E))$$ I/Os.

This algorithm is based on a data structure called buffered repository tree (BRT). It stores a multi-set of items from an ordered universe. Items are identified by key. A BTR offers two operations:
 * , which adds item x to T and needs $$O(1/B\log_2 (N/B))$$ amortized I/Os. N is the number of items added to the BTR.
 * , which reports and deletes from T all items with key k. It requires $$O(\log_2 (N/B)+S/B)$$ I/Os, where S is the size of the set returned by extract.

The algorithm simulates an internal depth-first search algorithm. A stack S of nodes is hold. During an iteration for the node v on top of S push an unvisited neighbor onto S and iterate. If there are no unvisited neighbors pop v.

The difficulty is to determine whether a node is unvisited without doing $$\Omega(1)$$ I/Os per edge. To do this for a node v incoming edges $(x,v)$ are put into a BRT D, when v is first discovered. Further, outgoing edges (v,x) are put into a priority queue P(v), keyed by the rank in the adjacency list.

For vertex u on top of S all edges (u,x) are extracted from D. Such edges only exist if x has been discovered since the last time u was on top of S (or since the start of the algorithm if u is the first time on top of S). For every edge (u,x) a delete(x) operation is performed on P(u). Finally a delete-min operation on $P(u)$ yields the next unvisited node. If P(u) is empty, u is popped from S.

Pseudocode for this algorithm is given below.

1 procedure BGVW-depth-first-search(G, v): 2     let S be a stack, P[] a priority queue for each node and D a BRT 3     S.push(v) 4     while S is not empty: 5         v = S.top 6         if v is not marked: 7             mark(v) 8         extract all edges (v, x) from D, &forall;x: P[v].delete(x) 9         if (u = P[v].delete-min) is not null: 10            S.push(u) 11        else: 12            S.pop 13 procedure mark(v) 14     put all edges (x, v) into D 15     &forall; (v, x): put x into P[v]