最短路径的现实应用很多很多。
强调单源顶点查找路径的方式,符合人类的正常思路,所以原理容易被理解,但是代码比较复杂.
如下图,从起点0到终点4的最短路径用黄色线标出,迪杰斯特拉算法思想诞生的依据是:如果这条路径是顶点0到顶点4的最短路径,那么对于其中经过的每一个结点v,起点到v和v到终点的距离也都是最短的。即起点到终点的最短路径由所有途径结点共享。所以我们就可以先找到起点到其邻近结点的最短距离,然后一步步逐渐往终点延伸。
迪杰斯特拉算法的终极本质:先找到起点到其邻近结点的最短距离,然后一步步逐渐往终点延伸。这也是贪婪算法的思想,所以迪杰斯特拉算法是一种贪婪算法,只不过他每一步找的当前最优(但是每一步找到的值后面的迭代过程中还可能变化,才能保证最终最优),一定可以保证最终也是最优。
迪杰斯特拉算法很像得到最小生成树的prim算法,把顶点集合划分为已访问和未访问,或者说已选集合和未选集合。
和prim算法一样,用到三个辅助数组,但是含义略微不同:
迪杰斯特拉算法的执行过程:
至此迪杰斯特拉算法结束,所有顶点加入了。我们来深入分析一下,可以发现:
可以通过parent数组画出最短路径(双线是路径,单线是parent数组中存储的其他走线,对我找0-4最短路径没用的线,线上数字是起点到线末尾顶点的最短距离):
#define MAXVEX 100
#define INF 65535
void ShortestPath_Dijkstra(MGraph * G, int v0, int * distance, int * parent)
{
//参数v0是最短路径的起点,终点不用传入,因为迪杰斯特拉算法运行结束后可得到v0到任意顶点的最短路径
bool visited[MAXVEX];
int v;
//初始化三个数组
for (v=0;v<G->numV;++v)
{
visited[v] = false;
(*distance)[v] = G->arc[v0][v];//不初始化为INF,而是直接初始化为v0到各顶点的距离,更快一步
(*parent)[v] = -1;
}
//v0是起点
visited[v0] = true;
(*distance)[v0] = 0;
int min, w, k;
for (v=1;v<G->numV;++v)
{
min = INF;
//扫描所有未选顶点,找到最小距离顶点
for (w=0;w<G->numV;++w)
{
if (!visited[w] && (*distance)[w] < min)
{
min = (*distance)[w];
k = w;//最小距离顶点
}
}
//加入顶点k
visited[k] = true;
//为新加入的顶点k更新distance和parent数组,min是v0到顶点k的最短距离
for (w=0;w<G->numV;++w)
{
if (!visited[w] && min+G->arc[k][w]<(*distance)[w])
{
(*distance)[w] = min + G->arc[k][w];
(*parent)[w] = k;
}
}
}
}
迪杰斯特拉算法的缺点是:
以算法精妙智慧,代码简洁优雅著称
完全抛开了单点的局限思维模式,巧妙的运用了矩阵的变换,用最清爽的代码实现了多顶点之间的最短路径的求解,原理理解难一点,但是代码很简洁。
需要用两个 n × n n\times n n×n的辅助矩阵:
比如这个图,这里是以有向图举例,但是无向图也可以用这本文这两个最短路径算法:
其D矩阵的初始状态就是上图的邻接矩阵:
对顶点u进行迭代的根本目的是判断:图中任意两个顶点v,w之间的最短路径是否可能经过结点u。
如果 d v u + d u w d_{vu}+d_{uw} dvu+duw小于当前D(v,w), 则应该途径顶点u,更新S(v,w)为S(v,u),更新D(v,w)为 d v u + d u w d_{vu}+d_{uw} dvu+duw;否则就不应该经过u。
这两个矩阵的更新是最为重要的,注意D一定是更新为更短的距离,而S是更新为S(v,u),即同一行的红色数字,即S从上一个S那里复制的固定不更新的标注为红色的数字。
对上述图做示例分析:
填充的方法是什么呢?就是判断行数顶点到列数顶点的最短路径是否可能途径顶点0(现在在为顶点0做迭代),比如D0的第1行第2列,即1-2的最短路径是否可能途径顶点0, d 10 + d 02 = i n f + 8 d_{10}+d_{02}=inf+8 d10+d02=inf+8,而 d 12 d_{12} d12本身也是inf,对于这种都是无穷大的,不用像数学上那样严格判断 i n f + 8 > i n f inf+8>inf inf+8>inf,而是直接就算了,认为不经过顶点0了,因为最短路长度再大也不可能到达inf,没必要在inf尺度上计较盘算。于是D0(1,2)仍为inf,而S0(1,2)为2,表示从1直接到2,没经过别的点。
直接复制D的第0行第0列到D0是因为:0-v(v=0,1,···,4)的最短路径一定要途径0,所以D0(0,v)都是确定值,就是0-v边的权值;而v-0的最短路径也必须经过点0,D0(v,0)也是v-0边的权值。就算不复制,走上面的计算流程,也会得到这组值,那不如直接复制,少了一部分判断。
为了便于观看和判断,我把复制过来的位置标红了,标红位置不需要更新。并且,对点u的迭代中,每一个空位D(i,j)的( d i u + d u j d_{iu}+d_{uj} diu+duj就是D(i,j)所在行的红色数字加上D(i,j)所在列的红色数字),很方便。
迭代前:
看D0(1,3), d 10 + d 03 = i n f + i n f d_{10}+d_{03}=inf+inf d10+d03=inf+inf,太大了,不经过点0,D(1,3)现在存的直达距离 d 13 = 1 < < i n f d_{13}=1<
d 10 + d 04 d_{10}+d_{04} d10+d04太大,不经过0,D(1,4)现在存的直达距离 d 14 = 7 d_{14}=7 d14=7,所以D0(1,4)和S0(1,4)不更新
d 30 + d 01 = 2 + 3 = 5 < D ( 3 , 1 ) = i n f d_{30}+d_{01}=2+3=5
d 30 + d 04 = 2 − 4 = − 2 < D ( 3 , 4 ) = i n f d_{30}+d_{04}=2-4=-2
D2(3,1)=5,大于红数字加和-1,所以应该走点2,即点3到点1的最短路径应该经过点2,而不是原来的点0,所以改D2(3,1)=-1,S2(3,1)=S(3,2)
结果:
还有一个很重要的我写完代码才发现的点:S矩阵在所有顶点迭代完毕后每一行的数字是一样的,我之前以为是个巧合,没怎么管,但是我写文末的求v-w最短路径的代码时,发现,打印v-w的最短路径,发现的途径顶点k一定会被v直达从而可以直接打印在v后面,而迭代找到更多顶点一定是在k-w之间去找,即k更新为S(k,w)。比如,找0-1的最短路:
所以根据S矩阵找v-w的最短路径时,注意途径顶点k一定能够被它前一个顶点直达,而只需要查看k是否可以直达终点。每一次都是在判断新找出的k是否可以直达终点w。
从这个最终的S矩阵找2到0的最短路径:
Floyd算法和迪杰斯特拉算法不一样的一点是:后者是多源最短路径算法,前者是单源最短路径算法。即:
弗洛伊德算法可以从任意一点出发,拿到所有顶点到所有顶点的最短距离及其路径。但是迪杰斯特拉要找v到某点的最短路,则只能从v出发,并且找到的不仅是v到自己需要的终点的最短路,而是把v到任意其他顶点的最短路都找到了。
所以无须把起点作为参数传入下面的函数。
typedef int Pathmatrix[MAXVEX][MAXVEX];//Pathmatrix是int[MAXVEX][MAXVEX]类型
typedef int ShortPathTable[MAXVEX][MAXVEX];
/*Floyd算法,求网图G中顶点v到顶点w的最短路径及其距离*/
void ShortestPath_Floyd(MGraph * G, Pathmatrix * S, ShortPathTable * D)
{
int v, w;
//初始化D和S矩阵
for (v=0;v<G->numV;++v)
{
for (w=0;w<G->numV;++w)
{
(*D)[v][w] = G->arc[v][w];
//if (v!=w)//对角线不管
(*S)[v][w] = w;
}
}
int k;
//主循环,三层嵌套
for (k=0;k<G->numV;++k)//从顶点0开始,迭代每一个顶点,以更新D和S
{
for (v=0;v<G->numV;++v)
{
for (w=0;w<G->numV;++w)
{
if (v!=w && v!=k && w!=k)//不更新对角线和第k行第k列的数据
{
if ((*D)[v][k]+(*D)[k][w] < (*D)[v][w])//如果途径k反而更短,则更新D和S,经过k
{
(*D)[v][w] = (*D)[v][k]+(*D)[k][w];
(*S)[v][w] = (*S)[v][k];
}
}
}
}
}
}
哈哈,我对弗洛伊德真的理解了,关键代码自己写的。
时间复杂度分析:
总的是 O ( n 3 ) O(n^3) O(n3)
/*打印所有顶点对之间的最短路径*/
void printShortestPaths(MGraph * G, Pathmatrix * S, ShortPathTable * D)
{
int v, w;
//枚举法遍历所有顶点对
for (v=0;v<G->numV;++v)
{
for (w=v+1;w<G->numV;++w)
{
printShortestPath(S, D, v, w);
}
printf("\n");
}
}
/*打印顶点v到顶点w的最短路径*/
void printShortestPath(Pathmatrix * S, ShortPathTable * D, int v, int w)
{
printf("v%d-v%d shortest distance: %d ", v, w, D[v][w]);
k = (*S)[v][w];
printf(" path: %d ", v);//打印路径的源点
while (k != w)//只需判断k是否可以直达终点,路径中k前面的点一定可以直达k,因为S矩阵每一行数字相同
{
printf(" -> %d ", k);
k = (*S)[k][w];
}
printf(" path: %d ", w);//打印路径的终点
}