Parallel single-source shortest path algorithm

From HandWiki
Revision as of 18:08, 6 February 2024 by Scavis2 (talk | contribs) (update)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)

A central problem in algorithmic graph theory is the shortest path problem. One of the generalizations of the shortest path problem is known as the single-source-shortest-paths (SSSP) problem, which consists of finding the shortest paths from a source vertex [math]\displaystyle{ s }[/math] to all other vertices in the graph. There are classical sequential algorithms which solve this problem, such as Dijkstra's algorithm. In this article, however, we present two parallel algorithms solving this problem. Another variation of the problem is the all-pairs-shortest-paths (APSP) problem, which also has parallel approaches: Parallel all-pairs shortest path algorithm.

Problem definition

Let [math]\displaystyle{ G=(V,E) }[/math] be a directed graph with [math]\displaystyle{ |V|=n }[/math] nodes and [math]\displaystyle{ |E|=m }[/math] edges. Let [math]\displaystyle{ s }[/math] be a distinguished vertex (called "source") and [math]\displaystyle{ c }[/math] be a function assigning a non-negative real-valued weight to each edge. The goal of the single-source-shortest-paths problem is to compute, for every vertex [math]\displaystyle{ v }[/math] reachable from [math]\displaystyle{ s }[/math], the weight of a minimum-weight path from [math]\displaystyle{ s }[/math] to [math]\displaystyle{ v }[/math], denoted by [math]\displaystyle{ \operatorname{dist}(s,v) }[/math] and abbreviated [math]\displaystyle{ \operatorname{dist}(v) }[/math]. The weight of a path is the sum of the weights of its edges. We set [math]\displaystyle{ \operatorname{dist}(u,v):=\infty }[/math] if [math]\displaystyle{ v }[/math] is unreachable from [math]\displaystyle{ u }[/math].[1]

Sequential shortest path algorithms commonly apply iterative labeling methods based on maintaining a tentative distance for all nodes; [math]\displaystyle{ \operatorname{tent}(v) }[/math] is always [math]\displaystyle{ \infty }[/math] or the weight of some path from [math]\displaystyle{ s }[/math] to [math]\displaystyle{ v }[/math] and hence an upper bound on [math]\displaystyle{ \operatorname{dist}(v) }[/math]. Tentative distances are improved by performing edge relaxations, i.e., for an edge [math]\displaystyle{ (v,w)\in E }[/math] the algorithm sets [math]\displaystyle{ \operatorname{tent}(w):=\min\{\operatorname{tent}(w), \operatorname{tent}(v)+c(v,w)\} }[/math].[1]

For all parallel algorithms we will assume a PRAM model with concurrent reads and concurrent writes.

Delta stepping algorithm

The delta stepping algorithm is a label-correcting algorithm, which means the tentative distance of a vertex can be corrected several times via edge relaxations until the last step of the algorithm, when all tentative distances are fixed.

The algorithm maintains eligible nodes with tentative distances in an array of buckets each of which represents a distance range of size [math]\displaystyle{ \Delta }[/math]. During each phase, the algorithm removes all nodes of the first nonempty bucket and relaxes all outgoing edges of weight at most [math]\displaystyle{ \Delta }[/math]. Edges of a higher weight are only relaxed after their respective starting nodes are surely settled.[1] The parameter [math]\displaystyle{ \Delta }[/math] is a positive real number that is also called the "step width" or "bucket width".[1]

Parallelism is obtained by concurrently removing all nodes of the first nonempty bucket and relaxing their outgoing light edges in a single phase. If a node [math]\displaystyle{ v }[/math] has been removed from the current bucket [math]\displaystyle{ B[i] }[/math] with non-final distance value then, in some subsequent phase, [math]\displaystyle{ v }[/math] will eventually be reinserted into [math]\displaystyle{ B[i] }[/math] , and the outgoing light edges of [math]\displaystyle{ v }[/math] will be re-relaxed. The remaining heavy edges emanating from all nodes that have been removed from [math]\displaystyle{ B[i] }[/math] so far are relaxed once and for all when [math]\displaystyle{ B[i] }[/math] finally remains empty. Subsequently, the algorithm searches for the next nonempty bucket and proceeds as described above.[1]

The maximum shortest path weight for the source node [math]\displaystyle{ s }[/math] is defined as [math]\displaystyle{ L(s):=\max\{\operatorname{dist}(s,v) : \operatorname{dist}(s,v)\lt \infty\} }[/math], abbreviated [math]\displaystyle{ L }[/math].[1] Also, the size of a path is defined to be the number of edges on the path.

We distinguish light edges from heavy edges, where light edges have weight at most [math]\displaystyle{ \Delta }[/math] and heavy edges have weight bigger than [math]\displaystyle{ \Delta }[/math] .

Following is the delta stepping algorithm in pseudocode:

1  foreach [math]\displaystyle{ v \in V }[/math] do [math]\displaystyle{ \operatorname{tent}(v):=\infty }[/math]
2  [math]\displaystyle{ relax(s, 0) }[/math];                                                    (*Insert source node with distance 0*)
3  while [math]\displaystyle{ \lnot isEmpty(B) }[/math] do                           (*A phase: Some queued nodes left (a)*)
4      [math]\displaystyle{ i:=\min\{j\geq 0: B[j]\neq \emptyset\} }[/math]                      (*Smallest nonempty bucket (b)*)
5      [math]\displaystyle{ R:=\emptyset }[/math]                                                (*No nodes deleted for bucket B[i] yet*)
6      while [math]\displaystyle{ B[i]\neq \emptyset }[/math] do                     (*New phase (c)*)
7          [math]\displaystyle{ Req:=findRequests(B[i],light) }[/math]                           (*Create requests for light edges (d)*)
8          [math]\displaystyle{ R:=R\cup B[i] }[/math]                                           (*Remember deleted nodes (e)*)
9          [math]\displaystyle{ B[i]:=\emptyset }[/math]                                         (*Current bucket empty*)
10         [math]\displaystyle{ relaxRequests(Req) }[/math]                                      (*Do relaxations, nodes may (re)enter B[i] (f)*)
11     [math]\displaystyle{ Req:=findRequests(R,heavy) }[/math]                                  (*Create requests for heavy edges (g)*)
12     [math]\displaystyle{ relaxRequests(Req) }[/math]                                          (*Relaxations will not refill B[i] (h)*)
13
14 function [math]\displaystyle{ findRequests(V',kind:\{\text{light}, \text{heavy}\}) }[/math]:set of Request
15     return [math]\displaystyle{ \{(w, \operatorname{tent}(v)+c(v,w)): v\in V' \land (v,w) \in E_\text{kind}\} }[/math]
16
17 procedure [math]\displaystyle{ relaxRequests(Req) }[/math]
18     foreach [math]\displaystyle{ (w, x)\in Req }[/math] do [math]\displaystyle{ relax(w,x) }[/math]
19
20 procedure [math]\displaystyle{ relax(w,x) }[/math]                                             (*Insert or move w in B if [math]\displaystyle{ x\lt \operatorname{tent}(w) }[/math]*)
21     if [math]\displaystyle{ x \lt  \operatorname{tent}(w) }[/math] then
22         [math]\displaystyle{ B[\lfloor \operatorname{tent}(w)/\Delta\rfloor]:=B[\lfloor \operatorname{tent}(w)/\Delta\rfloor]\setminus \{w\}       }[/math]                      (*If in, remove from old bucket*)
23         [math]\displaystyle{ B[\lfloor x/\Delta\rfloor]:=B[\lfloor x/\Delta\rfloor]\cup \{w\}       }[/math]                                 (*Insert into new bucket*)
24         [math]\displaystyle{ \operatorname{tent}(w):=x       }[/math]

Example

Example graph

Following is a step by step description of the algorithm execution for a small example graph. The source vertex is the vertex A and [math]\displaystyle{ \Delta }[/math] is equal to 3.

At the beginning of the algorithm, all vertices except for the source vertex A have infinite tentative distances.

Bucket [math]\displaystyle{ B[0] }[/math] has range [math]\displaystyle{ [0,2] }[/math], bucket [math]\displaystyle{ B[1] }[/math] has range [math]\displaystyle{ [3,5] }[/math] and bucket [math]\displaystyle{ B[2] }[/math] has range [math]\displaystyle{ [6,8] }[/math].

The bucket [math]\displaystyle{ B[0] }[/math] contains the vertex A. All other buckets are empty.

Node A B C D E F G
Tentative distance 0 [math]\displaystyle{ \infty }[/math] [math]\displaystyle{ \infty }[/math] [math]\displaystyle{ \infty }[/math] [math]\displaystyle{ \infty }[/math] [math]\displaystyle{ \infty }[/math] [math]\displaystyle{ \infty }[/math]

The algorithm relaxes all light edges incident to [math]\displaystyle{ B[0] }[/math], which are the edges connecting A to B, G and E.

The vertices B,G and E are inserted into bucket [math]\displaystyle{ B[1] }[/math]. Since [math]\displaystyle{ B[0] }[/math] is still empty, the heavy edge connecting A to D is also relaxed.

Node A B C D E F G
Tentative distance 0 3 [math]\displaystyle{ \infty }[/math] 5 3 [math]\displaystyle{ \infty }[/math] 3

Now the light edges incident to [math]\displaystyle{ B[1] }[/math] are relaxed. The vertex C is inserted into bucket [math]\displaystyle{ B[2] }[/math]. Since now [math]\displaystyle{ B[1] }[/math] is empty, the heavy edge connecting E to F can be relaxed.

Node A B C D E F G
Tentative distance 0 3 6 5 3 8 3

On the next step, the bucket [math]\displaystyle{ B[2] }[/math] is examined, but doesn't lead to any modifications to the tentative distances.

The algorithm terminates.

Runtime

As mentioned earlier, [math]\displaystyle{ L }[/math] is the maximum shortest path weight.

Let us call a path with total weight at most [math]\displaystyle{ \Delta }[/math] and without edge repetitions a [math]\displaystyle{ \Delta }[/math]-path.

Let [math]\displaystyle{ C_\Delta }[/math] denote the set of all node pairs [math]\displaystyle{ \langle u,v \rangle }[/math] connected by some [math]\displaystyle{ \Delta }[/math]-path [math]\displaystyle{ (u,\dots, v) }[/math] and let [math]\displaystyle{ n_\Delta:=|C_{\Delta}| }[/math]. Similarly, define [math]\displaystyle{ C_{\Delta+} }[/math] as the set of triples [math]\displaystyle{ \langle u,v',v \rangle }[/math] such that [math]\displaystyle{ \langle u,v' \rangle \in C_\Delta }[/math] and [math]\displaystyle{ (v',v) }[/math] is a light edge and let [math]\displaystyle{ m_\Delta:=|C_{\Delta+}| }[/math].

The sequential delta-stepping algorithm needs at most[math]\displaystyle{ \mathcal{O} (n+m + n_\Delta + m_\Delta + L/\Delta) }[/math] operations. A simple parallelization runs in time [math]\displaystyle{ \mathcal{O} \left(\frac{L}{\Delta}\cdot d\ell_\Delta \log n\right) }[/math] .[1]

If we take [math]\displaystyle{ \Delta = \Theta(1/d) }[/math] for graphs with maximum degree [math]\displaystyle{ d }[/math] and random edge weights uniformly distributed in [math]\displaystyle{ [0,1] }[/math], the sequential version of the algorithm needs [math]\displaystyle{ \mathcal{O}(n+m+dL) }[/math] total average-case time and a simple parallelization takes on average [math]\displaystyle{ \mathcal{O}(d^2 L \log^2n) }[/math].[1]

Graph 500

The third computational kernel of the Graph 500 benchmark runs a single-source shortest path computation.[2] The reference implementation of the Graph 500 benchmark uses the delta stepping algorithm for this computation.

Radius stepping algorithm

For the radius stepping algorithm, we must assume that our graph [math]\displaystyle{ G }[/math] is undirected.

The input to the algorithm is a weighted, undirected graph, a source vertex, and a target radius value for every vertex, given as a function [math]\displaystyle{ r : V \rightarrow \mathbb{R}_+ }[/math].[3] The algorithm visits vertices in increasing distance from the source [math]\displaystyle{ s }[/math]. On each step [math]\displaystyle{ i }[/math], the Radius-Stepping increments the radius centered at [math]\displaystyle{ s }[/math] from [math]\displaystyle{ d_{i-1} }[/math] to [math]\displaystyle{ d_i }[/math] , and settles all vertices [math]\displaystyle{ v }[/math] in the annulus [math]\displaystyle{ d_{i-1}\lt d(s,v)\leq d_i }[/math].[3]

Following is the radius stepping algorithm in pseudocode:

    Input: A graph [math]\displaystyle{ G=(V,E,w) }[/math], vertex radii [math]\displaystyle{ r(\cdot) }[/math], and a source node [math]\displaystyle{ s }[/math].
    Output: The graph distances [math]\displaystyle{ \delta(\cdot) }[/math] from [math]\displaystyle{ s }[/math].
 1  [math]\displaystyle{ \delta(\cdot)\leftarrow +\infty }[/math], [math]\displaystyle{ \delta(s)\leftarrow 0 }[/math]
 2  foreach [math]\displaystyle{ v \in N(s) }[/math] do [math]\displaystyle{ \delta(v)\leftarrow w(s,v) }[/math], [math]\displaystyle{ S_0\leftarrow \{s\} }[/math], [math]\displaystyle{ i \leftarrow 1 }[/math]
 3  while [math]\displaystyle{ |S_{i-1}|\lt |V| }[/math] do
 4      [math]\displaystyle{ d_i\leftarrow \min_{v\in V\setminus S_{i-1}}\{\delta(v) + r(v)\} }[/math]
 5      repeat   
 6          foreach [math]\displaystyle{ u \in V\setminus S_{i-1} }[/math] s.t [math]\displaystyle{ \delta(u) \leq d_i }[/math] do
 7              foreach [math]\displaystyle{ v \in N(u)\setminus S_{i-1} }[/math] do
 8                  [math]\displaystyle{ \delta(v) \leftarrow \min\{\delta(v), \delta(u)+w(u,v)\} }[/math]
 9      until no [math]\displaystyle{ \delta(v)\leq d_i }[/math] was updated
 10     [math]\displaystyle{ S_i=\{v\;|\;\delta(v)\leq d_i\} }[/math]
 11     [math]\displaystyle{ i=i+1 }[/math]
 12 return [math]\displaystyle{ \delta(\cdot) }[/math]

For all [math]\displaystyle{ S\subseteq V }[/math], define [math]\displaystyle{ N(S)=\bigcup_{u\in S}\{v\in V\mid d(u,v)\in E\} }[/math] to be the neighbor set of S. During the execution of standard breadth-first search or Dijkstra's algorithm, the frontier is the neighbor set of all visited vertices.[3]

In the Radius-Stepping algorithm, a new round distance [math]\displaystyle{ d_i }[/math] is decided on each round with the goal of bounding the number of substeps. The algorithm takes a radius [math]\displaystyle{ r(v) }[/math] for each vertex and selects a [math]\displaystyle{ d_i }[/math] on step [math]\displaystyle{ i }[/math] by taking the minimum [math]\displaystyle{ \delta(v) + r(v) }[/math] over all [math]\displaystyle{ v }[/math] in the frontier (Line 4).

Lines 5-9 then run the Bellman-Ford substeps until all vertices with radius less than [math]\displaystyle{ d_i }[/math] are settled. Vertices within [math]\displaystyle{ d_i }[/math] are then added to the visited set [math]\displaystyle{ S_i }[/math].[3]

Example

Example graph

Following is a step by step description of the algorithm execution for a small example graph. The source vertex is the vertex A and the radius of every vertex is equal to 1.

At the beginning of the algorithm, all vertices except for the source vertex A have infinite tentative distances, denoted by [math]\displaystyle{ \delta }[/math] in the pseudocode.

All neighbors of A are relaxed and [math]\displaystyle{ S_0=\{A\} }[/math].

Node A B C D E F G
Tentative distance 0 3 [math]\displaystyle{ \infty }[/math] 5 3 [math]\displaystyle{ \infty }[/math] 3

The variable [math]\displaystyle{ d_1 }[/math] is chosen to be equal to 4 and the neighbors of the vertices B, E and G are relaxed. [math]\displaystyle{ S_1=\{A,B,E,G\} }[/math]

Node A B C D E F G
Tentative distance 0 3 6 5 3 8 3

The variable [math]\displaystyle{ d_2 }[/math] is chosen to be equal to 6 and no values are changed. [math]\displaystyle{ S_2=\{A,B,C,D,E,G\} }[/math].

The variable [math]\displaystyle{ d_3 }[/math] is chosen to be equal to 9 and no values are changed. [math]\displaystyle{ S_3=\{A,B,C,D,E,F, G\} }[/math].

The algorithm terminates.

Runtime

After a preprocessing phase, the radius stepping algorithm can solve the SSSP problem in [math]\displaystyle{ \mathcal{O}(m\log n) }[/math] work and [math]\displaystyle{ \mathcal{O}(\frac{n}{p}\log n\log pL) }[/math] depth, for [math]\displaystyle{ p\leq \sqrt{n} }[/math]. In addition, the preprocessing phase takes [math]\displaystyle{ \mathcal{O}(m\log n + np^2) }[/math] work and [math]\displaystyle{ \mathcal{O}(p^2) }[/math] depth, or [math]\displaystyle{ \mathcal{O}(m\log n + np^2\log n) }[/math] work and [math]\displaystyle{ \mathcal{O}(p\log p) }[/math] depth.[3]

References

  1. 1.0 1.1 1.2 1.3 1.4 1.5 1.6 1.7 Meyer, U.; Sanders, P. (2003-10-01). "Δ-stepping: a parallelizable shortest path algorithm". Journal of Algorithms. 1998 European Symposium on Algorithms 49 (1): 114–152. doi:10.1016/S0196-6774(03)00076-2. ISSN 0196-6774. 
  2. "Graph 500". 9 March 2017. https://graph500.org/?page_id=12#sec-7. 
  3. 3.0 3.1 3.2 3.3 3.4 Blelloch, Guy E.; Gu, Yan; Sun, Yihan; Tangwongsan, Kanat (2016). "Parallel Shortest Paths Using Radius Stepping". Proceedings of the 28th ACM Symposium on Parallelism in Algorithms and Architectures. New York, New York, USA: ACM Press. pp. 443–454. doi:10.1145/2935764.2935765. ISBN 978-1-4503-4210-0.