The Shortest Path Problem: Given a map, what's the shortest path from s to t.
Consider a graph G=(V,E) and a weight function w that associates a real-valued weight w(u,v) to each edge (u,v). Given s and t in V, what's the min weight path from s to t?
The graph can be directed.
Negative edge weightallowed.
Negative Cyclenot allowed.
Single-Source Shortest Path(单源最短路径)
Single-Source Shortest Path (SSSP)
Given a graph G=(V,E) and a weight function w, given a source node s, find a shortest path from s to every node u∈V.
Consider directed graphs without negative cycle.
Cases:
Unit weight.
Arbitrary positive weight.
Arbitrary weight without cycle.
Arbitrary weight.
SSSP in unit weight graphs
Use BFS.
SSSP in positive weight graphs
Extension of unit SSSP algorithm: Add dummy nodes on edges so that all edges have unit weight. Then run BFS on the resulting graph.
However this is too slow when edge weights are large.
To save time, bypass events that process dummy nodes:
Imagine we have an alarm clock Tu for each node u.
Alarm for source node s goes off at time 0.
If Tu goes off, for each (u,v), update Tv=min{Tv,Tu+w(u,v)}.
At any time, value of Tu is an estimate of dist(s,u).
At any time, Tu⩾dist(s,u) with equality holds when Tu goes off.
The B alarm is assumed to be 80 at the beginning.
Use priority queue (such as binary heap) to implement the alarm clock.
1 2 3 4 5 6 7 8 9 10 11 12 13
DijkstraSSSP(G, s): for each u in V // O(n) u.dist := INF q.parent := NIL // Shortest-path tree similar to BFS tree s.dist := 0 Build prioirty queue Q based on dist // O(n) while !Q.empty() u := Q.ExtractMin() // O(n log n) for each edge(u, v) in E // O(m log n) if v.dist > u.dist + w(u, v) v.dist := u.dist + w(u, v) v.parent := u Q.UpdateKey(v)
Efficiency of Dijkstra's algorithm: O((n+m)logn) when using a binary heap.
O(n2logn).
Here's an alternative derivation of Dijkstra's algorithm.
What's BFS doing? Expand outward from s, growing the region to which distances and shortest paths are known. The growth should be orderly(closest nodes first).
Given known region R, how to identify the node to expand to?
Assume v is such node to expand to (i.e., the next closet node to s), let the shortest path from s to v is s→v.
It must have dist(s,v)⩾dist(s,v′) for any v′∈R, otherwise already v∈R.
Let the last node of path s→v before v to be u, then it must be u∈R. Otherwise, u is closer to s than v.
That means, find u′∈R,v′∈V−Rmin{dist(s,u′)+w(u′,v′)}. This is an optimal substructure property.
Any satisfied node v is the next node to expand to.
1 2 3 4 5 6 7 8 9 10 11
DijkstraSSSPAbs(G, s): for each u in V u.dist := INF s.dist := 0 R := {} while R != V Find node in V - R with minimum v.dist R := R + {v} for each edge(v, z) in E if z.dist > v.dist + w(v, z) z.dist := v.dist + w(v, z)
Dijkstra's algorithm process:
Universal Optimality of Dijkstra's algorithm
Dijkstra's algorithm is proved to be universal optimal beyond worst case in 2024.
Given the problem space:
Let G denotes a graph of n nodes and m edges
Let Gn,m denotes the set of all such G
Let WG denotes all possible weight functions for a given G.
Let A denotes all correct SSSP algorithms (when edge weights are possible).
Algorithm A∈A is existentially optimal (the usual definition) if
Dijkstra's algorithm no longer works when negative edge weights are allowed.
Let the last node of path s→v before v to be u, then it must be u∈R. Otherwise, u is closer to s than v.
Shortest path from s to any node v must pass through nodes that are closer than v.
This no longer holds.
But how dist values are maintained in Dijkstra is helpful.
This way two properties are maintained:
For any v, at any time, v.dist is either an overestimate or correct.
Assume u is the last node on a shortest path from s to v. If u.dist is correct and we run Update(u, v), then v.dist becomes correct.
This means, Update(u, v) is safe and helpful:
Safe: Regardless of the sequence of Update operations we execute, for any node v, value v.dist is either an overestimate or correct.
Helpful: With correct sequence of Update, we get correct v.dist.
Consider a shortest path from s to v, here are 2 observations:
If Update(s, u1), …, Update(uk-1, uk), Update(uk, v) are executed, then we correctly obtain the shortest path.
In above sequence, before and after each Update, we can add arbitrary Update sequence, and still get shortest path from s to v.
Algorithm: Simply Update all edges for k+1 times:
How large is k+1? Observation: Any shortest path cannot contain a cycle, or you can remove the cycle to get a shorter path (there's no negative cycle).
As a result, simply update all edges for n−1 times, and we get Bellman-Ford Algorithm:
1 2 3 4 5 6 7 8 9 10
BellmanFordSSSP(G, s): for each u in V u.dist := INF u.parent := NIL s.dist := 0 repeat n - 1 times for each edge(u, v) in E if v.dist > u.dist + w(u, v) v.dist := u.dist + w(u, v) v.parent := u
Complexity is Θ(n(m+n)).
O(n3).
Bellman-Ford algorithm process:
What if the graph contains a negative cycle? It means that after n−1 repetitions of Update, some node v still has v.dist > u.dist + w(u, v). So Bellman-Ford can also detect negative cycle:
1 2 3 4 5 6 7 8 9 10 11 12 13
BellmanFordSSSP(G, s): for each u in V u.dist := INF u.parent := NIL s.dist := 0 repeat n - 1 times for each edge(u, v) in E if v.dist > u.dist + w(u, v) v.dist := u.dist + w(u, v) v.parent := u for each edge(u, v) in E if v.dist > u.dist + w(u, v) return"Negative cycle detected"
SSSP in DAG with negative weights
This is Case 3.
Bellman-Ford's algorithm still works, but we can be more sufficient.
Core idea of Bellman-Ford: perform a sequence of Update that includes every shortest path as a subsequence.
Observation: in DAG, every path, thus every shortest path, is a subsequence in the topological order.
1 2 3 4 5 6 7 8 9 10 11
DAGSSSP(G, s): for each u in V u.dist := INF u.parent := NIL s.dist := 0 Run DFS to obtain topological order for each node u in topological order for each edge(u, v) in E if v.dist > u.dist + w(u, v) v.dist := u.dist + w(u, v) v.parent := u
Complexity is O(n+m).
O(n2).
DAG SSSP process:
Application of SSSP in DAG: Computing Critical Path.
Assume you want to finish a task that involves multiple steps. Each step takes some time. For some step(s), it can only begin after certain steps are done.
These dependency can be modeled as a DAG. (PERT Chart)
How fast can you finish this task?
Equivalently, longest path, a.k.a., critical path(关键路径), in the DAG? Negate edge weights and compute a shortest path.
Given a graph G=(V,E), how to find a (shortest) path from a source s to a destination t, preferably efficiently?
You may say, "Just run Dijkstra's algorithm from s and stop when t is extracted."
However we don't care about the shortest distance of other points, while Dijkstra's algorithm has labeled all nodes (via propagation).
We could be much better using Greedy Best-First Search:
1 2 3 4 5 6 7 8 9 10
GreedyBFS(G, s, t): s.est_to_goal := heuristic(s, t) Build priority queue Q based on est_to_goal while !Q.empty() u := Q.ExtractMin() for each edge(u, v) in E if v not in Q v.est_to_goal := heuristic(v, t) v.parent := u Q.Add(v)
A (not necessarily accurate) estimate on the distance from v to t.
On 2D grid, we can set heuristic(v, t) as the Manhattan distance between v and t, i.e., ∣v.x−t.x∣+∣v.y−t.y∣.
However greedy BFS not always generate correct answers:
Compared with Dijkstra's algorithm:
Combine the advantages of Dijkstra's algorithm and Greedy BFS, we get A* algorithm:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
AStarPathfinding(G, s, t): for each node u in V u.est_to_s := INF u.est_to_t := heuristic(u, t) u.metric := u.est_to_s + u.est_to_t s.est_to_s := 0 s.metric := s.est_to_s + s.est_to_t Build priority queue Q based on metric while !Q.empty() u := Q.ExtractMin() for each edge(u, v) in E if v not in Q or v.est_to_s > u.est_to_s + dist(u, v) v.est_to_s := u.est_to_s + dist(u, v) v.metric := v.est_to_s + v.est_to_t v.parent := u Q.Add(v)
For each node u:
u.est_to_s maintains an (over or accurate) estimate of dist(u, s), and this value changes during execution;
u.est_to_t maintains an (under or accurate) estimate of dist(u, t), and this value does not change during execution.
Use u.est_to_s + u.est_to_t as the metric to guide the search
Usually set to the straight-line distance between u and t.
A* algorithm process:
It's correct as long as u.est_to_t <= dist(u, t) always holds.
Time complexity of the A* algorithm is complicated as a node may be added to the queue multiple times.
In AI community, it's normally considered to be O(bd) where b is the branching factor (the average number of successors per state), and d is the depth of the solution (the shortest path).
The heuristic function has a major effiect on the practical performance of A* search, since a good heuristic allows A* to prune away many of the bd nodes.
All-Pairs Shortest Path(全源最短路径)
All-Pairs Shortest Path (APSP)
Given a graph G=(V,E) and a weight function w, for every pair (u,v)∈V×V, find a shortest path from u to v.
Straightforward solution for ASPS: for each v∈V, execute SSSP algorithm once. Complexity is O(n2⋅T) where T is the complexity of SSSP algorithm.
Or alternatively, for every path from u to v, w^ changes it by the same amount:
Let the w^(u,v)=h(u)+w(u,v)−h(v)
Imagine h(u) is an entry gift and h(v) is an exit tax for traveling through (u,v).
Since we need w^(u,v)=h(u)+w(u,v)−h(v)⩾0 for Dijkstra's algorithm, we can set h(u)=dist(z,u) for some fixed z∈V. Then w^(u,v)=dist(u)+w(u,v)−dist(v) cos the shortest path from z to v must be smaller than the shortest path from z to u plus the edge weight.
But it is possible that we cannot find such z that reaches every node.
The solution is to add a new node z that goes to every node in G with a weight 0 edge:
1 2 3 4 5 6 7 8 9
JohnsonAPSP(G, s): Create H := (V + {z}, E + {(z, v) | v in V}) with w(z, v) = 0 Bellman-FordSSSP(H, z) to obtain dist_H for each edge(u, v) in H.E w'(u, v) := w(u, v) + dist_H(z, u) - dist_H(z, v) for each node u in G.V DijkstraSSSP(G, u) with w' to obtain dist_(G,w') for each node v in G.V dist_G(u, v) := dist_(G,w')(u, v) + dist_H(z, v) - dist_H(z, u)
However it never ends if there's a cycle in the graph.
Then introduce an additional parameter in the recurrence: dist(u,v,l) is the shortest path from u to v that uses at most l edges:
dist(u,v,l)=⎩⎨⎧0,∞min⎩⎨⎧dist(u,v,l−1),(x,v)∈Emin{dist(u,x,l−1)+w(x,v)}⎭⎬⎫,if u=v and l=0if u=v and l=0otherwise
Evaluate this recurrence easily in a "bottom-up" fashion:
dist(⋅,⋅,0) is easy to compute, given input graph.
dist(⋅,⋅,l+1) is easy to compute if dist(⋅,⋅,l) is known.
dist(⋅,⋅,n−1) is what we want.
1 2 3 4 5 6 7 8 9 10 11
RecursiveAPSP(G): for each pair(u, v) in V * V ifu= v then dist[u, v, 0] := 0 else dist[u, v, 0] := INF for l := 1 to n - 1 for each node u for each node v dist[u, v, l] := dist[u, v, l - 1] for each edge(x, v) going to v if dist[u, v, l] > dist[u, x, l - 1] + w(x, v) dist[u, v, l] := dist[u, x, l - 1] + w(x, v)
Time complexity is O(n4).
This recursive is like 1 and l−1 split in divide and conquer. How about 2l and 2l split?
dist(u,v,l)=⎩⎨⎧w(u,v),∞x∈V{dist(u,x,2l),dist(x,v,2l)}minif l=1 and (u,v)∈Eif l=1 and (u,v)∈/E
Starting with dist(⋅,⋅,1), then double l each time until 2⌈logn⌉.
1 2 3 4 5 6 7 8 9 10 11 12
FasterRecursiveAPSP(G): RecursiveAPSP(G): for each pair(u, v) in V * V ifu= v then dist[u, v, 1] := w(u, v) else dist[u, v, 1] := INF for i := 1 to ceil(log(n)) for each node u for each node v dist[u, v, 2^i] := INF for each node x if dist[u, v, 2^i] > dist[u, x, 2^(i - 1)] + dist[x, v, 2^(i - 1)] dist[u, v, 2^i] := dist[u, x, 2^(i - 1)] + dist[x, v, 2^(i - 1)]
Time complexity is O(n3logn).
Previous algorithms recurse on number of edges the shortest paths use. Strategy: Recurse on the set of node the shortest paths use.
Number the vertices arbitrary: x1,…,xn, define Vr={x1,…,xr} to be the set of vertices numbered at most r.
Define dist(u,v,r) to be the shortest path from u to v that uses only vertices in Vr as intermediate vertices. Let π(u,v,r) be such a shortest path.
Observation: either π(u,v,r) goes through xr or not.
Latter case: π(u,v,r)=π(u,v,r−1)
Former case: π(u,v,r)=π(u,xr,r)+π(xr,v,r)=π(u,xr,r−1)+π(xr,v,r−1)
dist(u,v,r)=⎩⎨⎧w(u,v),∞min{dist(u,v,r−1),dist(u,xr,r−1)+dist(xr,v,r−1)},if r=0 and (u,v)∈Eif r=0 and (u,v)∈/Eotherwise
1 2 3 4 5 6 7 8 9 10
FloydWarshallAPSP(G): for each pair(u, v) in V * V if(u, v) in E then dist[u, v, 0] := w(u, v) else dist[u, v, 0] := INF for r := 1 to n for each node u for each node v dist[u, v, r] := dist[u, v, r - 1] if dist[u, v, r] > dist[u, x_r, r - 1] + dist[x_r, v, r - 1] dist[u, v, r] := dist[u, x_r, r - 1] + dist[x_r, v, r - 1]