1、最短路径问题的抽象
- 在网络中,求两个不同顶点之间的所有路径中,边的权值之和最小的那一条路径
- 这条路径就是两点之间的
最短路径
(Shortest Path) - 第一个顶点为
源点
(Source) - 最后一个顶点为
终点
(Destination)
- 这条路径就是两点之间的
2、问题分类
-
单源
最短路径问题:从某固定源点出发,求其到所有其他顶点的最短路径- (有向)无权图
- (有向)有权图
-
多源
最短路径问题:求任意两顶点间的最短路径
3、无权图的单源最短路算法
- 按照
递增(非递减)
的顺序找出到各个顶点的最短路
无权图的单源最短路算法
void Unweighted(Vertex S)
{
Enqueue(S, Q);
while (!IsEmpty(Q))
{
V = Dequeue(Q);
for (V 的每个邻接点W)
if (dist[W] == -1)
{
dist[W] = dist[V] + 1;
path[W] = V;
Enqueue(W, Q);
}
}
}
dist[W]
:S到W的最短距离,初始化时,初始化为不可能的数,这里初始化为-1
dist[S]
:0
path[W]
:S到W的路上经过的某顶点,path[W] = V表示S到W的路径上,W上一步为V。顺着path数组顺着向前推,就是路径,用栈可以得到顺序的路径。
下图所示,源点为V3,经过上面的算法,每一个顶点都被标记的到源点的距离,从每一个顶点反向推可以得到源点到该点最短路径经过的顶点。
时间复杂度:
如果有|V|个顶点和|E|条边的图用邻接表存储,则
T = O( |V| + |E| )
每个顶点入栈一次,出栈一次,每条边被访问一次
4、有权图的单源最短路算法(Dijkstra算法)
这里不考录负值圈
按照递增的顺序找出到各个顶点的最短路
Dijkstra 算法
- 令S={源点s + 已经确定了最短路径的顶点vi}(先将路径较小的顶点收录进来,如剩下未收录的顶点中路径分别为3,5,2,无穷大,则下一步将距离为2的收录进来)
- 对任一未收录的顶点v,定义dist[v]为s到v的最短路径长度,但该路径
仅经过S中的顶点
。即路径的最小长度(注意:这里的最短路径不是最终的最短路径,随着其他顶点加进来,dist[v]会逐渐变小,最终成为最终的最短路径) - 若路径是按照
递增(非递减)
的顺序生成的,则- 真正的最短路必须只经过S中的顶点(为什么?)
- 每次从未收录的顶点中选一个dist最小的收录(贪心)
- 增加一个v进入S,可能影响另外一个w的dist值!
- dist[w] = min{dist[w], dist[v] + <v,w>的权重}
-
真正的最短路必须只经过S中的顶点,原因如下:
现在假设在集合S之外存在一个顶点w,使得s经w到到v的距离小于s直接到v的距离,则s到w的距离一定小于s到v的距离。下一步,我们要收录顶点v,s到w的距离小于s到v的距离,顶点w应该在v之前被收录到集合S中了,自相矛盾。 -
如果收录v使得s到w的路径变短,则:s到w的路径一定经过v,并且v到w有一条边,原因如下:
现在假设v到w的路径上还有一点a,则s到a的路径长度一定大于s到v的路径长度,由于若路径是按照递增(非递减)的顺序生成的,所以a的收录应该在点v之后,自相矛盾。 -
Dijkstra算法中的dist应该如何初始化?
如果s到w有直接的边,则dist[w]=<s,w>的权重;否则dist[w]定义为正无穷
//不能解决有负边的情况
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| )
- 对于稀疏图效果好
5、多源最短路算法
- 方法1:直接将单源最短路算法调用|V|遍
- T = O( |V|3 + |E|*|V|)
- 对于稀疏图效果好
- Floyd算法
- T = O( |V|3 )
- 对于稠密图效果好
Floyd 算法
-
Dk[i][j] = 路径{ i → { l<=k } → j }的最小长度,i到j的最短路径,只经过编号小于等于k的顶点。
-
D0, D1, …, D|V|-1[i][j]即给出了i到j的真正最短距离
-
最初的D-1是什么?
-
当Dk-1已经完成,递推到Dk时:
- 或者k不属于最短路径{ i → { l<=k } → j },则Dk= Dk-1
- 或者k属于最短路径{ i → { l<=k } → j },则该路径必定由两段最短路径组成: Dk[i][j]=Dk-1[i][k]+Dk-1[k][j]
i到j的顶点路径经过顶点k时,i到j的顶点路径包括两段,i到k,k到j,这两段距离中间都不包含k,所以应该在k-1步的时候已经求出。
D矩阵应该初始化:带权的邻接矩阵,对角元是0;如果i和j之间有直接边相连,初始化为边的权重,没有直接的边,D[i][j]应该定义为正无穷
void Floyd()
{
for (i = 0; i < N; i++)
for (j = 0; j < N; j++)
{//初始化就是邻接矩阵,有相邻节点初始化为权重,
//没有直接边相连,初始化为正无穷,对角元素为0,就是邻接矩阵
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;
}
}
根据path打印路径,path[i][j]=k;表示从结点i到结点j要经过结点k,递归调用,从节点i到节点k要经过那些顶点,从节点k到节点j要经过哪些顶点,最终得到路径。