关于图的基础知识和图的存储相关实现,以及数据结构与算法的其他相关知识,请查看文章《数据结构与算法基础知识文章汇总》。
一、图的最短路径
图的最短路径
是指一个连通图
中,任意两个顶点
之间的连接路径中,连接所经过的边的权值和
最小的路径。
本文章讲解图的最短路径的两种求法:Dijkstra算法
和Floyd算法
。
二、Dijkstra算法
1.算法思路
第一步:从V0
出发,与V0连接的有V1和V2
,边的权值分别为1和5
,由于1比5小
,所以选择V1
,记录此时所经过边的权值和为sum = 1,标记V0、V1已经加入了最短路径
第二步:与V1
相连的顶点有V2、V3、V4
,边的权值为3、5、7
,最小
为3,所以选择顶点V2
,sum + 3 = 4,记录V2已经加入最短路径
第三步:与V2
相连的顶点为V4、V5
,边的权值为1和7
,最小
为1,所以选择顶点V4,sum+1 = 5,记录V4已经加入最短路径
第四步:与V4
相连且没有加入最短路径的顶点V3、V5、V6、V7
,边的权值为2、3、6、9
,最小
为2,所以选择V3
,sum + 2 = 7,记录V3已经加入了最短路径
第五步:与V3
相连且没有加入最短路径的顶点为V6
,边的权值为3,只能选择V6
,sum + 3 = 10,记录V6已经加入了最短路径
第六步:与V6
相连且没有加入最短路径的顶点为V7、V8
,边的权值为4、7
,最小
为4,所以选择V7
,sum + 2 = 12,记录V7已经加入了最短路径
第七步:与
V7
相连的且没有加入最短路径的顶点为V5、V8
,边的权值为5、4
,最小
为4,所以选择V4
,sum + 4 = 16,记录V8已经加入了最短路径。
经过上面的步骤,我们最终得到了从
V0到V8
的最短路径
和权值和
为:
- 最短路径:V0 -> V1 -> V2 -> V4 -> V3 -> V6 -> V7 -> V8;
- 权值和:1 + 3 + 1 + 2 + 3 + 2 + 4 = 16。
虽然我们最终得到了最终的正确结果,但是上面的分析过程还是存在着一些漏洞
的,比如:
- 1.如果V7与V5的权值小于V7与V8的权值呢?
- 2.如果V6与V8的权值小于V6与V7的权值+V7与V8的权值呢?
- 3.你的思考是什么?
其实前面的分析只是大概讲述了Dijkstra算法
的思路,不是真实的过程,之所以讲这个思路是为了让读者对Dijkstra的算法思路有一个初步的认识
。下面我们进一步讲解Dijkstra算法的实现逻辑。
2.代码逻辑
1.首先使用邻接矩阵的顺序存储将图存储在内存中
2.设计3个数组来实现算法思路
中的求解步骤,在求解过程中会更新这3个数组
- 1.
final数组
:表示V0到顶点Vw是否已经
求得了最短路径的标记
,如果已经求得结果,则标记final[w] = 1
;final数组的初始化所有元素都为0。- 2.
D数组
:V0到顶点Vw所经过的所有边的权值和
的最小值
,在求解的过程中,这个最小值会不断的更新
,遇到比当前所存的值更小的值就更新。D数组的初始值为V0在邻接矩阵中对应的边表数组,因为刚开始时,只能通过边表数组来确定与V0相连的顶点的权值。
- 3.
P数组
:P数组的索引值表示图中所有顶点的下标值
,P数组中的值表示索引对应的顶点的前驱顶点的下标,P数组初始值全为0,表示所有顶点的前驱都为0.
如上图,V0
没有前驱
,所以P[0] = -1
,V1的前驱是V0
,所以P[1] = 0
,V2、V3、V4的前驱是V1
,所以P[2]、P[3]、P[3] = 1
,虽然此时三个顶点的前驱都为1,但是在最短路径中一个顶点只能是另外唯一一个顶点的前驱,所以P数组会在算法求解的过程为进行更新
,请认准最终结果。
3.代码实现
1.定义一些状态值和数据类型
#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0
#define MAXEDGE 20
#define MAXVEX 20
#define INFINITYC 65535
typedef int Status;
//用于存储最短路径下标的数组
typedef int Patharc[MAXVEX];
//用于存储到各点最短路径权值的和
typedef int ShortPathTable[MAXVEX];
复制代码
2.邻接矩阵顺序存储的数据结构设计
typedef struct
{
int vexs[MAXVEX];//顶点数组
int arc[MAXVEX][MAXVEX];//邻接矩阵:边表数组
int numVertexes, numEdges;//顶点数、边数
}MGraph;
复制代码
3.邻接矩阵的顺序存储实现
void CreateMGraph(MGraph *G)
{
int i, j;
G->numEdges=16;
G->numVertexes=9;
for(i = 0; i < G->numVertexes; i++)
{
G->vexs[i]=i;
}
for(i = 0; i < G->numVertexes; i++)
{
for( j = 0; j < G->numVertexes; j++)
{
if(i==j)
G->arc[i][j]=0;
else
G->arc[i][j] = G->arc[j][i] = INFINITYC;
}
}
G->arc[0][1]=1;
G->arc[0][2]=5;
G->arc[1][2]=3;
G->arc[1][3]=7;
G->arc[1][4]=5;
G->arc[2][4]=1;
G->arc[2][5]=7;
G->arc[3][4]=2;
G->arc[3][6]=3;
G->arc[4][5]=3;
G->arc[4][6]=6;
G->arc[4][7]=9;
G->arc[5][7]=5;
G->arc[6][7]=2;
G->arc[6][8]=7;
G->arc[7][8]=4;
for(i = 0; i < G->numVertexes; i++)
{
for(j = i; j < G->numVertexes; j++)
{
G->arc[j][i] =G->arc[i][j];
}
}
}
复制代码
4.Dijkstra算法代码实现
/*10.2 求得网图中2点间最短路径
Dijkstra 算法
G: 网图;
v0: V0开始的顶点;
p[v]: 前驱顶点下标;
D[v]: 表示从V0到V的最短路径长度和;
*/
void ShortestPath_Dijkstra(MGraph G, int v0, Patharc *P, ShortPathTable *D)
{
int v,w,k,min;
k = 0;
//final[w] = 1 表示已经求得顶点V0~Vw的最短路径
int final[MAXVEX];
//1.初始化数据
for(v=0; v<G.numVertexes; v++)
{
//全部顶点初始化为未知最短路径状态0
final[v] = 0;
//将V0的边表数组(与V0有连接的顶点的权值)存入D数组
(*D)[v] = G.arc[v0][v];
//初始化路径数组p = 0
(*P)[v] = 0;
}
//V0到V0的路径为0
(*D)[v0] = 0;
//记录V0到V0已经求过最短路径了
final[v0] = 1;
//v0没有前驱顶点,记录为-1
(*P)[v0] = -1;
//2. 开始主循环,有numVertexes个顶点,所以遍历numVertexes-1次
for(v = 1; v < G.numVertexes; v++)
{
//与V0有连接的顶点的边中的最小权值
min=INFINITYC;
//3.寻找与V0有连接的顶点中权值最小的边和对应的顶点
for(w=0; w<G.numVertexes; w++)
{
if(!final[w] && (*D)[w]< min)
{
k=w;
//w顶点距离V0顶点更近
min = (*D)[w];
}
}
//将目前找到最近的顶点标记为1;
final[k] = 1;
//4.遍历与k有连接的所有顶点,如果它们的权值小于D数组中原有对应索引的权值,则更新,并记录w的前驱结点是k
for(w=0; w<G.numVertexes; w++)
{
//如果经过v顶点的路径比现在这条路径长度短,则更新
if(!final[w] && (min + G.arc[k][w]<(*D)[w]))
{
//找到更短路径, 则修改D[W],P[W]
//修改当前路径的长度
(*D)[w] = min + G.arc[k][w];
(*P)[w] = k;//w的前驱是k
}
}
}
}
复制代码
5.调试代码
int main(void)
{
printf("最短路径-Dijkstra算法\n");
int i,j,v0;
MGraph G;
Patharc P;
ShortPathTable D;
v0 = 0;
CreateMGraph(&G);
ShortestPath_Dijkstra(G, v0, &P, &D);
printf("最短路径路线:\n");
for(i=1;i<G.numVertexes;++i)
{
printf("v%d -> v%d : ",v0,i);
j = i;
while(P[j] != -1)
{
printf("%d ",P[j]);
j = P[j];
}
printf("\n");
}
printf("\n最短路径权值和\n");
for(i=1;i<G.numVertexes;++i)
printf("v%d -> v%d : %d \n",G.vexs[0],G.vexs[i],D[i]);
printf("\n");
return 0;
}
复制代码
执行结果:
通过Dijkstra算法
,最终求得V0
到任一
顶点之间的最短路径
。这个算法的局限性
在于,你需要在邻接矩阵存储数据时,把指定某个顶点作为第0个
内存的数据,而且只能求指定顶点
到任意
顶点之间的最短路径
。后面介绍的Floyd算法
就能求解出任意两个顶点
之间的最短路径
。
4.执行过程
第一次执行
代码执行时数据的变化 图中最短路径的变化
第二次执行
代码执行时数据的变化
图中最短路径的变化
第三次执行
代码执行时数据的变化
图的最短路径的变化
第四次执行
代码执行时数据的变化
图的最短路径的变化
第五次执行
代码执行时数据的变化
图的最短路径的变化
第六次执行
代码执行时数据的变化
图的最短路径的变化
第七次执行
代码执行时数据的变化
图的最短路径的变化
第八次执行
代码执行时数据的变化 图的最短路径的变化
最终得到的D数组和P数组的数据为:
5.小结
- 1.
Dijkstra算法
只能求解指定的顶点
到其他任意顶点
的最短路径和对应的权值;- 2.
Dijkstra算法
所求的指定的顶点
的数据必须存储在邻接矩阵的第0个
内存的位置。
三、Floyd算法
1.算法思路
同样还是这个图,现在通过邻接矩阵的顺序存储到内存中
- 1.首先我们知道,
连通图
中的任意顶点
都可以通过有限条边与图中的其他任何一个顶点相连
;- 2.邻接矩阵描述是的任意
两个顶点
之间的直接连接关系,顶点自己与自己
相连时,边的权值为0
;顶点与顶点
有连接时,边的权值是一个大于0的非无穷大
的数值;当顶点与顶点之间没有
直接连接时,边的权值为无穷大
;- 3.有了
1和2
两个条件,那么对于图中的任意两个
顶点都可以通过另外的任意一个
顶点或多个相连
的顶点连接;当两个顶点有直接连接时,它们的边的权值
有可能大于
通过另外一个顶点
或相连的顶点
连接的边的权值和
。假如我们可以通过
另外的一个或多个
顶点来到目标
顶点,并且所经历的边的权值和
比直接到对应顶点的边的权值更小
,那么我们就可以选择更优的路径
了,举例如下:
假设现在只有上图中V0、V1、V2三个顶点,它在邻接矩阵中的存储如左上图;由图的可知,直接从V1到V2的边的权值为5,在邻接矩阵中的体现是D[1][2] = 5;而从V1到V0再到V2所经历的权值和为2+1 = 3,3比5小,于是当需要求解V1到V2的最短路径时,我们就可以选择从V1->V0->V2了。
- 4.在3思路的基础上,对于上面完整的图的最短路径的求解,我们就可以通过图中的任意一个顶点作为中间顶点,分别求解图中的任意两个顶点之间的边的权值和,如果比之前方案的权值和更小,就可以更新这两个顶点之间边的权值了;
- 5.当遇到遇到无穷大时,说明两个顶点之间不用通过另外的顶点相连,此时我们无需更新两个顶点的边的权值。
通过上面的分析,我们需要确定的是,如果比较两个顶点之间的权值和是否小于通过另外一个或多个顶点相连的权值和,这里提供如下公式
:
顶点
v
和顶点w
之间的权值和
,取顶点v和顶点w的权值,与顶点v和顶点O的权值 + 顶点O和顶点w的权值的较小值
。
2.代码实现思路
- 1.设计
二维数组
D,用来记录顶点之间的权值
,它的初始值设置为图的邻接矩阵的数据;在算法实现的过种中,会不断的更新
,最终得到的结果即为任意两个顶点之间的最短路径的权值和;- 2.设计一个
二维数组
P,用来记录
任意两个顶点之间最短路径
的顶点下标
;它的初始值每一列都是对应的列值,表示列对应的顶点与其他顶点之间都是通过列对应的顶点相连的,即没有相连。
3.代码实现
1.图的存储
在Dijkstra算法
以中已经介绍了邻接矩阵顺序存储
的实现
2.D和P数组的定义
typedef int Patharc[MAXVEX][MAXVEX];
typedef int ShortPathTable[MAXVEX][MAXVEX];
复制代码
3.Floyd算法的实现
void ShortestPath_Floyd(MGraph G, Patharc *P, ShortPathTable *D)
{
int v,w,k;
//1.初始化D与P 矩阵
for(v=0; v<G.numVertexes; ++v)
{
for(w=0; w<G.numVertexes; ++w)
{
//D[v][w]值即为邻接矩阵的边表数组的值
(*D)[v][w]=G.arc[v][w];
//初始化P P[v][w] = w
(*P)[v][w]=w;
}
}
//2.k表示经过的中转顶点
for(k=0; k<G.numVertexes; ++k)
{
for(v=0; v<G.numVertexes; ++v)
{
for(w=0; w<G.numVertexes; ++w)
{
//如果经过下标为k顶点路径比原两点间路径更短
if ((*D)[v][w] > (*D)[v][k] + (*D)[k][w])
{
//将当前两点间权值设为更小的一个
(*D)[v][w] = (*D)[v][k] + (*D)[k][w];
//路径设置为经过下标为k的顶点
(*P)[v][w] = (*P)[v][k];
}
}
}
}
}
复制代码
4.调式代码
int main(void)
{
printf("Hello,最短路径弗洛伊德Floyd算法");
int v,w,k;
MGraph G;
Patharc P;
ShortPathTable D; //求某点到其余各点的最短路径
CreateMGraph(&G);
ShortestPath_Floyd(G,&P,&D);
//打印所有可能的顶点之间的最短路径以及路线值
printf("各顶点间最短路径如下:\n");
for(v=0; v<G.numVertexes; ++v)
{
for(w=v+1; w<G.numVertexes; w++)
{
printf("v%d-v%d weight: %d ",v,w,D[v][w]);
//获得第一个路径顶点下标
k=P[v][w];
//打印源点
printf(" path: %d",v);
//如果路径顶点下标不是终点
while(k!=w)
{
//打印路径顶点
printf(" -> %d",k);
//获得下一个路径顶点下标
k=P[k][w];
}
//打印终点
printf(" -> %d\n",w);
}
printf("\n");
}
//打印最终变换后的最短路径D数组
printf("最短路径D数组\n");
for(v=0; v<G.numVertexes; ++v)
{
for(w=0; w<G.numVertexes; ++w)
{
printf("%d\t",D[v][w]);
}
printf("\n");
}
//打印最终变换后的最短路径P数组
printf("最短路径P数组\n");
for(v=0; v<G.numVertexes; ++v)
{
for(w=0; w<G.numVertexes; ++w)
{
printf("%d ",P[v][w]);
}
printf("\n");
}
return 0;
}
复制代码
5.执行结果
通过上面的算法,最终得到的D数组和P数组的值为:
4.小结
Floyd算法
求解了任意
两个顶点的最短路径
。
四、总结
图的最短路径
的求合算法有Dijkstra算法
和Floyd算法
。两种算法的理解难度都很大,读者可以多多体会。