Dijkstra算法属于稍微复杂一些的算法,理解起来也不是那么容易,当初在网上搜最短路径算法的时候感觉大多数博文都讲的不是那么通俗,主要是代码,许多需要注释的地方都省略了。也不是说这样做不好,注释的越少越能提高程序员阅读代码的能力。但是对新人来讲,先给一个注释详细的代码,然后接下来自己多动手敲几遍(不要看之前的代码),这样可能更有效果。要想理解算法不自己完整地敲一遍是不可能的。
算法分析:对于一个有向图G=<V,E>,和源点(计算的是源点到其他的距离)如下图:
现在我们选F点作为源点,箭头上的数值即为该边的权值(算法的前提:没有负权值),没有直接通路的两点间的距离记为MAX。
我们先思考一个问题,所有F点到其他点的最短的直接距离是多少(直接距离:无中间点的一条有向边的距离,如F→A,而F→B→A不是)。想必大家都知道,如上图,F点到其他点的直接距离最短的明显是F→B,显然假如不存在负权的边的话,F→B即为从F到B的最短路径。现在,我们把源点切换到B,再做一次刚才的查找,我们可以得出B到C的最短路径为B→C,而非B→A→D→C。现在就是算法的关键,我们来思考一下F到C的最短路径是多少?
F到C的最短路径其实就是F→B→C,那么为什么是这样呢?我们需要用一下贪心的思想,每一次都要做出来最优的选择,这样总结果才可能是最优的。F→B,B→C均是按照最最短的直接距离选择的,那么F到C的最短路就是F→B→C,那么真的是这样吗?
可能很多人会疑问,假如F→C的距离为6的话,岂不是比F→B→C短的多?说的非常对,F→B→C仅仅是恰好为最短路径,才会有这样的结果。所以我们需要提出一个当前最短路径的概念。当前最短路径即为截止到当前计算为止的最短路,这时候我们想一想刚才的疑惑,为什么会出现F→C的距离为6时,比我们F→B→C的距离要短呢。因为F→B→C仅仅是截止到当前计算为止的最短路径,而我们的计算是沿着一条线的,每次比较也仅仅局限于从当前点到其他点直接距离路径的比较,自然可能忽略一些可能更短的路,所以我们在每次更新当前最短路时需要加入源点到N点的直接路径与源点到N点的当前最短路径比较(当前点到N点的直接距离最短)。
这样的话,可能又有人有疑问了,为什么只比较源点到N点的距离?这就是Dijkstra算法的巧妙之处,我们思考如下的一个情景(与上图无关):B→C = 7,B→A = 10,A→C = 10,显然B到C的最短路为B→C而非B→A→C,有人可能会说,像刚才一样,假如B→A = 2,A→C = 3的话不就又犯了和刚才一样的错误了吗。那我们想想,假如B→A < B→C的话那么最短直接距离的选择的时候还会不会选择B→C了呢?显然,假如我们选择了B→C作为最短直接距离则B→A一定比B→C大,这样的话无论接下来A→C的距离多短总距离都不会超过B→C,现在为什么不能有负权的边的原因大家也会清楚了(假如有负权的话要用Floyd算法)。
这点我们搞清楚的话,接下来的步骤就是不断的循环,我会放上我写的一个带有详尽注释的算法实现,附有一个图示的测试样例,测试过完全可以运行,大家可以复制到IDE中自行测试,记住一定要自己多敲几遍。
算法实现:
#define MAX 10000
#define NUM 6
//最终目的:求出源点到其他点的距离
//origin:选定的源点
//dist[a][b]:由 a 点到 b 点的距离,当 a == b 时,dist[a][b] == 0
void dijkstra(int origin, int dist[][NUM]){ //这里为了方便用了int类型的权值,可以自行更改为其他类型
int miniDist[NUM] = {0}; //当前最短距离,在所有循环结束时为最终的最短距离
int p[NUM]; //p : previovs (当前最短路的)前驱顶点 ATTENTION:从1开始记
int s[NUM]; //s[i] == 1 表示源点到i点的最短路径已经求出 ATTENTION:不是"当前"最短距离
int min = 0; //当前最短距离
int i,j; //循环变量
int k = 0; //k第一次出现时有解释
int pre = 0;//前驱顶点,与p[n]关联
int tempOrigin = origin; //用一个临时变量来进行运算避免因为之后的改动丢失原始参数
for(i = 0; i < NUM; i++) {
miniDist[i] = dist[origin][i];
if (miniDist[i] != MAX) {
p[i] = origin + 1; //将所有与源点有联通的点的前驱顶点记为 源点+1
//+1的原因是需要将无通路的顶点的前驱顶点记为0,如果不+1则要将无通路的前驱顶点记为-1
} else {
p[i] = 0;
}
s[i] = 0; //初始化s数组
}
s[origin] = 1;
//以下循环为算法主要部分
for(i = 0; i < NUM - 1; i++) {
min = MAX + 1; //这样可以将无通路的点也加入计算
for(j = 0; j < NUM; j++) {
//两种情况会更新当前最短路:
//1:(非当前)最短路未求出
//2:源点到该点的距离是(当前)最短的
if( !s[j] && (miniDist[j] < min) ) {
min = miniDist[j];
k = j;
}
}
s[k] = j; //如上,源点到k是最短路
for(j = 0; j < NUM; j++) {
//两种情况会更新最短路
//1:(非当前)最短路未求出
//2: (当前)间接最短路小于(当前)直接最短路
//"miniDist[k] + dist[k][j]" 表示从源点到j点的(当前)间接最短路
//"miniDist[j]" 表示从源点到j点的(当前)直接最短路
if( !s[j] && (miniDist[j] > miniDist[k] + dist[k][j]) ){
miniDist[j] = miniDist[k] + dist[k][j];
p[j] = k + 1; //更新前驱顶点
}
}
}
//循环结束,所有的当前最短路即为所求最短路
//打印最短路和前驱顶点
//ATTENTION:这时的前驱顶点从0开始计数
for(i = 0; i < NUM; i++) {
printf("最短路:%6d\t路径:%d",miniDist[i],i);
pre = p[i];
while( (pre != 0) && (pre != origin + 1) ){
printf("<-%d",pre - 1);
pre = p[pre - 1];
}
printf("<-%d\n",origin);
}
}
int main(){
//以下为样例,可以自行试验
int dist[6][6];
for(int i = 0; i < 6; ++i){
for(int j = 0; j < 6; ++j){
if(i == j){
dist[i][j] = 0;
continue;
}
dist[i][j] = 10000;
}
}
dist[5][1] = 5;
dist[0][1] = 6;
dist[1][0] = 18;
dist[1][5] = 10;
dist[5][3] = 25;
dist[5][0] = 24;
dist[1][2] = 7;
dist[4][2] = 4;
dist[3][2] = 12;
dist[2][3] = 15;
dist[2][0] = 9;
dist[0][3] = 8;
dijkstra(5,dist);
}