Yen's algorithm

From HandWiki

In graph theory, Yen's algorithm computes single-source K-shortest loopless paths for a graph with non-negative edge cost.[1] The algorithm was published by Jin Y. Yen in 1971 and employs any shortest path algorithm to find the best path, then proceeds to find K − 1 deviations of the best path.[2]

Algorithm

Terminology and notation

Notation Description
[math]\displaystyle{ N }[/math] The size of the graph, i.e., the number of nodes in the network.
[math]\displaystyle{ (i) }[/math] The [math]\displaystyle{ i }[/math]-th node of the graph, where [math]\displaystyle{ i }[/math] ranges from [math]\displaystyle{ 1 }[/math] to [math]\displaystyle{ N }[/math]. This means that [math]\displaystyle{ (1) }[/math] is the source node of the graph, and [math]\displaystyle{ (N) }[/math] is the sink node of the graph.
[math]\displaystyle{ d_{ij} }[/math] The cost of the edge between [math]\displaystyle{ (i) }[/math] and [math]\displaystyle{ (j) }[/math], assuming that [math]\displaystyle{ (i) \ne (j) }[/math] and [math]\displaystyle{ d_{ij} \ge 0 }[/math].
[math]\displaystyle{ A^k }[/math] The [math]\displaystyle{ k }[/math]-th shortest path from [math]\displaystyle{ (1) }[/math] to [math]\displaystyle{ (N) }[/math], where [math]\displaystyle{ k }[/math] ranges from [math]\displaystyle{ 1 }[/math] to [math]\displaystyle{ K }[/math]. Then [math]\displaystyle{ A^k = (1) - (2^k) - (3^k) - \cdots - ({Q_k}^k) - (N) }[/math], where [math]\displaystyle{ (2^k) }[/math] is the 2nd node of the [math]\displaystyle{ k }[/math]-th shortest path, [math]\displaystyle{ (3^k) }[/math] is the 3rd node of the [math]\displaystyle{ k }[/math]-th shortest path, and so on.
[math]\displaystyle{ {A^k}_i }[/math] A deviation path from [math]\displaystyle{ A^{k-1} }[/math] at node [math]\displaystyle{ (i) }[/math], where [math]\displaystyle{ i }[/math] ranges from [math]\displaystyle{ 1 }[/math] to [math]\displaystyle{ Q_k }[/math]. Note that the maximum value of [math]\displaystyle{ i }[/math] is [math]\displaystyle{ Q_k }[/math], which is the node just before the sink in the [math]\displaystyle{ k }[/math] shortest path. This means that the deviation path cannot deviate from the [math]\displaystyle{ k - 1 }[/math] shortest path at the sink. The paths [math]\displaystyle{ A^k }[/math] and [math]\displaystyle{ A^{k-1} }[/math] follow the same path until the [math]\displaystyle{ i }[/math]-th node, then [math]\displaystyle{ (i)^k - (i + 1)^k }[/math] edge is different from any path in [math]\displaystyle{ A^j }[/math], where [math]\displaystyle{ j }[/math] ranges from [math]\displaystyle{ 1 }[/math] to [math]\displaystyle{ k - 1 }[/math].
[math]\displaystyle{ {R^k}_i }[/math] The root path of [math]\displaystyle{ {A^k}_i }[/math] that follows that [math]\displaystyle{ A^{k-1} }[/math] until the [math]\displaystyle{ i }[/math]-th node of [math]\displaystyle{ A^{k-1} }[/math].
[math]\displaystyle{ {S^k}_i }[/math] The spur path of [math]\displaystyle{ {A^k}_i }[/math] that starts at the [math]\displaystyle{ i }[/math]-th node of [math]\displaystyle{ {A^k}_i }[/math] and ends at the sink.

Description

The algorithm can be broken down into two parts: determining the first k-shortest path, [math]\displaystyle{ A^1 }[/math], and then determining all other k-shortest paths. It is assumed that the container [math]\displaystyle{ A }[/math] will hold the k-shortest path, whereas the container [math]\displaystyle{ B }[/math] will hold the potential k-shortest paths. To determine [math]\displaystyle{ A^1 }[/math], the shortest path from the source to the sink, any efficient shortest path algorithm can be used.

To find the [math]\displaystyle{ A^k }[/math], where [math]\displaystyle{ k }[/math] ranges from [math]\displaystyle{ 2 }[/math] to [math]\displaystyle{ K }[/math], the algorithm assumes that all paths from [math]\displaystyle{ A^1 }[/math] to [math]\displaystyle{ A^{k-1} }[/math] have previously been found. The [math]\displaystyle{ k }[/math] iteration can be divided into two processes: finding all the deviations [math]\displaystyle{ {A^k}_i }[/math] and choosing a minimum length path to become [math]\displaystyle{ A^k }[/math]. Note that in this iteration, [math]\displaystyle{ i }[/math] ranges from [math]\displaystyle{ 1 }[/math] to [math]\displaystyle{ {Q^k}_k }[/math].

The first process can be further subdivided into three operations: choosing the [math]\displaystyle{ {R^k}_i }[/math], finding [math]\displaystyle{ {S^k}_i }[/math], and then adding [math]\displaystyle{ {A^k}_i }[/math] to the container [math]\displaystyle{ B }[/math]. The root path, [math]\displaystyle{ {R^k}_i }[/math], is chosen by finding the subpath in [math]\displaystyle{ A^{k-1} }[/math] that follows the first [math]\displaystyle{ i }[/math] nodes of [math]\displaystyle{ A^j }[/math], where [math]\displaystyle{ j }[/math] ranges from [math]\displaystyle{ 1 }[/math] to [math]\displaystyle{ k - 1 }[/math]. Then, if a path is found, the cost of edge [math]\displaystyle{ d_{i(i+1)} }[/math] of [math]\displaystyle{ A^j }[/math] is set to infinity. Next, the spur path, [math]\displaystyle{ {S^k}_i }[/math], is found by computing the shortest path from the spur node, node [math]\displaystyle{ i }[/math], to the sink. The removal of previous used edges from [math]\displaystyle{ (i) }[/math] to [math]\displaystyle{ (i + 1) }[/math] ensures that the spur path is different. [math]\displaystyle{ {A^k}_i = {R^k}_i + {S^k}_i }[/math], the addition of the root path and the spur path, is added to [math]\displaystyle{ B }[/math]. Next, the edges that were removed, i.e. had their cost set to infinity, are restored to their initial values.

The second process determines a suitable path for [math]\displaystyle{ A^k }[/math] by finding the path in container [math]\displaystyle{ B }[/math] with the lowest cost. This path is removed from container [math]\displaystyle{ B }[/math] and inserted into container [math]\displaystyle{ A }[/math], and the algorithm continues to the next iteration.

Pseudocode

The algorithm assumes that the Dijkstra algorithm is used to find the shortest path between two nodes, but any shortest path algorithm can be used in its place.

function YenKSP(Graph, source, sink, K):
    // Determine the shortest path from the source to the sink.
    A[0] = Dijkstra(Graph, source, sink);
    // Initialize the set to store the potential kth shortest path.
    B = [];
    
    for k from 1 to K:
        // The spur node ranges from the first node to the next to last node in the previous k-shortest path.
        for i from 0 to size(A[k − 1]) − 2:
            
            // Spur node is retrieved from the previous k-shortest path, k − 1.
            spurNode = A[k-1].node(i);
            // The sequence of nodes from the source to the spur node of the previous k-shortest path.
            rootPath = A[k-1].nodes(0, i);
            
            for each path p in A:
                if rootPath == p.nodes(0, i):
                    // Remove the links that are part of the previous shortest paths which share the same root path.
                    remove p.edge(i,i + 1) from Graph;
            
            for each node rootPathNode in rootPath except spurNode:
                remove rootPathNode from Graph;
            
            // Calculate the spur path from the spur node to the sink.
            // Consider also checking if any spurPath found
            spurPath = Dijkstra(Graph, spurNode, sink);
            
            // Entire path is made up of the root path and spur path.
            totalPath = rootPath + spurPath;
            // Add the potential k-shortest path to the heap.
            if (totalPath not in B):
                B.append(totalPath);
            
            // Add back the edges and nodes that were removed from the graph.
            restore edges to Graph;
            restore nodes in rootPath to Graph;
                    
        if B is empty:
            // This handles the case of there being no spur paths, or no spur paths left.
            // This could happen if the spur paths have already been exhausted (added to A), 
            // or there are no spur paths at all - such as when both the source and sink vertices 
            // lie along a "dead end".
            break;
        // Sort the potential k-shortest paths by cost.
        B.sort();
        // Add the lowest cost path becomes the k-shortest path.
        A[k] = B[0];
        // In fact we should rather use shift since we are removing the first element
        B.pop();
    
    return A;

Example

Yen's k-shortest path algorithm, K = 3, A to F

The example uses Yen's K-Shortest Path Algorithm to compute three paths from [math]\displaystyle{ (C) }[/math] to [math]\displaystyle{ (H) }[/math]. Dijkstra's algorithm is used to calculate the best path from [math]\displaystyle{ (C) }[/math] to [math]\displaystyle{ (H) }[/math], which is [math]\displaystyle{ (C)-(E)-(F)-(H) }[/math] with cost 5. This path is appended to container [math]\displaystyle{ A }[/math] and becomes the first k-shortest path, [math]\displaystyle{ A^1 }[/math].

Node [math]\displaystyle{ (C) }[/math] of [math]\displaystyle{ A^1 }[/math] becomes the spur node with a root path of itself, [math]\displaystyle{ {R^2}_1 = (C) }[/math]. The edge, [math]\displaystyle{ (C)-(E) }[/math], is removed because it coincides with the root path and a path in container [math]\displaystyle{ A }[/math]. Dijkstra's algorithm is used to compute the spur path [math]\displaystyle{ {S^2}_1 }[/math], which is [math]\displaystyle{ (C)-(D)-(F)-(H) }[/math], with a cost of 8. [math]\displaystyle{ {A^2}_1 = {R^2}_1 + {S^2}_1 = (C)-(D)-(F)-(H) }[/math] is added to container [math]\displaystyle{ B }[/math] as a potential k-shortest path.

Node [math]\displaystyle{ (E) }[/math] of [math]\displaystyle{ A^1 }[/math] becomes the spur node with [math]\displaystyle{ {R^2}_2 = (C)-(E) }[/math]. The edge, [math]\displaystyle{ (E)-(F) }[/math], is removed because it coincides with the root path and a path in container [math]\displaystyle{ A }[/math]. Dijkstra's algorithm is used to compute the spur path [math]\displaystyle{ {S^2}_2 }[/math], which is [math]\displaystyle{ (E)-(G)-(H) }[/math], with a cost of 7. [math]\displaystyle{ {A^2}_2 = {R^2}_2 + {S^2}_2 = (C)-(E)-(G)-(H) }[/math] is added to container [math]\displaystyle{ B }[/math] as a potential k-shortest path.

Node [math]\displaystyle{ (F) }[/math] of [math]\displaystyle{ A^1 }[/math] becomes the spur node with a root path, [math]\displaystyle{ {R^2}_3 = (C)-(E)-(F) }[/math]. The edge, [math]\displaystyle{ (F)-(H) }[/math], is removed because it coincides with the root path and a path in container [math]\displaystyle{ A }[/math]. Dijkstra's algorithm is used to compute the spur path [math]\displaystyle{ {S^2}_3 }[/math], which is [math]\displaystyle{ (F)-(G)-(H) }[/math], with a cost of 8. [math]\displaystyle{ {A^2}_3 = {R^2}_3 + {S^2}_3 = (C)-(E)-(F)-(G)-(H) }[/math] is added to container [math]\displaystyle{ B }[/math] as a potential k-shortest path.

Of the three paths in container B, [math]\displaystyle{ {A^2}_2 }[/math] is chosen to become [math]\displaystyle{ A^2 }[/math] because it has the lowest cost of 7. This process is continued to the 3rd k-shortest path. However, within this 3rd iteration, note that some spur paths do not exist. And the path that is chosen to become [math]\displaystyle{ A^3 }[/math] is [math]\displaystyle{ (C)-(D)-(F)-(H) }[/math].

Features

Space complexity

To store the edges of the graph, the shortest path list [math]\displaystyle{ A }[/math], and the potential shortest path list [math]\displaystyle{ B }[/math], [math]\displaystyle{ N^2 + KN }[/math] memory addresses are required.[2] At worse case, the every node in the graph has an edge to every other node in the graph, thus [math]\displaystyle{ N^2 }[/math] addresses are needed. Only [math]\displaystyle{ KN }[/math] addresses are need for both list [math]\displaystyle{ A }[/math] and [math]\displaystyle{ B }[/math] because at most only [math]\displaystyle{ K }[/math] paths will be stored,[2] where it is possible for each path to have [math]\displaystyle{ N }[/math] nodes.

Time complexity

The time complexity of Yen's algorithm is dependent on the shortest path algorithm used in the computation of the spur paths, so the Dijkstra algorithm is assumed. Dijkstra's algorithm has a worse case time complexity of [math]\displaystyle{ O(N^2) }[/math], but using a Fibonacci heap it becomes [math]\displaystyle{ O(M + N\log N) }[/math],[3] where [math]\displaystyle{ M }[/math] is the number of edges in the graph. Since Yen's algorithm makes [math]\displaystyle{ Kl }[/math] calls to the Dijkstra in computing the spur paths, where [math]\displaystyle{ l }[/math] is the length of spur paths. In a condensed graph, the expected value of [math]\displaystyle{ l }[/math] is [math]\displaystyle{ O(\log N) }[/math], while the worst case is [math]\displaystyle{ N }[/math]. The time complexity becomes [math]\displaystyle{ O(KN(M + N\log N)) }[/math].[4]

Improvements

Yen's algorithm can be improved by using a heap to store [math]\displaystyle{ B }[/math], the set of potential k-shortest paths. Using a heap instead of a list will improve the performance of the algorithm, but not the complexity.[5] One method to slightly decrease complexity is to skip the nodes where there are non-existent spur paths. This case is produced when all the spur paths from a spur node have been used in the previous [math]\displaystyle{ A^k }[/math]. Also, if container [math]\displaystyle{ B }[/math] has [math]\displaystyle{ K-k }[/math] paths of minimum length, in reference to those in container [math]\displaystyle{ A }[/math], then they can be extract and inserted into container [math]\displaystyle{ A }[/math] since no shorter paths will be found.

Lawler's modification

Eugene Lawler proposed a modification to Yen's algorithm in which duplicates path are not calculated as opposed to the original algorithm where they are calculated and then discarded when they are found to be duplicates.[6] These duplicates paths result from calculating spur paths of nodes in the root of [math]\displaystyle{ A^k }[/math]. For instance, [math]\displaystyle{ A^{k} }[/math] deviates from [math]\displaystyle{ A^{k-1} }[/math] at some node [math]\displaystyle{ (i) }[/math]. Any spur path, [math]\displaystyle{ {S^k}_j }[/math] where [math]\displaystyle{ j = 0,\ldots,i }[/math], that is calculated will be a duplicate because they have already been calculated during the [math]\displaystyle{ k-1 }[/math] iteration. Therefore, only spur paths for nodes that were on the spur path of [math]\displaystyle{ A^{k-1} }[/math] must be calculated, i.e. only [math]\displaystyle{ {S^k}_h }[/math] where [math]\displaystyle{ h }[/math] ranges from [math]\displaystyle{ (i+1)^{k-1} }[/math] to [math]\displaystyle{ (Q_k)^{k-1} }[/math]. To perform this operation for [math]\displaystyle{ A^{k} }[/math], a record is needed to identify the node where [math]\displaystyle{ A^{k-1} }[/math] branched from [math]\displaystyle{ A^{k-2} }[/math].

See also

References

  1. Yen, Jin Y. (1970). "An algorithm for finding shortest routes from all source nodes to a given destination in general networks". Quarterly of Applied Mathematics 27 (4): 526–530. doi:10.1090/qam/253822. 
  2. 2.0 2.1 2.2 Yen, Jin Y. (Jul 1971). "Finding the k Shortest Loopless Paths in a Network". Management Science 17 (11): 712–716. doi:10.1287/mnsc.17.11.712. 
  3. Fredman, Michael Lawrence; Tarjan, Robert E. (1984). "Fibonacci heaps and their uses in improved network optimization algorithms". 25th Annual Symposium on Foundations of Computer Science. IEEE. pp. 338–346. doi:10.1109/SFCS.1984.715934. 
  4. Bouillet, Eric (2007). Path routing in mesh optical networks. Chichester, England: John Wiley & Sons. ISBN 9780470032985. 
  5. Brander, Andrew William; Sinclair, Mark C.. A comparative study of k-shortest path algorithms.. Department of Electronic Systems Engineering, University of Essex, 1995. 
  6. Lawler, EL (1972). "A procedure for computing the k best solutions to discrete optimization problems and its application to the shortest path problem.". Management Science 18 (7): 401–405. doi:10.1287/mnsc.18.7.401. 

External links