20230330 7.1. 最短路径问题
最短路径问题的抽象
- 在网络中,求两个不同顶点之间的所有路径中,边的权值之和最小的那一条路径
- 这条路径就是两点之间的最短路径(Shortest Path)
- 第一个顶点为源点(Source)
- 最后一个顶点为终点(Destination)
问题分类:
- 单源最短路径问题:从某固定源点出发,求其到所有其他顶点的最短路径
- (有向)无权图
- (有向)有权图
- 多源最短路径问题:求任意两顶点间的最短路径
无权图的单源最短路算法
按照递增(非递减)的顺序找出到各个顶点的最短路
使用BFS广度优先遍历
代码实现
/**
* 无权图的单源最短路算法
*/
public void unweighted(int start, int[] dist, int[] path) {
Queue<Integer> queue = new LinkedList<>();
Arrays.fill(dist, -1);
dist[start] = 0;
Arrays.fill(path, -1);
path[start] = start;
queue.offer(start);
while (!queue.isEmpty()) {
int v = queue.poll();
for (int w = 0; w < size; w++) {
if (edges[v][w] == 1 && dist[w] == -1) {
dist[w] = dist[v] + 1;
queue.offer(w);
path[w] = v;
}
}
}
}
有权图的单源最短路算法
Dijkstra 算法(迪杰斯特拉算法)
- 令S=
- 对任一未收录的顶点v,定义dist[v]为s到v的最短路径长度,但该路径仅经过S中的顶点。即路径{s -> (vi ∈ S) -> v}的最小长度
- 若路径是按照递增(非递减)的顺序生成的,则
- 真正的最短路必须只经过S中的顶点(为什么?)
- 每次从未收录的顶点中选一个dist最小的收录(贪心)
- 增加一个v进入S,可能影响另外一个w的dist值!
- dist[w] = min
- 参考博客
void Dijkstra( Vertex s )
{
while (1) {
V = 未收录顶点中dist最小者;
if ( 这样的V不存在 )
break;
collected[V] = true;
for ( V 的每个邻接点 W )
if ( collected[W] == false )
if ( dist[V]+E<V,W> < dist[W] ) {
dist[W] = dist[V] + E<V,W> ;
path[W] = V;
}
}
}
/* 不能解决有负边的情况 */
- 方法1:直接扫描所有未收录顶点 – O( |V| )
- \(T = O( |V|^2 + |E| )\)
- 对于稠密图效果好
- 方法2:将dist存在最小堆中 – O( log|V| )
- 更新dist[w]的值 – O( log|V| )
- T = O( |V| log|V| + |E| log|V| ) = O( |E| log|V| )
- 对于稀疏图效果好
代码实现
/**
* 有权图的单源最短路算法
*/
public void dijkstra(int start, int[] dist, int[] prev) {
boolean[] collected = new boolean[size];
collected[start] = true;
for (int i = 0; i < size; i++) {
dist[i] = edges[start][i];
}
dist[start] = 0;
Arrays.fill(prev, start);
while (true) {
// 寻找最短路径相接的节点
int min = Integer.MAX_VALUE, v = -1;
for (int j = 0; j < size; j++) {
if (!collected[j] && min > dist[j]) {
min = dist[j];
v = j;
}
}
if (v == -1) {
// 无法找到
break;
}
collected[v] = true;
// 修正当前最短路径和前驱顶点
// 可能存在从1-2-3比直接从1-3更短
for (int k = 0; k < size; k++) {
if (!collected[k]) {
int tmp = (edges[v][k] == Integer.MAX_VALUE ? Integer.MAX_VALUE : (min + edges[v][k]));
if (tmp < dist[k]) {
dist[k] = tmp;
prev[k] = v;
}
}
}
}
}
多源最短路算法
- 方法1:直接将单源最短路算法调用|V|遍
- \(T = O( |V|^3 + |E|*|V|)\)
- 对于稀疏图效果好
- 方法2:Floyd 算法
- \(T = O( |V|^3 )\)
- 对于稠密图效果好
Floyd 算法(弗洛伊德算法)
- \(D^k[i][j] = 路径{ i \rightarrow \{ l \le k \} \rightarrow j }\) 的最小长度
- \(D^0, D^1, …, D^{|V|-1}[i][j]\) 即给出了 i 到 j 的真正最短距离
- 最初的 \(D^{-1}\) 是什么?
- 当 \(D^{k-1}\) 已经完成,递推到 \(D^k\) 时:
- 或者k ∉ 最短路径 \({ i \rightarrow \{ l \le k \} \rightarrow j }\) ,则 \(D^k = D^{k-1}\)
- 或者k ∈ 最短路径 \({ i \rightarrow { l \le k } \rightarrow j }\) ,则该路径必定由两段最短路径组成: \(D^k[i][j]=D^{k-1}[i][k]+D^{k-1}[k][j]\)
- 参考博客
void Floyd()
{
for ( i = 0; i < N; i++ )
for( j = 0; j < N; j++ ) {
D[i][j] = G[i][j];
path[i][j] = -1;
}
for( k = 0; k < N; k++ )
for( i = 0; i < N; i++ )
for( j = 0; j < N; j++ )
if( D[i][k] + D[k][j] < D[i][j] ) {
D[i][j] = D[i][k] + D[k][j];
path[i][j] = k;
}
}
代码实现
/**
* 多源最短路算法
*/
public void floyd(int[][] dist, int[][] path) {
for (int i = 0; i < size; i++) {
for (int j = 0; j < size; j++) {
dist[i][j] = edges[i][j];
path[i][j] = j;
}
}
// 计算最短路径
for (int k = 0; k < size; k++) {
for (int i = 0; i < size; i++) {
for (int j = 0; j < size; j++) {
// 如果经过下标为k顶点路径比原两点间路径更短,则更新dist[i][j]和path[i][j]
// 也就是i-k-j的距离小于i-j
int tmp = (dist[i][k] == Integer.MAX_VALUE || dist[k][j] == Integer.MAX_VALUE) ? Integer.MAX_VALUE :
(dist[i][k] + dist[k][j]);
if (dist[i][j] > tmp) {
dist[i][j] = tmp;
path[i][j] = path[i][k];
}
}
}
}
}