图的最短路径拓扑排序和关键路径.docx
《图的最短路径拓扑排序和关键路径.docx》由会员分享,可在线阅读,更多相关《图的最短路径拓扑排序和关键路径.docx(24页珍藏版)》请在冰豆网上搜索。
![图的最短路径拓扑排序和关键路径.docx](https://file1.bdocx.com/fileroot1/2022-12/31/a141cb46-5524-4858-82ee-e0ffa28abb15/a141cb46-5524-4858-82ee-e0ffa28abb151.gif)
图的最短路径拓扑排序和关键路径
数据结构课程辅导
---图的最短路径、拓扑排序和关键路径
一、最短路径
由图的概念可知,在一个图中,若从一顶点到另一顶点存在着一条路径(这里只讨论无回路的简单路径),则称该路径长度为该路径上所经过的边的数目,它也等于该路径上的顶点数减1。
由于从一顶点到另一顶点可能存在着多条路径,每条路径上所经过的边数可能不同,即路径长度不同,我们把路径长度最短(即经过的边数最少)的那条路径叫做最短路径,其路径长度叫做最短路径长度或最短距离。
上面所述的图的最短路径问题只是对无权图而言的,若图是带权图,则把从一个顶点i到图中其余任一个顶点j的一条路径上所经过边的权值之和定义为该路径的带权路径长度,从vi到vj可能不止一条路径,我们把带权路径长度最短(即其值最小)的那条路径也称作最短路径,其权值也称作最短路径长度或最短距离。
例如,在图3-1中,从v0到v4共有三条路径:
{0,4},{0,1,3,4}和{0,1,2,4},其带权路径长度分别为30,23和38,可知最短路径为{0,1,3,4},最短距离为23。
图3-1带权图和对应的邻接矩阵
实际上,这两类最短路径问题可合并为一类,这只要把无权图上的每条边标上数值为1的权就归属于有权图了,所以在以后的讨论中,若不特别指明,均认为是求带权图的最短路径问题。
求图的最短路径问题用途很广。
例如,若用一个图表示城市之间的运输网,图的顶点代表城市,图上的边表示两端点对应城市之间存在着运输线,边上的权表示该运输线上的运输时间或单位重量的运费,考虑到两城市间的海拔高度不同,流水方向不同等因素,将造成来回运输时间或运费的不同,所以这种图通常是一个有向图。
如何能够使从一城市到另一城市的运输时间最短或者运费最省呢?
这就是一个求两城市间的最短路径问题。
求图的最短路径问题包括两个方面:
一是求图中一顶点到其余各顶点的最短路径,二是求图中每对顶点之间的最短路径。
下面分别进行讨论。
1.从一顶点到其余各顶点的最短路径
对于一个具有n个顶点和e条边的图G,从某一顶点vi(称此为源点)到其余任一顶点vj(称此为终点)的最短路径,可能是它们之间的边(i,j)或,也可能是经过k个(1≤k≤n-2,最多经过除源点和终点之外的所有顶点)中间顶点和k+1条边所形成的路径。
例如在图3-1中,从v0到v1的最短路径就是它们之间的有向边<0,1>,其长度为3;从v0到v4的最短路径经过两个中间点v1和v3以及三条有向边<0,1>,<1,3>和<3,4>,其长度为23。
那么,如何求出从源点i到图中其余每一个顶点的最短路径呢?
狄克斯特拉(Dijkstra)于1959年提出了解决此问题的一般算法,具体做法是按照从源点到其余每一顶点的最短路径长度的升序依次求出从源点到各顶点的最短路径及长度,每次求出从源点i到一个终点m的最短路径及长度后,都要以该顶点m作为新考虑的中间点,用vi到vm的最短路径和最短路径长度对vi到其它尚未求出最短路径的那些终点的当前最短路径及长度作必要地修改,使之成为当前新的最短路径和最短路径长度,当进行n-2次(因最多考虑n-2个中间点)后算法结束。
狄克斯特拉算法需要设置一个集合,假定用S表示,其作用是保存已求得最短路径的终点序号,它的初值中只有一个元素,即源点i,以后每求出一个从源点i到终点m的最短路径,就将该顶点m并入S集合中,以便作为新考虑的中间点;还需要设置一个整型(假定权值类型为整型)数组dist[MaxVertexNum],该数组中的第j个元素dist[j]用来保存从源点i到终点j的目前最短路径长度,它的初值为(i,j)或边上的权值,若vi到vj没有边,则权值为MaxValue,以后每考虑一个新的中间点时,dist[j]的值可能变小;另外,再设置一个与dist数组相对应的、类型为adjlist的邻接表表头向量数组path,该数组中的第j个元素path[j]指向一个单链表,该单链表中保存着从源点i到终点j的目前最短路径,即一个顶点序列,当vi到vj存在着一条边时,则path[j]初始指向由顶点i和j构成的单链表,否则path[j]的初值为空。
此算法的执行过程是:
首先从S集合以外的顶点(即待求出最短路径的终点)所对应的dist数组元素中,查找出其值最小的元素,假定为dist[m],该元素值就是从源点i到终点m的最短路径长度(证明从略),对应path数组中的元素path[m]所指向的单链表链接着从源点i到终点m的最短路径,即经过的顶点序列或称边序列;接着把已求得最短路径的终点m并入集合S中;然后以vm作为新考虑的中间点,对S集合以外的每个顶点j,比较dist[m]+GA[m][j](GA为图G的邻接矩阵)与dist[j]的大小,若前者小于后者,表明加入了新的中间点vm之后,从vi到vj的路径长度比原来变短,应用它替换dist[j]的原值,使dist[j]始终保持到目前为止最短的路径长度,同时把path[m]单链表复制到path[j]上,并在其后插入vj结点,使之构成从源点i到终点j的目前最短路径。
重复n-2次上述运算过程,即可在dist数组中得到从源点i到其余每个顶点的最短路径长度,在path数组中得到相应的最短路径。
为了简便起见,可采用一维数组s[n]来保存已求得最短路径的终点的集合S,具体做法是:
若顶点j在集合S中,则令数组元素s[j]的值取1,否则取0。
这样,当判断一个顶点j是否在集合S以外时,只要判断对应的数组元素s[j]是否为0即可。
例如,对于图3-1来说,若求从源点v0到其余各顶点的最短路径,则开始时三个一维数组s,dist和path的值为:
1
0
0
0
0
0
3
∞
∞
30
v0,v1
v0,v4
01234
s
dist
path
下面开始进行第一次运算,求出从源点v0到第一个终点的最短路径。
首先从s元素为0的对应dist元素中,查找出值最小的元素,求得dist[2]的值最小,所以第一个终点为v1,最短距离为dist[2]=3,最短路径为path[2]={0,1},接着把s[1]置为1,表示v1已加入S集合中,然后以v1为新考虑的中间点,对s数组中元素为0的每个顶点j(此时2≤j≤4)的目前最短路径长度dist[j]和目前最短路径path[j]进行必要地修改,因dist[1]+GA[1][2]=3+25=28,小于dist[2]=∞,所以将28赋给dist[2],将path[1]并上v2后赋给path[2],同理因dist[1]+GA[1][3]=3+8=11,小于dist[3]=∞,所以将11赋给dist[3],将path[1]并上v3后赋给path[3],最后再看从v0到v4,以v1作为新考虑的中间点的情况,由于v1到v4没有出边,所以GA[1][4]=∞,故dist[1]+GA[1][4]不小于dist[4],因此dist[4]和path[4]无需修改,应维持原值。
至此,第一次运算结束,三个一维数组的当前状态为:
01234
1
1
0
0
0
0
3
28
11
30
v0,v1
v0,v1,v2
v0,v1,v3
v0,v4
s
dist
path
下面开始进行第二次运算,求出从源点v0到第二个终点的最短路径。
首先从s数组中元素为0的对应dist元素中,查找出值最小的元素,求得dist[3]的值最小,所以第二个终点为v3,最短距离为dist[3]=11,最短路径为path[3]={0,1,3},接着把s[3]置为1,然后以v3作为新考虑的中间点,对s中元素为0的每个顶点j(此时j=3,5)的dist[j]和path[j]进行必要的修改,因dist[3]+GA[3][2]=11+4=15,小于dist[2]=28,所以将15赋给dist[2],将path[3]并上v2后赋给path[2],同理,因dist[3]+GA[3][4]=11+12=23,小于dist[4]=30,所以将23赋给dist[4],将path[3]并上v4后赋给path[4]。
至此,第二次运算结束,三个一维数组的当前状态为:
01234
1
1
0
1
0
0
3
15
11
23
v0,v1
v0,v1,v3,v2
v0,v1,v3
v0,v1,v3,v4
s
dist
path
下面开始进行第三次运算,求出从源点v0到第三个终点的最短路径。
首先从s中元素为0的对应dist元素中,查找出值最小的元素为dist[2],所以求得第三个终点为v2,最短距离为dist[2]=15,最短路径为path[2]={0,1,3,2},接着把s[2]置为1,然后以v2作为新考虑的中间点,对s中元素为0的每个顶点j(此时只有v4一个)的dist[j]和path[j]进行必要的修改,因dist[2]+GA[2][4]=15+10=25,大于dist[4]=23,所以无需修改,原值不变。
至此,第三次运算结束,三个一维数组的当前状态为:
1
1
1
1
0
0
3
15
11
23
v0,v1
v0,v1,v3,v2
v0,v1,v3
v0,v1,v3,v4
01234
s
dist
path
由于图中有5个顶点,只需运算3次,即n-2次,虽然此时还有一个顶点未加入S集合中,但它的最短路径及最短距离已经最后确定,所以整个运算结束。
最后在dist中得到从源v0到每个顶点的最短路径长度,在path中得到相应的最短路径。
如果用图形表示上述过程中每次运算的结果,则对应的图形分别如图3-2(b)~(e)所示,其中实线有向边所指向的顶点为集合S中的顶点,虚线有向边所指向的顶点为集合S外的顶点;S集合中的顶点上所标数值为从源点v0到该顶点的最短路径长度,从源点v0到该顶点所经过的有向边为从v0到该顶点的最短路径;S集合外的顶点上所标数值为从源点v0到该顶点的目前最短路径长度,从v0到该顶点所经过的有向边为从v0到该顶点的目前最短路径。
为了便于对照分析,把图3-1(a)重画于图3-2(a)中。
图3-2利用狄克斯特拉算法求最短路径的图形说明
根据以上分析和举例,不难给出狄克斯特拉算法的描述:
voidDijkstra(adjmatrixGA,intdist[],
adjlistpath,inti,intn)
//利用狄克斯特拉算法求图GA中从顶点i到其余每个顶点间的
//最短距离和最短路径,它们分别被存于数组dist和path数组中
{
intj,k,w,m;
//定义作为集合使用的动态数组s
int*s=newint[n];
//分别给s,dist和path数组赋初值
for(j=0;jif(j==i)
s[j]=1;
else
s[j]=0;
dist[j]=GA[i][j];
if(dist[j]=i){
edgenode*p1=newedgenode;
edgenode*p2=newedgenode;
p1->adjvex=i;
p2->adjvex=j;
p2->next=NULL;
p1->next=p2;
path[j]=p1;
}
else
path[j]=NULL;
}
//共进行n-2次循环,每次求出从源点i到终点m的最短路径及长度
for(k=1;k<=n-2;k++)
{
//求出第k个终点m
w=MaxValue;m=i;
for(j=0;jif(s[j]==0&&dist[j]w=dist[j];
m=j;
}
//若条件成立,则把顶点m并入集合S中,否则退出循环,因为剩余
//的顶点,其最短路径长度均为MaxValue,无需再计算下去
if(m!
=i)
s[m]=1;
else
break;
//对s元素为0的对应dist和path中的元素作必要修改
for(j=0;jif(s[j]==0&&dist[m]+GA[m][j]dist[j]=dist[m]+GA[m][j];
PATH(path,m,j);//调用此函数,由到顶点m的
//最短路径和顶点j构成到顶点j的目前最短路径
}
}
}
PATH函数的定义如下:
voidPATH(adjlistpath,intm,intj)
//由到顶点m的最短路径和顶点j构成到顶点j的目前最短路径
{
edgenode*p,*q,*s;
//把顶点j的当前最短路径清除掉
p=path[j];
while(p!
=NULL){
path[j]=p->next;
deletep;
p=path[j];
}
//把到顶点m的最短路径拷贝过来到顶点j的最短路径上
p=path[m];
while(p!
=NULL){
q=newedgenode;
q->adjvex=p->adjvex;
if(path[j]==NULL)
path[j]=q;
else
s->next=q;
s=q;
p=p->next;
}
//把顶点j加入到path[j]单链表的最后,形成新的目前最短路径
q=newedgenode;
q->adjvex=j;
q->next=NULL;
s->next=q;
}
2.每对顶点之间的最短路径
求图中每对顶点之间的最短路径是指把图中任意两个顶点vi和vj(i≠j)之间的最短路径都计算出来。
若图中有n个顶点,则共需要计算n(n-1)条最短路径。
解决此问题有两种方法:
一是分别以图中的每个顶点为源点共调用n次狄克斯特拉算法,因狄克斯特拉算法的时间复杂性为O(n2),所以此方法的时间复杂性为O(n3);二是采用下面介绍的弗洛伊德(Floyed)算法,此算法的时间复杂性仍为O(n3),但比较简单。
弗洛伊德算法从图的邻接矩阵开始,按照顶点v0,v1,…,vn-1的次序,分别以每个顶点vk(0≤k≤n-1)作为新考虑的中间点,在第k-1次运算得到的A(k-1)(A(-1)为图的邻接矩阵GA)的基础上,求出每对顶点vi到vj的目前最短路径长度A(k)[i][j],计算公式为:
A(k)[i][j]=min(A(k-1)[i][j],A(k-1)[i][k]+A(k-1)[k][j])
(0≤i≤n-1,0≤j≤n-1)
其中min函数表示取其参数表中的较小值,参数表中的前项表示在第k-1次运算后得到的从vi到vj的目前最短路径长度,后项表示考虑以vk作为新的中间点所得到的从vi到vj的路径长度。
若后项小于前项,则表明以vk作为中间点(不排除已经以v0,v1,…,vk-1中的一部分或全部作为其中间点)使得从vi到vj的路径长度变短,所以应把它的值赋给A(k)[i][j],否则把A(k-1)[i][j]的值赋给A(k)[i][j]。
总之,使A(k)[i][j]保存第k次运算后得到的从vi到vj的目前最短路径长度。
当k从0取到n-1后,矩阵A(n-1)就是最后得到的结果,其中每个元素A(n-1)[i][j]就是从顶点vi到vj的最短路径长度。
对于上面的计算公式,当i=j时变为:
A(k)[i][i]=min(A(k-1)[i][i],A(k-1)[i][k]+A(k-1)[k][i])(0≤i≤n-1)
若k=0,则参数表中的前项A(-1)[i][i]=GA[i][i]=0,后项A(-1)[i][0]+A(-1)[0][i]必定大于等于0,所以A(0)中的对角线元素同A(-1)中的对角线元素一样,均为0。
同理,当k=1,2,…,n-1时,A(k)中的对角线元素也均为0。
对于上面的计算公式,当i=k或j=k时分别变为:
A(k)[k][j]=min(A(k-1)[k][j],A(k-1)[k][k]+A(k-1)[k][j])(0≤j≤n-1)
A(k)[i][k]=min(A(k-1)[i][k],A(k-1)[i][k]+A(k-1)[k][k])(0≤i≤n-1)
每个参数表中的后一项都由它的前一项加上A(k-1)[k][k]所组成,因A(k-1)[k][k]=0,所以A(k)[k][j]和A(k)[i][k]分别取上一次的运算结果A(k-1)[k][j]和A(k-1)[i][k]的值,也就是说,矩阵A(k)中的第k行和第k列上的元素均取上一次运算的结果。
下面以求图3-3(a)中每对顶点之间的最短路径长度为例来说明弗洛伊德算法的运算过程。
图3-3弗洛伊德算法求最短路径的运算过程。
(1)令k取0,即以v0作为新考虑的中间点,对图3-3(b)所示A(-1)中的每对顶点之间的路径长度进行必要的修改后得到第0次运算结果A(0),如图3-3(c)所示。
在A(0)中,第0行和第0列用虚线框起来表示i=k和j=k的情况,它们同对角线上的元素一样为A(-1)中的对应值,对于其它六个元素,若vi通过新中间点v0然后到vj的路径长度A(-1)[i][0]+A(-1)[0][j]小于原来的路径长度A(-1)[i][j],则用前者修改之,否则仍保持原值。
因v2到v1的路径长度A(-1)[2][1]=5,通过新中间点v0后变短,即为A(-1)[2][0]+A(-1)[0][1]=3+1=4,所以被修改为4,对应的路径为{2,0,1};同样,v2到v3的路径长度通过新中间点v0后也由8变为7,所以被修改为7,对应的路径为{2,0,3};剩余的四对顶点的路径长度,因加入v0作为新中间点后仍不变短,所以保持原值不变。
(2)令k=1,即以v1作为新考虑的中间点,对A(0)中每对顶点之间的路径长度进行必要的修改后得到第1次运算结果A
(1),如图3-3(d)所示。
此时第1行和第1列同对角线的元素一样,取上一次的值,对于其它六个元素,若vi通过新中间点v1然后到vj的路径长度A(0)[i][1]+A(0)[1][j]小于原来的路径长度A(0)[i][j],则用前者修改之,否则仍保持原值。
因v0到v2的路径长度A(0)[0][2]=∞,通过新中间点v1后变短,即为A(0)[0][1]+A(0)[1][2]=1+9=10,所以被修改为10,对应的路径为{0,1,2};v0到v3的路径长度A(0)[0][3]=4,通过新中间点v1后变短,即为A(0)[0][1]+A(0)[1][3]=1+2=3,所以也被修改为3,对应的路径为{0,1,3};v2到v3的路径长度A(0)[2][3]=7,通过新中间点v1后也变短,即为A(0)[2][1]+A(0)[1][3]=4+2=6,所以在第一次被修改的基础上又重新被修改为6,对应的路径为A(0)[2][1]的路径{2,0,1}并上A(0)[1][3]的路径{1,3},即为{2,0,1,3};剩余三对顶点的路径长度,因加入新中间点v1后不变短,所以仍保持原值不变。
(3)令k=2,即以v2作为新考虑的中间点,对A
(1)中每对顶点的路径长度进行必要地修改,得到第2次运算的结果,如图3-3(e)所示。
同上两次的分析过程一样,请读者分析这一次结果。
(4)令k=3,即以v3作为新考虑的中间点,这也是最后一个要考虑的中间点,在A
(2)的基础上进行运算,得到的运算结果A(3)如图3-3(f)所示,也请读者同学们自行分析。
A(3)就是最后得到的整个运算的结果,A(3)中的每个元素A(3)[i][j]的值就是图3-3(a)中顶点vi到vj的最短路径长度。
当然相应的最短路径也可以通过另设一个矩阵记录下来。
通过以上分析可知,在每次运算中,对i=k或j=k或i=j的那些元素无需进行计算,因为它们不会被修改,对于其余元素,只有满足A(k-1)[i][k]+A(k-1)[k][j]<A(k-1)[i][j]的元素才会被修改,即把小于号左边的两个元素之和赋给A(k)[i][j],在这两个元素中,一个是列号等于k,一个是行号等于k,所以它们在进行第k次运算的整个过程中,其值都不会改变,故每一次运算都可以在原数组上“就地”进行,即用新修改的值替换原值即可,不需要使用两个数组交替进行。
设具有n个顶点的一个带权图G的邻接矩阵用GA表示,与GA同类型的,求每对顶点之间最短路径长度的二维数组用A表示,A的初值等于GA。
弗洛伊德算法需要在A上进行n次运算,每次以vk(0≤k≤n-1)作为一个新考虑的中间点,求出每对顶点之间的当前最短路径长度,最后一次运算后,A中的每个元素A[i][j]就是图G中从顶点vi到顶点vj的最短路径长度。
利用C++语言编写出弗洛伊德算法如下,假定在该算法中不需要记录每对顶点之间的最短路径,只需要记录每对顶点之间的最短距离。
voidFloyed(adjmatrixGA,adjmatrixA,intn)
//利用弗洛伊德算法求GA表示的图中每对顶点之间的最短长度,
//对应保存于二维数组A中
{
inti,j,k;
//给二维数组A赋初值,它等于图的邻接矩阵GA
for(i=0;ifor(j=0;jA[i][j]=GA[i][j];
//依次以每个顶点作为中间点,逐步优化数组A
for(k=0;kfor(i=0;ifor(j=0;j{
if(i==k||j==k||i==j)
continue;
if(A[i][k]+A[k][j]A[i][j]=A[i][k]+A[k][j];
}
}
二、拓扑排序
一个较大的工程往往被划分成许多子工程,我们把这些子工程称作活动(activity)。
在整个工程中,有些子工程(活动)必须在其它有关子工程完成之后才能开始,也就是说,一个子工程的开始是以它的所有前序子工程的结束为先决条件的,但有些子工程没有先决条件,可以安排在任何时间开始。
为了形象地反映出整个工程中各个子工程(活动)之间的先后关系,可用一个有向图来表示,图中的顶点代表活动(子工程),图中的有向边代表活动的先后关系,即有向边的起点的活动是终点活动的前序活动,只有当起点活动完成之后,其终点活动才能进行。
通常,我们把这种顶点表示活动、边表示活动间先后关系的有向图称做顶点活动网(ActivityOnVertexnetwork),简称AOV网。
课程代号课程名称先修课程
C1高等数学无C2程序设计基础无
C3离散数学C1,C2
C4数据结构C3,C5
C5算法语言C2
C6编译技术C4,C5
C7操作系统C4,C9
C8普通物理C1
C9计算机原理C8
图3-4课程表
例如,假