k-th Shortest Path
Problem Description
Given a directed graph with \(n\) vertices and \(m\) edges, find the length of the \(k\)-th shortest path among all distinct paths from \(s\) to \(t\).
「Path」
The term "path" in this article allows traversing the same edge or vertex multiple times, so strictly speaking it should be called a "walk" rather than a "path". The problem discussed in this article is, strictly speaking, the \(k\) shortest walks problem. However, following convention, this article still uses the term "path", while paths that do not intersect themselves are called "simple paths".
A* Algorithm
The A algorithm is a search algorithm. It assigns an evaluation function \(f(x)=g(x)+h(x)\) to each current state \(x\), where \(g(x)\) is the actual cost from the initial state to the current state, and \(h(x)\) is the estimated cost of the best path from the current state to the goal state. During the search, the state \(x\) with the optimal \(f(x)\) is extracted each time, and all its successor states are expanded. A priority queue* can be used to maintain this value.
When solving the \(k\) shortest paths problem, let \(h(x)\) be the length of the shortest path from the current vertex to the destination \(t\). This value for each vertex can be preprocessed by running a single-source shortest path algorithm from vertex \(t\) on the reversed graph. For each state, two values need to be recorded: the current vertex \(x\) and the distance already traveled \(g(x)\). Denote this state as \((x,g(x))\). Initially, add the initial state \((s,0)\) to the priority queue. Each time, extract the state with the smallest evaluation function \(f(x)=g(x)+h(x)\), enumerate all outgoing edges from vertex \(x\) of this state, and add the corresponding successor states to the priority queue. When a vertex is visited for the \(k\)-th time, the \(g(x)\) of the corresponding state is the length of the \(k\)-th shortest path from the starting vertex \(s\) to that vertex.
This search process can be optimized. Since only the \(k\)-th shortest path from the initial vertex to the target vertex is required, when a state reaches a vertex more than \(k\) times, its successor states need not be expanded. This state will not affect the final answer. This is because the previous \(k\) times this vertex was extracted, \(k\) valid paths to this vertex have already been formed, which are sufficient to construct the first \(k\) shortest paths to the target vertex.
If a priority queue is used to optimize Dijkstra's algorithm, since each edge is added to the priority queue at most \(k\) times, the time complexity of the algorithm is \(O(km\log km)\), and the space complexity is \(O(km)\). Compared to a direct search, the A* algorithm prunes with respect to the target vertex \(t\), but this only improves the constant factor, not the asymptotic complexity. Although the algorithm described in this section does not have an excellent complexity, it can find the first \(k\) shortest paths from the starting point \(s\) to each vertex (in the shortest path tree rooted at \(t\)) within the same complexity.
Implementation
Reference implementation for template problem Library Checker - K-Shortest Walk
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 | |
Persistent Mergeable Heap Approach
The aforementioned algorithm actually finds the \(k\) shortest paths to all vertices. If only the \(k\) shortest paths to a given target vertex \(t\) are desired, it can actually be done faster. This section presents an \(O(m\log m+k\log k)\) approach based on persistent mergeable heaps.
Shortest Path Tree and Sidetrack Edges
The bottleneck of the previous algorithm is that the answer is only updated when the target vertex \(t\) is reached. However, different paths may not differ much. For example, the second shortest path may differ from the shortest path only by detouring around one extra vertex at a certain edge, while the rest of the path is the same; the previous algorithm may need to repeatedly search through these same edges to find the second shortest path. Since only the detour part is critical, to obtain the first \(k\) shortest paths, it suffices to consider only the \(k\) cheapest detour methods. This leads to the concept of the shortest path tree.
Run a single-source shortest path algorithm from the target vertex \(t\) on the reversed graph, record the shortest path length \(h(x)\) from each vertex \(x\) to \(t\), and record the first edge \(f_x\) taken on the shortest path from vertex \(x\); if there are multiple optimal choices, any one can be selected. All these edges \(f_x\) and their endpoints form a tree, and the simple path from any vertex \(x\) on the tree to the root \(t\) is a shortest path from \(x\) to \(t\). This is the shortest path tree \(T\).
After obtaining the shortest path tree \(T\), the additional cost incurred by each edge not in \(T\) can be calculated. For an edge \(e=(u,v)\notin T\) with weight \(w\), a new edge can be defined, still from \(u\) to \(v\), with cost \(\Delta(e)=w + h(v) - h(u)\). This article figuratively calls these edges with weight \(\Delta(e)\) sidetrack edges, and the weight \(\Delta(e)\) is called the sidetrack cost. If an edge's endpoints are not all in the shortest path tree \(T\), it does not affect the computation of the \(k\) shortest paths to vertex \(t\) and can be directly removed.
The left side of the figure below is the directed graph \(G\), and the right side is its corresponding shortest path tree \(T\) (thick edges) and the corresponding sidetrack edges (thin edges):
Let the set of edges traversed by a path from \(s\) to \(t\) be \(P\), and remove the intersection of \(P\) with \(T\) to obtain \(P'\). Then, arranging the edges in \(P'\) in order, any two consecutive edges \(e_1=(u_1,v_1)\) and \(e_2=(u_2,v_2)\) must satisfy
- Condition \((*)\): The starting point \(u_2\) of the latter is an ancestor (including itself) of the ending point \(v_1\) of the former in the shortest path tree \(T\).
This is because in the corresponding original path \(P\), several tree edges from \(T\) connect \(v_1\) and \(u_2\). Conversely, for an edge set \(P'\) satisfying condition \((*)\), there exists a unique path \(P\) in graph \(G\) corresponding to it. This is because the simple path from \(v_1\) to \(u_2\) on the shortest path tree \(T\) is unique. This shows that any path \(P\) in the original graph corresponds one-to-one with a sidetrack edge sequence \(P'\) satisfying condition \((*)\). Moreover, the length of path \(P\) equals the sum of the shortest path length \(h(s)\) and these sidetrack costs:
This discussion indicates that the task of finding the \(k\) shortest paths transforms into the task of finding the \(k\)-th smallest cost sidetrack edge sequence \(P'\) satisfying condition \((*)\).
To handle condition \((*)\), instead of searching for ancestors in the shortest path tree each time during queries, it is better to directly propagate each vertex's sidetrack edge set down to its descendants in the shortest path tree. This is equivalent to constructing the following graph \(G'\):
In this graph, condition \((*)\) transforms into requiring that the edges in \(P'\) are connected end to end, meaning \(P'\) is a path in graph \(G'\). The problem further transforms into finding the \(k\)-th smallest length path to any vertex starting from \(s\) in this graph. Compared to the original \(k\) shortest paths problem, it is no longer required that the path must end at the target vertex \(t\).
The transformed problem is easy to solve. Simply start from the starting vertex \(s\) and compute single-source shortest paths. Each time a vertex is extracted from the priority queue, it corresponds to finding a path in graph \(G'\), which in turn corresponds to a path to the target vertex \(t\) in graph \(G\).
Persistent Mergeable Heap Optimization
The algorithmic idea is now clear. However, a naive implementation of this algorithm is too inefficient. In graph \(G'\), the size of edges at a single vertex could be \(\Theta(m)\), so each time single-source shortest paths are computed, an edge set of size \(\Theta(m)\) may need to be pushed into the priority queue. Actually, it is unnecessary to push all edges into the priority queue: in many cases, among these edges pushed into the queue, only the shortest ones might be popped later. That is, the entire edge set at a single vertex can be treated as a single unit pushed into the priority queue, and only the shortest edge in the set needs to be quickly accessed each time.
This inspires us to use a min-heap to store the edge set at a single vertex. In the priority queue for single-source shortest paths, only these heaps need to be stored, and their cost is the shortest path cost corresponding to the heap's top element. Each time the front of the queue is popped, the top edge must also be popped from the heap at the front. Then, both the heap after popping its top must be pushed back into the priority queue, and the heap top corresponding to the sidetrack edge set at the endpoint of the popped top edge must be pushed into the priority queue.
Using heaps to store edge sets also solves the problem of propagating edge sets down the shortest path tree. Because propagating edge sets down is equivalent to merging the current vertex's edge set into its child vertices, the heap must also support merge operations; meanwhile, merging into child vertices must not destroy the edge set at the current vertex, so the heap must also support persistence. This is exactly a persistent mergeable heap.
Thus, the complete algorithm process is obtained:
- Starting from the target vertex \(t\), run single-source shortest paths to obtain the shortest path tree.
- For each vertex in the shortest path tree, construct its corresponding sidetrack edge set and store it in a persistent mergeable heap.
- Along the edges of the shortest path tree, starting from the target vertex \(t\), merge each vertex's heap into its child vertices' heaps.
- Starting from the starting vertex \(s\), push its heap into the priority queue.
- Pop the heap at the front of the queue, record the answer, then push the heap after popping its top back into the priority queue, and push the heap at the endpoint of the popped top edge into the priority queue.
Persistent mergeable heaps are usually implemented using leftist heaps or randomized heaps. In this case, the last step can be further optimized. The internal structure of these heaps is a binary tree. After popping the heap top, originally the two child nodes would be merged, and the merged heap top would be pushed into the priority queue; however, in this algorithm, the merge operation can be skipped, and the two child nodes' corresponding heaps can be directly pushed into the priority queue separately. This saves the \(O(\log m)\) complexity of a single merge. Since each time the front heap is popped, at most three new heaps are pushed into the priority queue, the size of the priority queue is \(O(k)\). Thus, the time complexity of a single query is reduced to \(O(\log k)\). The total query complexity becomes \(O(k\log k)\).
Since constructing the shortest path tree and constructing the persistent mergeable heaps both have complexity \(O(m\log m)\), the total time complexity of the algorithm is \(O(m\log m+k\log k)\).
Implementation
Reference implementation for template problem Library Checker - K-Shortest Walk
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 | |
Exercises
References and Notes
本页面最近更新:,更新历史
发现错误?想一起完善? 在 GitHub 上编辑此页!
本页面贡献者:OI-wiki
本页面的全部内容在 CC BY-SA 4.0 和 SATA 协议之条款下提供,附加条款亦可能应用