本文参考博客:学图论,你真的了解最短路吗? - 绝顶我为峰 的妙妙屋 - 洛谷博客
Dijkstra算法是一种求非负权值的单源最短路径算法。问题的提法是:给定一个有向带权图D与源点v,各边上的权值均为非负数,要求找出从v到D中其他各个顶点的最短路径。
首先我们知道,如果起点v到终点u存在一条最短路径,那么该条路径的某一部分也一定是起点v到某个中途节点的最短路径,如下图的例子
假如黄色路径是0到4的最短路径,我们就可以知道0到8的最短路径一定是0 -> 1 -> 7 -> 8。若非如此,假如0到8之间存在一条更短的路径,如下图红色路径,
那么0到4之间就一定存在一条更短的路径,即红色路径加上8到4的黄色路径,便与原来的前提“黄色路径是0到4的最短路径”相矛盾。
说回Dijkstra算法,对于求最短路径,Dijkstra的思想是:用一个集合S存放已经求出最短路径的终点,每次操作找出与集合相邻且距离起点最近(即边权值最小)的点加入集合中,直到所有点都被加进集合内,算法结束。(看不懂没关系,看完下面的例子就懂了)
同时,算法引入了一个数组dist[],其中每个分量dist[i]为起点到第i个点的最短路径长度(初始化为无穷大,实际操作时可自己定义一个MAX_VALUE);若有需要,可以多引入一个数组path[]来记录最短路径,该数组的用法下文会进行介绍。
我们用一个例子来直观的感受一下Dijkstra算法的过程。
第一步,我们先把 a (起点)加入集合,不加粗的点为集合中的点,下同(s={a} ;dis[]={0,∞,∞,∞,∞,∞,∞,∞}):
第二步,我们找出与集合相邻且距离起点最近的点 b,把它加入集合,并确定它的最短路 0+2=2,存入数组(s={a,b} ;dis[]={0,2,∞,∞,∞,∞,∞,∞}):
第三步,我们找出与集合相邻且距离起点最近的点 d,把它加入集合,并确定它的最短路 2+1=3,存入数组(s={a,b,d} ;dis[]={0,2,∞,3,∞,∞,∞,∞}):
第四步,我们找出与集合相邻且距离起点最近的点 e,把它加入集合,并确定它的最短路 3+2=5(s={a,b,d,e} ;dis[]={0,2,∞,3,5,∞,∞,∞}):
第五步,我们找出与集合相邻且距离起点最近的点 f,把它加入集合,并确定它的最短路 0+9=90+9=9(s=\{a,b,d,e,f\}s={a,b,d,e,f} ;dis[]=\{0,2,∞,3,5,9,∞,∞\}dis[]={0,2,∞,3,5,9,∞,∞}):
第六步,我们找出与集合相邻且距离起点最近的点 g,把它加入集合,并确定它的最短路 5+7=12(s={a,b,d,e,f,g} ;dis[]={0,2,∞,3,5,9,12,∞}):
第七步,我们找出与集合相邻且距离起点最近的点 c,把它加入集合,并确定它的最短路 5+8=13(s={a,b,c,d,e,f,g} ;dis[]={0,2,13,3,5,9,12,∞}):
第八步,也是最后一步,我们找出与集合相邻且距离起点最近的点 h,把它加入集合,并确定它的最短路 13+5=18(s={a,b,c,d,e,f,g,h} ;dis[]={0,2,13,3,5,9,12,18}):
至此,整个图的最短路被我们求了出来,DijkstraDijkstra 顺利完成!
求出了最短路径长度,如果还想知道起点到终点的最短路径经过了哪些点怎么办?这时候就要用到前面提过的path[]数组了。其中path[i]表示在起点到i的最短路径上i的前驱节点。还是拿开篇的图做例子:
在该例子中,path[4]=5 (即在最短路径中,4的前驱节点是5)path[5]=2,path[2]=8,path[8]=7,path[7]=1,path[1]=0。
由此,我们便可以知道0到4的最短路径为0 -> 1 -> 7 -> 8 -> 2 -> 5 -> 4
而path[i]只需要在i放入集合s时可以顺便更新,具体看下面实现。
思路出来了,下面就是算法的实现(C++)
(代码仅供参考,具体代码还需要根据图的实现来完成)
int n;//图顶点的个数
bool* s=new bool[n];//顶点集合,true表示起点到该顶点已找到最短路径
int* dist =new int[n];//起点到该顶点的最短路径长度
int* path=new int[n];//最短路径
void Dijkstra(Graph G,int v)//图G,起点v
{
//初始化
for (int i = 0; i < n; ++i)
{
s[i] = false;
dist[i] = G.getWeight(v, i);
//最短路径设置为起点v到顶点i的权重,若两点间没有边,则为MAX_VALUE
if (i != v && dist[i] < MAX_VALUE) path[i] = v;
//已存在最短路径则将i的前驱设为v
else path[i] = -1;
//否则设为-1
}
s[v] = true; dist[v] = 0; //将起点加入集合
/*每次循环会将一个顶点加入集合,起点已经加入了集合,
因此进行n - 1次循环就可以求出起点所有顶点的最短路径*/
for (int i = 0; i < n - 1; ++i)
{
int u = v;
//u在下面的循环找出与集合s相邻且距离起点最近(即边权值最小)的点,此时dist[u]=MAX_VALUE
for (int j = 0; j < n; ++j)
{
if (s[j] = false && dist[j] < dist[u]) u = j;
//找出不在集合内(即还没找到最短路径)且距离起点最近的顶点则更新
}
s[u] = true; //将找到的点加入集合
for (int k = 0; k < n; ++k) //对于每个顶点
{
int w = G.getWeight(u, k); //得出每个顶点与u之间的权重
if (s[k] == false && w < MAX_VALUE && dist[u] + w < dist[k])
//若该顶点没访问过且存在u到k的边且起点到u的最短路长度加上权重小于起点到k的最短路
{
dist[k] = dist[u] + w; //更新k的最短路
path[k] = u; //设置k的前驱
}
}
}
}
容易知道该算法复杂度为O(n^2)