最短路径
The Shortest Path Problem: Given a map, what's the shortest path from to .
Consider a graph and a weight function that associates a real-valued weight to each edge . Given and in , what's the min weight path from to ?
- The graph can be directed.
- Negative edge weight allowed.
- Negative Cycle not allowed.
Single-Source Shortest Path(单源最短路径)
Single-Source Shortest Path (SSSP)
Given a graph and a weight function , given a source node , find a shortest path from to every node .
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 for each node .
- Alarm for source node goes off at time .
- If goes off, for each , update .
- At any time, value of is an estimate of .
- At any time, with equality holds when goes off.

The B alarm is assumed to be 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: when using a binary heap.
.
Here's an alternative derivation of Dijkstra's algorithm.
What's BFS doing? Expand outward from , growing the region to which distances and shortest paths are known. The growth should be orderly(closest nodes first).

Given known region , how to identify the node to expand to?
Assume is such node to expand to (i.e., the next closet node to ), let the shortest path from to is .
It must have for any , otherwise already .
Let the last node of path before to be , then it must be . Otherwise, is closer to than .
That means, find . This is an optimal substructure property.
Any satisfied node 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 denotes a graph of nodes and edges
- Let denotes the set of all such
- Let denotes all possible weight functions for a given .
- Let denotes all correct SSSP algorithms (when edge weights are possible).
Algorithm is existentially optimal (the usual definition) if
Algorithm is instance optimal (extremely hard to achieve) if
Algorithm is universally optimal (something in between) if
就写这么多吧,剩下略了。
SSSP in arbitrary weight graphs
This is Case 4.
Dijkstra's algorithm no longer works when negative edge weights are allowed.
Let the last node of path before to be , then it must be . Otherwise, is closer to than .
Shortest path from to any node must pass through nodes that are closer than .
This no longer holds.
But how values are maintained in Dijkstra is helpful.
This way two properties are maintained:
- For any , at any time,
v.distis either an overestimate or correct. - Assume is the last node on a shortest path from to . If
u.distis correct and we runUpdate(u, v), thenv.distbecomes correct.
This means, Update(u, v) is safe and helpful:
- Safe: Regardless of the sequence of
Updateoperations we execute, for any node , valuev.distis either an overestimate or correct. - Helpful: With correct sequence of
Update, we get correctv.dist.

Consider a shortest path from to , 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 arbitraryUpdatesequence, and still get shortest path from to .
Algorithm: Simply Update all edges for times:

How large is ? 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 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 .
.
Bellman-Ford algorithm process:

What if the graph contains a negative cycle? It means that after repetitions of Update, some node 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 .
.
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.

Pathfinding*
Reference & Online Dynamic Demonstration: Introduction to the A* Algorithm.
Given a graph , how to find a (shortest) path from a source to a destination , preferably efficiently?
You may say, "Just run Dijkstra's algorithm from and stop when 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 to .
On 2D grid, we can set heuristic(v, t) as the Manhattan distance between and , i.e., .
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.est_to_smaintains an (over or accurate) estimate ofdist(u, s), and this value changes during execution;u.est_to_tmaintains an (under or accurate) estimate ofdist(u, t), and this value does not change during execution.- Use
u.est_to_s + u.est_to_tas the metric to guide the search
Usually set to the straight-line distance between and .
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 where is the branching factor (the average number of successors per state), and 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 nodes.
All-Pairs Shortest Path(全源最短路径)
All-Pairs Shortest Path (APSP)
Given a graph and a weight function , for every pair , find a shortest path from to .
Straightforward solution for ASPS: for each , execute SSSP algorithm once. Complexity is where is the complexity of SSSP algorithm.
APSP from multiple SSSP
- Positive-weight graphs: Repeating Dijkstra gives .
- Arbitrary-weight graphs: Repeating Bellman-Ford gives .
Faster algorithms for arbitrary weight graphs? Modify edge weights without changing the shortest path, so that Dijkstra's algorithm can work.
However simply add a constant to all edge weights doesn't work:

Requirement: .
Or alternatively, for every path from to , changes it by the same amount:
- Let the
- Imagine is an entry gift and is an exit tax for traveling through .
Since we need for Dijkstra's algorithm, we can set for some fixed . Then cos the shortest path from to must be smaller than the shortest path from to plus the edge weight.
But it is possible that we cannot find such that reaches every node.
The solution is to add a new node that goes to every node in with a weight 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) |
Johnson's algorithm process:

Complexity is .
Floyd-Warshall Algorithm
APSP via recursion:
However it never ends if there's a cycle in the graph.
Then introduce an additional parameter in the recurrence: is the shortest path from to that uses at most edges:
Evaluate this recurrence easily in a "bottom-up" fashion:
- is easy to compute, given input graph.
- is easy to compute if is known.
- 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 if u = 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 .
This recursive is like and split in divide and conquer. How about and split?
Starting with , then double each time until .
1 2 3 4 5 6 7 8 9 10 11 12 | FasterRecursiveAPSP(G): RecursiveAPSP(G): for each pair (u, v) in V * V if u = 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 .
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: , define to be the set of vertices numbered at most .
Define to be the shortest path from to that uses only vertices in as intermediate vertices. Let be such a shortest path.

Observation: either goes through or not.
- Latter case:
- Former case:

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] |
Time complexity is .
「传递闭包」部分在上面给的《离散数学》笔记链接已经更详细地讲过了,这里略。