1、 算法描述
在一个如图1的有向图中,如何求源点到到其它各点的最短路径呢?
图1示
Dijkstra 提出了一个按路径长度递增的次序产生最路径的算法。
首先,引进一个辅助向量D,它的每个分量D[j]表示当前所找到的从始点v到每个
终点vi的最短路径的长度。它的初态为:若从v到vi有弧,则D[i]为弧上的权值;否则
置D[i]为无穷。显然,长度为
D[j]=Min{D[i]|vi属于V}
的路径就是从v出发的长度最短的一条最短路径。此路径为(v,vj)。
一般储况下,假设S为已求得最短路径的终点的集合,则可证明:下一条员短路径
(设其终点为x)或者是弧(v,x),或者是中间只经过S中的顶点而最后到达顶点x的路径。
我们用C语言描述为:
设邻接矩阵为如下所示,则在求从v0 到其它各点的最短路径中D向量的变化过程如下示:
完整的动态图如上所示
2、 对算法的完整实现
#define MAX_NAME 5 //顶点字符串的最大长度+1
#define MAX_INFO 20 //相关信息字符串的最大长度+1
#define MAX_VERTEX_NUM 20//最大顶点数
#include "string.h"
#include "ctype.h"
#include "malloc.h" //malloc etc
#include "limits.h" //INT_MAX etc
#include "stdio.h" //EOF(=^Z orF6),NULL
#include "stdlib.h" //atio()
//#include "math.h"//floor(),ceil(),abs()
#define TRUE 1
#define FALSE 0
#define OK 1
#define INFINITY INT_MAX //用整形最大值代替无穷
typedef int VRType;
typedef char InfoType;
typedef char VertexType[MAX_NAME];
typedef char InfoType;
typedef int Status;
typedef int Boolean;
typedef intPathMatrix[MAX_VERTEX_NUM][MAX_VERTEX_NUM];
typedef int ShortPathTable[MAX_VERTEX_NUM];
enum GraphKind {DG,DN,AG,AN};
typedef struct
{
VRType adj; //顶点关系类型,无权图,用1/0表示相邻否,对于带权图,则为权值类型。
InfoType *info;
}AdjMatrix[MAX_VERTEX_NUM][MAX_VERTEX_NUM],ArcCell;
typedef struct
{
VertexType vexs[MAX_VERTEX_NUM];//顶点向量,注意,这里vexs是二唯阵
AdjMatrix arcs; // 邻接矩阵
int vexnum,arcnum;//图的当前顶点数和弧数
GraphKind kind; //图的种类标志
}MGraph;
//这个定义包含了顶点的名字及个数,邻接矩阵的权值,顶点和弧的数目,及图的类型。
int LocateVex(MGraph G,VertexType u)
{
int i;
for(i=0;i<G.vexnum;++i)
if(strcmp(u,G.vexs[i])==0)
returni;
return-1;
}
Status CreateDN(MGraph &G)
{
int i,j,k,w;
VertexType va,vb;
printf("请输入有向图G的顶点数,弧数\n");
scanf("%d%d",&G.vexnum,&G.arcnum);
printf("请输入%d顶点的值(<%d个字符):\n",G.vexnum,MAX_NAME);
for(i=0;i<G.vexnum;++i)
scanf("%s",G.vexs[i]);//注意,这里G.vexs[i]是顶点字符串的首地址。
for(i=0;i<G.vexnum;++i)
for(j=0;j<G.vexnum;++j)//初始化邻接矩阵
{
G.arcs[i][j].adj=INFINITY; //任意两个顶点间距离为无穷大。
G.arcs[i][j].info=NULL;
}//for
printf("请输入%d条弧的弧尾 弧头 权值(以空格为间隔)\n",G.arcnum);
for(k=0;k<G.arcnum;++k)
{
scanf("%s%s%d%*c",va,vb,&w);//用%*c吃掉回车符
i=LocateVex(G,va); //注意,这里定义的是char va[5],也就是说va是首地址
j=LocateVex(G,vb);
G.arcs[i][j].adj=w;//有向网
}//for
G.kind=DN;
returnOK;
}
void ShortestPath_DIJ(MGraph G,intv0,PathMatrix &P,ShortPathTable &D)
{
//求有向网络G的v0到其它各个顶点v的最短路径P[v]及带权长度D[v]。
//如果P[v][w]是TRUE,则w是从v0到v当前求得最短路径上的顶点。
//final[v]是TRUE,当且仅当v属于S,即已经求得从v0到v的最短路径。
//D[i]或者是弧(V0到Vi)的权值,或者是D[k](Vk belong to S)和弧(Vk,Vi)上的权值之和。
//用final[]来标识集合S.
int w,v,i,j,min;
Status final[MAX_VERTEX_NUM];
for(v=0;v<G.vexnum;++v)
{
final[v]=FALSE;
D[v]=G.arcs[v0][v].adj; //用邻接矩阵初始化带权路径。
for(w=0;w<G.vexnum;++w)
P[v][w]=FALSE; //设空路径,开始时假设任何两点间都不存在路径。
if(D[v]<INFINITY)
{
P[v][v0]=TRUE; //表达式成立,说明v和v0间存在一条路径,指的是直接线,
//这句话的意思是从V0到V点,要经过V0,
P[v][v]=TRUE; //也要经过V点,很显然成立,所以初始化。
}//if 1
}//for 1
D[v0]=0;
final[v0]=TRUE; //初始化,v0顶点属于S集。
for(i=1;i<G.vexnum;++i) //求其它G.vexnum-1个顶点
{
//开始主循环,每次求得v0到某个顶点的最短路径,并add v to Set of S
min=INFINITY; //当前所知的离v0最近的距离
for(w=0;w<G.vexnum;++w)
if(!final[w]) //w 在V——S集中
if(D[w]<min) //存在一条路从V0到w.
{
v=w;
min=D[w];
}//if,经过这一轮循环,得出w离v0更近
final[v]=TRUE; //离v0最近的v加入S集中
for(w=0;w<G.vexnum;++w)//更新当前最短路径
if(!final[w]&&min<INFINITY&&G.arcs[v][w].adj<INFINITY&&(min+G.arcs[v][w].adj)<D[w])
//修改D[w] and P[w],w属于V——S
{
D[w]=min+G.arcs[v][w].adj;
for(j=0;j<G.vexnum;++j) //到v要走的路,到w也肯定要走
P[w][j]=P[v][j];
P[w][w]=TRUE; //到W当然要经过w了。 **
}//if of final[w] 1
}//for 1
}
int main()
{
int i,j,v0=0; //vo作为源点
MGraph g;
PathMatrix p;
ShortPathTable d;
CreateDN(g);
ShortestPath_DIJ(g,v0,p,d);
printf("最短路径数组P如下\n");
for(i=0;i<g.vexnum;++i)
{
for(j=0;j<g.vexnum;++j)
printf("%2d",p[i][j]);
printf("\n");
}//for
printf("%s到各顶点的最短路径长度是:\n",g.vexs[0]);
for(i=1;i<g.vexnum;++i)
printf("%s-%s:%d\n",g.vexs[0],g.vexs[i],d[i]);
}//main
3、 需要我们注意的地方
实现过程中要注意的细节,已在注意的注释中作了较为详尽的描述。
1) 算法的核心是找到当前最新的最短路径后,对所有不在S集中的顶点进行更新,好像虚拟出一条条直通路一样,使得V0 到其它可达的节点貌似有一条直通线,而由P[][]来记录中间经过的节点。
2) %*c 的应用
scanf("格式串" ,地址1,地址2,···);
其中的格式符这
syntax: %[*][width][h|l|L]type
其中的*号表示数据输入项要按指定格式进行转换,但不保存变量,即该%没有对应的变量。常用来吃掉其它字符.
width表示读入多少个字符就结束本数据项的转换。如果没有指定width,则遇到空格 、TAB键、回车/换行符、非法输入则结束数据项的转换(%c格式除外)。
比如:
scanf("%d%*c%c%f",&age,&sex,&height);
如输入为:19 W 1.62 /*正确输入,9和W之间只能间隔一个空格,W和1.62之间允许多个空格*/
注意,一个%*c只能吸收掉一个字符。
3) 字符数组及字符串
(1)字符数组定义格式如下:
类型名 数组名[数组元素个数];
其中的“类型名”必须是char
(2)初始化
1)按单个字符的方式赋初值,其中必须有一个字符是字符串的结束标记。例如:
char s[]={'1', '2','3','\0'};
如果花括号中数据元素的个数小于字符数组定义的数据元素的个数, C语言规定:初始化中未给出数据元素值得对应元素被自动赋值为空字符'\0'。
2)直接在初值表中写一个字符串常量。例如:
char s[]={"123"};
其中的“{}”可以省略,如:
char s[]="123";
当需要处理多个字符串时,可以采用二维数组。如
chars[3][4]={"123","ab","A"}; 此时s[0],s[1],s[2]分别为指向字符串的指针了。
(3)输入/输出
关于字符数组的输入/输出,C语言有如下规定:
(A)逐个字符输入/输出。用格式符"%c"输入/输出一个字符。
(B)将整个字符串一次性输入/输出。用格式符"%s"对整个字符串进行一次性输入/输出。
注意:使用"%s"格式从键盘上向字符数组中输入字符串时,回车换行符或空格符号均作为字符串的结束标记。当用scanf函数输入字符串时,字符串中不能含有空格,否则将以空格作为串的结束符。
(C)scanf(“格式串”,字符串首地址)
printf(“%s”,字符串首地址)
gets(字符串首地址).
(D)char s[5];
可以用printf(“%s”,s)来输出,也可以用for(i=0;i<strlen(s);i++) printf(“%c”,s[i])来输出。
(4) 涉及的函数
字符串输出函数puts
字符串输入函数gets
测试字符串长度函数strlen
字符串连接函数strcat
字符串比较函数strcmp
字符串拷贝函数strcpy
小写变大写函数strupr
大写变小写函数strlwr
4、算法的改进:
我们不仅要求记录从源节点到达目的节点的最短路径,我们还要求记录从源节点到达目的节点当中所走过路的先后顺序;此时,在源程序
P[w][w]=TRUE; //到W当然要经过w了。 **
这一句后面插入如下函数 void InsertNode(int v, int w)即可。对于InsertNode函数的定义如下所示:
void InsertNode(int v, int w)
{
int i,j,Rec;
for(i=0;Path[w][i] != -1;i++)
Path[v][i]=Path[w][i];
Rec=i;
for(j=Rec;j<20;j++)
Path[v][j]=-1;
Path[v][Rec]=w;
}