最短路径和最小生成树在应用很是不同的,比如:一开始修建一条地铁,然后在地铁点上有多个点,需要修建一个路程最短的地铁线,将这些地铁点连接起来,这就是最小生成树(点与点之间距离是已知的)。小强需要从A点去B点旅游,中间会经过好几个点,需要找出条最短路径到达B点。从应用上明显看出,两者的目的不同、初始化条件也是不同的。
一、Dijkstra(迪杰斯特拉)算法
Dijkstra(迪杰斯特拉)算法是典型的最短路径路由算法,用于计算一个节点到其他所有节点的最短路径。主要特点是以起始点为中心向外层层扩展,直到扩展到终点为止。
1、算法思想
令G = (V,E)为一个带权有向图,把图中的顶点集合V分成两组,第一组为已求出最短路径的顶点集合S(初始时S中只有源节点,以后每求得一条最短路径,就将它对应的顶点加入到集合S中,直到全部顶点都加入到S中);第二组是未确定最短路径的顶点集合U。在加入过程中,总保持从源节点v到S中各顶点的最短路径长度不大于从源节点v到U中任何顶点的最短路径长度。
针对上图建立的两个集合,之后运用Dijkstra算法运行后,两个集合中元素为将ABCDEF在数组中坐标为1,2,3,4,5,6:
迭代 | S | U | dist[2] | dist[3] | dist[4] | dist[5] | dist[6] |
1 | A | - | 6 | 3 | MAX | MAX | MAX |
2 | A,C | C | 5 | 3 | 6 | 7 | MAX |
3 | A,C,B | B | 5 | 3 | 6 | 7 | MAX |
4 | A,C,B,D | D | 5 | 3 | 6 | 7 | 9 |
5 | A,C,B,D,E | E | 5 | 3 | 6 | 7 | 9 |
6 | A,B,C,D,E,F | F | 5 | 3 | 6 | 7 | 9 |
2、算法分析
根据上面分析,得知需要创建两个数组,创建顶点集合,还有边集合,用于保存点到各个边的权重,然后在权重集合选取最小的权值边对的顶点,然后继续循环。
- 创建顶点结合nNodeIndex,初始化为0,数组中为1是,表示对应的顶点已经添加到最短路径顶点集合S了。
- 创建初始顶点到各个顶点的边集合,保存此顶点到各个顶点的距离(权重),用邻接矩阵中行元素初始化(类似最小生成树)。
- 循环计算此顶点到各个顶点的最小值,得知后nNodeIndex[i] = 1,同时更新边集合 的数值。
- 总体上讲还是比较容易的。
3、例图解释
4、代码
//Dijkstra算法 void GraphData::ShortPath_Dijkstra(GraphArray *pArray) { //Dijkstra算法和最小生成树的算法,在某种程度是相似的 int min,i,j,k; int nNodeIndex[MAXVEX]; //保存相关顶点坐标,1就是已经遍历访问的过结点(在最小生成树中为数值表示遍历过同时值与坐标是一条边) int nNodeWeight[MAXVEX]; //保存某个顶点到各个顶点的权值,为不为0和最大值表示遍历过了。 int nPathLength[MAXVEX]; //坐标和元素表示为同时值和坐标表示一边,与Primes有相同的 //两个数组的初始化 printf("开始初始化,当前顶点边的权值为:"); for(i = 0;i<pArray->numVertexes;i++) { nNodeIndex[i] = 0; nNodeWeight[i] = pArray->arg[0][i];//设定在矩阵中第一个顶点为初始点。 nPathLength[i] = 0; printf(" %c",nNodeWeight[i]); } nNodeWeight[0] = 0; //选取坐标点0为起始点。 nNodeIndex[0] = 1; //这样0就是起始点,设为1.(和Prime的不同) //算法思想 for (i = 1;i< pArray->numVertexes;i++) { min = INFINITY; //初始化权值为最大值; j = 1; k = 0; // 循环全部顶点,寻找与初始点边权值最小的顶点,记下权值和坐标 while(j < pArray->numVertexes) { //如果权值不为0,且权值小于min,为0表示本身 if (!nNodeIndex[j]&&nNodeWeight[j] < min) //这里Prime是权重中不为0, { min = nNodeWeight[j]; k = j; //保存上述顶点的坐标值 } j++; } printf("当前顶点边中权值最小边(%d,%d)\n",nNodeIndex[k] , k); //打印当前顶点边中权值最小 //nNodeWeight[k] = 0; //将当前顶点的权值设置为0,表示此顶点已经完成任务 nNodeIndex[k] = 1; //将目前找到的最近的顶点置为1 //for (j = 1;j< pArray->numVertexes;j++) //循环所有顶点,查找与k顶点的最小边 //{ // //若下标为k的顶点各边权值小于此前这些顶点未被加入的生成树权值 // if (nNodeWeight[j] != 0&&pArray->arg[k][j] < nNodeWeight[j]) // { // nNodeWeight[j] = pArray->arg[k][j]; // nNodeIndex[j] = k; //将下标为k的顶点存入adjvex // } //} //修正当前最短路径及距离 for (j = 1;j< pArray->numVertexes;j++) //循环所有顶点,查找与k顶点的最小边 { //若下标为k的顶点各边权值小于此前这些顶点未被加入的生成树权值 if (!nNodeIndex[j] && pArray->arg[k][j] + min< nNodeWeight[j]) { nNodeWeight[j] = pArray->arg[k][j] + min; nPathLength[j] = k; //将下标为k的顶点存入adjvex } } //打印当前顶点状况 printf("坐标点数组为:"); for(j = 0;j< pArray->numVertexes;j++) { printf("%3d ",nPathLength[j]); } printf("\n"); printf("权重数组为:"); for(j = 0;j< pArray->numVertexes;j++) { printf("%3d ",nNodeWeight[j]); } printf("\n"); } }
5、代码分析
上图就是图的邻接矩阵,对应上面的例图分析,我们分析下,代码运算结果。
上图是代码运算后的结果,首先是第一次,最小边为(0,2),加入的顶点是C。第二次最小边是(0,1),加入的结果是B。等等,会发现结果同前面例图分析结果一样。
最后的权重数组为(0,5,3,6,7,9)意味着顶点A到B,C,D,E,F的距离是5,3,6,7,9。A->B:5,根据坐标点数组nPathLength[1] = 2,意味着经过坐标为2的顶点C,再看nPathLength[2] = 0,结束。再比如A->D:6,nPathLength[4] = 2,所以为A->C->D.同理其他路劲也是如此寻找。
二、Floyd算法
Dijkstra算法解决了某个源点到其余各个顶点的最短距离问题。从循环语句上判断,算法的时间复杂度是O(n2)。在循环的外面再加一个循环,也就成了所有顶点的最短距离。此时算法的复杂度就是O(n3).
弗洛依德(Floyd)算法就是一个事件复杂度为O(n)的算法,只不过算法非常简洁优雅。
1、算法思想
Floyd算法是一个经典的动态规划算法。用通俗的语言来描述的话,首先我们的目标是寻找从点i到点j的最短路径。从动态规划的角度看问题,我们需要为这个目标重新做一个诠释(这个诠释正是动态规划最富创造力的精华所在)。
从任意节点i到任意节点j的最短路径不外乎2种可能,1是直接从i到j,2是从i经过若干个节点k到j。所以,我们假设Dis(i,j)为节点u到节点v的最短路径的距离,对于每一个节点k,我们检查Dis(i,k) + Dis(k,j) < Dis(i,j)是否成立,如果成立,证明从i到k再到j的路径比i直接到j的路径短,我们便设置Dis(i,j) = Dis(i,k) + Dis(k,j),这样一来,当我们遍历完所有节点k,Dis(i,j)中记录的便是i到j的最短路径的距离。
2、算法分析
- 创建一个矩阵,记录两个顶点的权值。在DIjkstra算法中是记录一个顶点到其他顶点的路径长度,声明一个数组,此处是各个顶点,所以为矩阵。
- 创建一个矩阵,记录顶点到另个顶点路径的走法,这个后面会讲解。
- Floyd算法思想:对于每一对顶点 u 和 v,看看是否存在一个顶点 w 使得从 u 到 w 再到 v 比己知的路径更短。将权值鞠振宁更新,同样还有路径矩阵。
3、例图解释
(1)程序开始运行,第4-11行就是初始化了D和P,使得它们成为 上图 的两个矩阵。从矩阵也得到,v0->v1路径权值为1,v0->v2路径权值为5,v0->v3无边连线,所以路径权值为极大值65535。
(2)第12~25行,是算法的主循环,一共三层嵌套,k代表的就是中转顶点的下标。v代表起始顶点,w代表结束顶点。
(3)当k = 0时,也就是所有的顶点都经过v0中转,计算是否有最短路径的变化。可惜结果是,没有任何变化,如下图所示。
(4)当k = 1时,也就是所有的顶点都经过v1中转。此时,当v = 0 时,原本D[0][2] = 5,现在由于D[0][1] + D[1][2] = 4。因此由代码的的第20行,二者取其最小值,得到D[0][2] = 4,同理可得D[0][3] = 8、D[0][4] = 6,当v = 2、3、4时,也修改了一些数据,请看下图左图中虚线框数据。由于这些最小权值的修正,所以在路径矩阵P上,也要做处理,将它们都改为当前的P[v][k]值,见代码第21行。
(5)接下来就是k = 2,一直到8结束,表示针对每个顶点做中转得到的计算结果,当然,我们也要清楚,D0是以D-1为基础,D1是以D0为基础,......,D8是以D7为基础的。最终,当k = 8时,两个矩阵数据如下图所示。
至此,我们的最短路径就算是完成了。可以看到矩阵第v0行的数值与迪杰斯特拉算法求得的D数组的数值是完全相同。而且这里是所有顶点到所有顶点的最短路径权值和都可以计算出。
那么如何由P这个路径数组得出具体的最短路径呢?以v0到v8为例,从上图的右图第v8列,P[0][8]= 1,得到要经过顶点v1,然后将1取代0,得到P[1][8] = 2,说明要经过v2,然后2取代1得到P[2][8] = 4,说明要经过v4,然后4取代2,得到P[4][8]= 3,说明要经过3,........,这样很容易就推倒出最终的最短路径值为v0->v1->v2->v4->v3->v6->v7->v8。
4、示例代码
//Floyd算法 void GraphData::ShortPath_Floyd(GraphArray *pArray) { int i,j,m,k; int nNodeIndex[MAXVEX][MAXVEX]; int nNodeWeight[MAXVEX][MAXVEX]; for ( i = 0;i< pArray->numVertexes;i++) { for (j = 0;j< pArray->numVertexes;j++) { nNodeIndex[i][j] = j; /* 初始化 */ nNodeWeight[i][j] = pArray->arg[i][j]; /* [i][j]值即为对应点间的权值 */ } } for (i = 0;i< pArray->numVertexes;i++) { for (j = 0;j< pArray->numVertexes;j++) { for (k = 0;k<pArray->numVertexes;k++) { if (pArray->arg[j][k] > pArray->arg[j][i] + pArray->arg[i][k]) { /* 如果经过下标为k顶点路径比原两点间路径更短 */ nNodeWeight[j][k] = pArray->arg[j][i] + pArray->arg[i][k]; /* 将当前两点间权值设为更小的一个 */ nNodeIndex[j][k] = nNodeIndex[j][i]; /* 路径设置为经过下标为k的顶点 */ } } } } for (i = 0; i< pArray->numVertexes;i++) { for (j = 0;j< pArray->numVertexes;j++) { printf("v%d-v%d weight: %d",i,j,nNodeWeight[i][j]); m = nNodeIndex[i][j]; //获得第一个路径点的顶点下标 printf("path :%d",i); //打印源点 while(m!=j) { printf(" -> %d",m); //打印路径顶点 m = nNodeIndex[m][j]; //获取下一个路径顶点下标 } printf(" -> %d\n",m); //打印路径终点。 } printf("\n"); } }
5、代码分析
同样对于Dijkstra算法中,同样的邻接矩阵,我们最后发现其中v0到各个顶点数据与Dijkstra中数据一样奥,同时显示出路径中通过的顶点。