单源最短路径算法——Dijkstra算法(迪杰斯特拉算法)

时间:2023-12-20 17:23:32

一 综述

Dijkstra算法(迪杰斯特拉算法)主要是用于求解有向图中单源最短路径问题。其本质是基于贪心策略的(具体见下文)。其基本原理如下:

(1)初始化:集合vertex_set初始为{source_vertex},dist数组初始值为$dist[i] = G.arc[source\_vertex][i],i=0,1,\ldots,n-1$

(2)从顶点集合V-vertex_set中选出$v_j$,满足$dist[j] = Min\left\{dist[i] | v_i∈V-vertex\_set\right\}$,那么$v_j$就是当前求得的一条从source_vertex出发的最短路径的终点,并令$vertex\_set = vertex\_set ∪ j$。

(3)修改从source_vertex出发到集合V-vertex_set上任一顶点$v_k$可达的最短路径长度:如果$dist[j] + arc[j][k] < dist[k]$,则令$dist[k] = dist[j] + G.arc[j][k]$。

(4)重复(2)~(3)操作共n-1次,直到所有的顶点都包含在vertex_set中。

具体代码如下:

#include<iostream>
#include<unordered_map>
#include<queue>
#include<cstring>
#include<cstdlib>
#include<cmath>
#include<algorithm>
#include<sstream>
#include<set>
#include<map>
using namespace std;
#define MAX_NUM 100
#define INF 0x7fffffff
/*
dijkstra算法的实现
参数说明:
1.source_vertex:表示源点
2.G:表示图(此处以邻接矩阵为例)
3.dist数组:表示源点到其他所有顶点的最短路径的长度。例如dist[j]表示源点到顶点Vj的最短路径长度
4.vertex_set数组:表示已找到最短路径的顶点的集合,其中vertex_set[i] = true表示顶点dist[i]已经最终确定
5.pre数组:用以表示顶点的最短路径的前驱顶点。例如,pre[i] = k表示顶点vi的最短路径上的前驱顶点为vk。
*/
class Graph
{
public:
int vertexNum;//顶点个数
int arcNum;//弧的个数
int vertex[MAX_NUM];//顶点表
int arc[MAX_NUM][MAX_NUM] = {{0,INF,10,INF,30,100},
{INF,0,5,INF,INF,INF},
{INF,INF,0,50,INF,INF},
{INF,INF,INF,0,INF,10},
{INF,INF,INF,20,0,60},
{INF,INF,INF,INF,INF,0}};//弧信息表
};
void Dijkstra(Graph &G,int source_vertex,int dist[],bool vertex_set[],int pre[])
{
int _min;
int k;
int vertex_num = G.vertexNum;//顶点个数
//初始化
for(int i = 0 ; i < vertex_num; i++)
{
dist[i] = G.arc[source_vertex][i];//初始化dist数组
if(i == source_vertex)
vertex_set[i] = true;//将顶点source_vertex加入vertex_set数组
else
vertex_set[i] = false;
pre[i] = 0;//前驱顶点为v0,后面会更新
}
//遍历n-1次,每次找到一个顶点的最短路径
for(int i = 1; i < vertex_num; i++)
{
_min = INF;//初始化辅助变量
//在未获取最短路径的顶点中,找到离source_vertex最近的顶点vk。
for(int j = 0; j < vertex_num; j++)
{
if(vertex_set[j] == false && dist[j] < _min)
{
_min = dist[j];
k = j;
} }
//此时源点到顶点vk的最短距离已经找到,即dist[k]已经确定,将k加入到vertes_set中
vertex_set[k] = true;
//对dist[j]进行检验更新
for(int j = 0; j < vertex_num; j++)
{
int tmp = (G.arc[k][j]== INF ? INF : _min + G.arc[k][j]);//防止溢出
if(vertex_set[j] == false && tmp < dist[j])
{
dist[j] = _min + G.arc[k][j];//更新满足条件的顶点的dist数组值
pre[j] = k;//更新前驱顶点
}
}
} } int main()
{
Graph G;
G.vertexNum = 6;
int source_vertex = 0;
int dist[6] = {0};
bool vertex_set[6];
int pre[100] = {0};
Dijkstra(G,source_vertex,dist,vertex_set,pre);
cout<<dist[0]<<endl; }

  

该算法的时间的时间复杂度为O(n^3),n为图中顶点的个数。其中比较核心的部分是最里面的两个for循环,第一个for循环对应的是第二步;而第二个for循环对应的是第三步;最外层的for循环对应的是第四步。

此外,(1)由于INF表示的int能表示的最大值,它加上一个正值必然会溢出,所以应该考虑溢出的问题。(或者别把INF设成这么大,设小些)

     (2)我们默认带权有向图在表示时,若果i == j,则w(i,j)=0而不是w(i,j)=∞。

二 相关理论和注意事项

1.Dijkstra算法的核心之处在于:从源点$v_0$到目标顶点$v_j$的最短路径,要么是弧$(v_0,v_j)$;要么是中间只经过vertex_set中的顶点而最后达到顶点$v_j$的路径。

证明如下:假设符合上述结论的最短路径为L1,假设从$v_0$到顶点$v_j$的最短路径上有一个顶点不在vertex_set中,则说明存在一条终点不在vertex_set中而长度比此路径更短的路径,设该路径为L2。但是,这是与事实相矛盾的。因为我们是按路径长度递增的次序来产生个最短路径的,故长度比此路径短的所有路径都已经产生,它们的终点必然在vertex_set中;即若L2 < L1,L2中的终点必然在集合vertex_set中。

2.更新集合vertex_set只在第二步,相当于每次选出最小的dist[j],实质就是一个贪心的过程;而第三部相当于更新每个满足条件的dist[j]。第二步和第三步就体现了理论中的两种情况。

3.可以根据pre数组追溯到源点到目标顶点的最短路径序列。  

4.Dijkstra算法不适用于边上带有负权重的有向图,如果边上有负值的话,有可能出现当与vertex_set内某点(记为A)以负边相连的点(记为B)确定最短路径时,它的最短路径加上负边的权值结果小于A原先确定的最短路径的长度,而此时的A在dijkstra算法下是无法更新的。例如:

单源最短路径算法——Dijkstra算法(迪杰斯特拉算法)

根据Dijkstra算法而言,如果求$v_0$到其他顶点的最短路径的话,首先一开始确定的是$dist[0] = 0$且$vertex\_set[0] = true$,然后由于$dist[2]$是最小的,所以$vertex\_se[2] = true$即$v_0$到$v_2$的最短路径长度为5,然而实际上$v_0$到$v_1$再到$v_2$的距离明显更小,所以实际上$v_0$到$v_2$的最短路径长度为应为7 - 5 = 2;但是$dist[2]$无法再更新了,所以此时利用Dijkstra算法求得的最短路径是错误的。