详解图的应用(最小生成树、拓扑排序、关键路径、最短路径)
1.最小生成树:无向连通图的所有生成树中有一棵边的权值总和最小的生成树 1.1 问题背景: 1.2 分析问题(建立模型): 可以用连通网来表示n个城市以及n个城市间可能设置的通信线路,其中网的顶点表示城市,边表示两城市之间的线路,赋于边的权值表示相应的代价。对于n个顶点的连通网可以建立许多不同的生成树,每一棵生成树都可以是一个通信网。即无向连通图的生成树不是唯一的。连通图的一次遍历所经过的边的集合及图中所有顶点的集合就构成了该图的一棵生成树,对连通图的不同遍历,就可能得到不同的生成树。 图 G5无向连通图的生成树 为(a)、(b)和(c)图所示: G5 G5的三棵生成树: 可以证明,对于有n 个顶点的无向连通图,无论其生成树的形态如何,所有生成树中都有且仅有n-1 条边。 1.3最小生成树的定义: 如果无向连通图是一个网,那么,它的所有生成树中必有一棵边的权值总和最小的生成树,我们称这棵生成树为最小生成树,简称为最小生成树。 最小生成树的性质: 则必存在一棵包含边(u,v)的最小生成树。 1.4 解决方案: 两种常用的构造最小生成树的算法:普里姆(Prim)和克鲁斯卡尔(Kruskal)。他们都利用了最小生成树的性质 1.普里姆(Prim)算法:有线到点,适合边稠密。时间复杂度O(N^2) 假设G=(V,E)为连通图,其中V 为网图中所有顶点的集合,E 为网图中所有带权边的集合。设置两个新的集合U 和T,其中 集合U(顶点集) 用于存放G 的最小生成树中的顶点, 集合T (边集合)存放G 的最小生成树中的边。 T ,U的初始状态:令集合U 的初值为U={u1}(假设构造最小生成树时,从顶点u1 出发),集合T 的初值为T={}。 Prim 算法的思想是:从所有u∈U,v∈V-U 的边中,选取具有最小权值的边(u,v)∈E,将顶点v 加入集合U 中,将边(u,v)加入集合T 中,如此不断重复,直到U=V 时,最小生成树构造完毕,这时集合T 中包含了最小生成树的所有边。 Prim 算法可用下述过程描述,其中用wuv 表示顶点u 与顶点v 边上的权值。 为实现Prim 算法,需设置两个辅助closedge,用来保存U到集合V-U 的各个顶点中具有最小权值的边的权值。对每个Vi∈(V-U )在辅助数组中存在一个相应的分量closedge[i-1],它包括两个域: typedef struct ArcNode { int adjvex; //adjex域存储该边依附的在U中的顶点 显然: 初始状态时,U={v1}(u1 为出发的顶点),则到V-U 中各项中最小的边,即依附顶点v1的各条边中,找到一条代价最小的边(u0,v0)= (1,3)为生成树上一条边。 1)将closedge[2].lowcost = 0;//表示顶点V3三已经并入U 2) 由于边(v2,v3)的权值小于closedge[1].lowcost,故需修改closedge[1]为边(v2,v3)及其权值,同理修改closedge[4],closedge[5]. closedge[1].adjvex = 3. closedge[1].lowcost = 5. closedge[4].adjvex = 1. closedge[4].lowcost = 5. closedge[5].adjvex = 3. closedge[5].lowcost = 6. 以此类推,直至U = V; 下图给出了在用上述算法构造网图7.16的最小生成树的过程中: Prim 算法实现: 按照算法框架: (1)U={u1},T={}; //记录从顶点集U到V―U的代价最小的边的辅助数组定义: // struct{ // VertexType adjvex; // VRType lowcost; // }closedge[ MAX_VERTEX_NUM ] void MiniSpanTree_PRIM (MGraph G,VertexType u){ //用普里姆算法从第u个顶点出发构造网G的最小生成树T,输出T的各条边。 k =LocateVex(G,u); for(j=0; j<G.vexnum; ++j) if(j!=k) closedge[j] ={u,G.arcs[k][j].adj}; // {adjvex,lowcost} closedge[k].lowcost =0; //初始,U={u} for( i=1;i<G.vexnum;++i){ //选择其余G.vexnum-1个顶点 k=minimum(closedge); printf(closedge[k].adjvex,G.vexs[k]);//输出生成树的边 //第k顶点并入U集 closedge[k].lowcost=0; for(j=0; j<G.vexnum; ++j) if (G.acrs[k][j].adj<closedge[j].lowcost) closedge[j]={G.vexs[k],G.arcs[k][j].adj}; }//for }//MiniSpanTree 假设网中有n个顶点,则第一个进行初始化的循环语句的频度为n,第二个循环语句的频度为n-1。其中有两个内循环:其一是在closedge[v].lowcost中求最小值,其频度为n-1;其二是重新选择具有最小代价的边,其频度为n。 由此,普里姆算法的时间复杂度为O(n2),与网中的边数无关,因此适用于求边稠密的网的最小生成树。 Kruskal 算法是一种按照网中边的权值递增的顺序构造最小生成树的方法。 基本思想是: 1) 设无向连通网为G=(V,E),令G 的最小生成树为T,其初态为T=(V,{}),即开始时,最小生成树T 由图G 中的n 个顶点构成,顶点之间没有一条边,这样T 中各顶点各自构成一个连通分量。 2) 在E中选择代价最小的边,若该边依附的顶点落在T中不同的连通分量,则将此边加入到T中,否则舍弃此边而选择下一条边(若该边依附的两个顶点属于同一个连通分量,则舍去此边,以免造成回路)。依此类推,当T 中的连通分量个数为1 时,此连通分量便为G 的一棵最小生成树。 按照Kruskal 方法构造最小生成树的过程如图所示: 在构造过程中,按照网中边的权值由小到大的顺序,不断选取当前未被选取的边集中权值最小的边。依据生成树的概念,n 个结点的生成树,有n-1 条边,故反复上述过程,直到选取了n-1 条边为止,就构成了一棵最小生成树。
构造非连通图T=(V,{}) k = i= 0;//k为边数 while(k《< n-1) { i++; 检查边E中第i条边的权值 最小边(u,v) 若(u,v) 加入T不是T产生回路, 则(u,v)加入T,且k++ } c语言实现: C 语言实现Kruskal 算法,其中函数Find 的作用是寻找图中顶点所在树的根结点在数组father 中的序号。需说明的是,在程序中将顶点的数据类型定义成整型,而在实际应用中,可依据实际需要来设定。 typedef int elemtype; typedef struct { elemtype v1; elemtype v2; int cost; }EdgeType; void Kruskal(EdgeType edges[ ],int n) /*用Kruskal 方法构造有n 个顶点的图edges 的最小生成树*/ { int father[MAXEDGE]; int i,j,vf1,vf2; for (i=0;i<n;i++) father[i]=-1; i=0;j=0; while(i<MAXEDGE && j<n-1) { vf1=Find(father,edges[i].v1); vf2=Find(father,edges[i].v2); if (vf1!=vf2) { father[vf2]=vf1; j++; printf(“%3d%3dn”,edges[i].v1,edges[i].v2); } i++; } } //find 函数 int Find(int father[ ],int v) /*寻找顶点v 所在树的根结点*/ { int t; t=v; while(father[t]>=0) t=father[t]; return(t); } 2. AOV网与拓扑排序:由偏序定义得到拓扑有序的操作便是拓扑排序。建立模型是AOV网 所有的工程或者某种流程可以分为若干个小的工程或阶段,这些小的工程或阶段就称为活动。若以图中的顶点来表示活动,有向边(弧)表示活动之间的优先关系,则这样活动在顶点上的有向图称为AOV 网(Activity On Vertex Network)。在AOV 网中,若从顶点i到顶点j之间存在一条有向路径,称顶点i是顶点j的前驱,或者称顶点j 是顶点i的后继。若<i,j>是图中的弧,则称顶点i是顶点j 的直接前驱,顶点j 是顶点i 的直接后驱。 AOV 网中的弧表示了活动之间存在的制约关系。例如,计算机专业的学生必须完成一系列规定的基础课和专业课才能毕业。学生按照怎样的顺序来学习这些课程呢?这个问题可以被看成是一个大的工程,其活动就是学习每一门课程。这些课程的名称与相应代号如表所示。 课程之间的优先关系图:
注意: 2.2.拓扑排序 离散数学基础知识: 首先复习一下离散数学中的偏序集合与全序集合两个概念。 若集合A 中的二元关系R 是自反的、非对称的和传递的,则R 是A 上的偏序关系。集合A 与关系R 一起称为一个偏序集合。 若R 是集合A 上的一个偏序关系,如果对每个a、b∈A 必有aRb 或bRa ,则R 是A上的全序关系。集合A 与关系R 一起称为一个全序集合。 直观地看,偏序指集合中仅有部分成员之间可比较,而全序指集合中全体成员之间均可比较。
2.3 拓扑排序算法 对AOV 网进行拓扑排序的方法和步骤是: 这样操作的结果有两种:一种是网中全部顶点都被输出,这说明网中不存在有向回路;另一种就是网中顶点未被全部输出,剩余的顶点均不前驱顶点,这说明网中存在有向回路。 以下图(a)中的有向图为例,图中v1,和v6没有前驱,则可任选一个。假设先输出v6,在删除v6及弧<v6,v4>,<v6,v5>之后,只有顶点v1没有前驱,则输出vl且删去vl及弧<v1,v2>、<v1,v3>和<v1,v4>,之后v3和v4都没有前驱。依次类推,可从中任选一个继续进行。最后得到该有向图的拓扑有序序列为:
顶点表结点结构的描述改为: 为了避免重复检测入度为零的顶点,可另设一栈暂存所有入度为零的顶点。 Status Topological Sort(ALGraph G){ //有向图G采用邻接表存储结构。 //若G无回路,则输出G的顶点的1个拓扑序列并返回OK,否则ERROR。 FindInDegree(G,indegree); //对各顶点求入度indegree[0..vernum-1] InitStack(S); for(i=0;i<G.vexnum; ++i) if(!indegree[i])Push(S,i) //建零入度顶点栈,s入度为0者进栈 count=0; //对输出顶点计数 while (!StackEmpty(S)) { Pop(S,i); printf(i,G.vertices[i].data); ++count; //输出i号顶点并计数 for(p=G.vertices[i].firstarc;p; p=p―>nextarc) { k=p―>adivex; //对i号顶点的每个邻接点的入度减1 if(!(--indegree[k]))Push(S,k);//若入度减为0,则入栈 }//for }//while if(count<G.vexnum) return ERROR; //该有向图有回路 else return OK; }//TopologicalSort 3. 关键路径(AOE网):在AOE-网中有些活动可以并行地进行,所以完成工程的最短时间是从开始点到完成点的最长路径的长度,路径长度最长的路径叫做关键路径(Critical Path)。 3.1AOE网:(Activity on edge network) 3.2 实际问题: 如果用AOE网来表示一项工程,那么,仅仅考虑各个子工程之间的优先关系还不够,更多的是关心整个工程完成的最短时间是多少;哪些活动的延期将会影响整个工程的进度,而加速这些活动是否会提高整个工程的效率。因此,通常在AOE网中列出完成预定工程计划所需要进行的活动,每个活动计划完成的时间,要发生哪些事件以及这些事件与活动之间的关系,从而可以确定该项工程是否可行,估算工程完成的时间以及确定哪些活动是影响工程进度的关键。 如图是一个假想的有11项活动的AOE-网: 其中有9个事件v1,v2,v3,…,v9,每个事件表示在它之前的活动已经完成,在它之后的活动可以开始。如v1表示整个工程开始,v9表示整个工程结束,v5表示a4和a5已经完成,a7和a8可以开始。与每个活动相联系的数是执行该活动所需的时间。比如,活动a1需要6天,a2需要4天等。 和AOV-网不同,对AOE-网有待研究的问题是: 3.3 关键路径 由于在AOE-网中有些活动可以并行地进行,所以完成工程的最短时间是从开始点到完成点的最长路径的长度(这里所说的路径长度是指路径上各活动持续时间之和,不是路径上弧的数目)。路径长度最长的路径叫做关键路径(Critical Path)。 AOE网有关的概念: 2)完成工程的最短时间:由于AOE网中有活动是并行进行的,所以完成工程的最短时间就是从开始点到完成点的最长路劲长度。 e(i ) = ve(j) l(i) = vl(k)-dut(<j,k>) 求ve(j)和vl(j)需分两步进行: 其中,T是所有以第j个顶点为头的弧的结合。 (2)从vl(n-1)=ve(n-1)起向后递推 其中,S是所有以第i个顶点为尾的弧的集合。 这两个递推公式的计算必须分别在拓扑有序和逆拓扑有序的前提下进行。也就是说ve(j-1)必须在vj的所有前驱的最早发生时间求得之后才能确定,而vl(j-1)则必须在vj的所有后继的最迟发生时间求得之后才能确定。因此,可以在拓扑排序的基础上计算ve(j-1)和vl(j-1)。 3.5 关键路径的算法: 先将拓扑排序算法:TopologicalOrder() CriticalPath便为求关键路径的算法: Status TopologicalOrder(ALGraph G,Stack &T){ //有向网G采用邻接表存储结构,求各顶点事件的最早发生时间ve(全局变量)。 //T为拓扑序列顶点栈,s为零入度顶点栈。若G无回路,返回G的一拓扑序列,函数值为OK,否则ERROR。 FindInDegree(G,indegree);//对各顶点求入度indegree[0..vernum-1] for(i=0;i<G.vexnum; ++i) if(!indegree[i])Push(S,i) //建零入度顶点栈,s入度为0者进栈 InitStack(T); count=0;ve[0..G.vexnum-1]=0; //初始化 while(!StackEmpty(S)){ //j号顶点入T栈并计数 Pop(S,j); Push(T,j);++count; for(p=G.vertices[j].firstarc;p;p=p->nextarc){ k=p―>adjvex; //对i号顶点的每个邻接点的入度减l if(--indegree[k]==0)Push(S,k); //若入度减为0,则入栈 if(ve[j]+*(p->info)>ve[k] ) ve[k]=ve[j]+*(p->info); }//for *(p->info)=dut(<j,k>) }//while if(count<G.vexnum) return ERROR; //该有向网有回路 else return OK; }//TopologicalOrder Status CriticalPath (ALGraph G){ //G为有向网,输出G的各项关键活动。 if(!TopologicalOrder(G,T)) return ERROR; //初始化顶点事件的最迟发生时间 vl[0..G.vexnum-1]=ve[0..C.vexnum-1]; //按拓扑逆序求各顶点的vl值 while(!StackEmpty(T)) for( Pop(T,j),p=G.vertices[j].firstarc;p; p=p->nextarc){ k=p->adjvex; dut=*(p―>info); //dut<i,k> if(vl[k]-dut<vl[j]) vl[j]=vl[k]-dut; }//for for(j=0;j<G.vexnum;++j) //求ee,el和关键活动 for(p=G.vertices[j];p;p=p->nextarc){ k=p->adjvex; dut=*(p―>info);ee=ve[j];el=v1[k]-dut;tag = (ee==e1) ? ‘*' : ‘'; printf(j,k,dut,ee,el,tag); //输出关键活动 } }//CriticalPath 图(a)所示网的关键路径:可见a2、a5和a7为关键活动,组成一条从源点到汇点的关键路径,如图(b)所示。 图(a)所示网的计算结果: 4. 最短路径:最短路径问题是图研究中的一个经典算法问题, 旨在寻找图(由结点和路径组成的)中两结点之间的最短路径。 如果将城市用点表示,城市间的公路用边表示,公路的长度作为边的权值,那么,这个问题就可归结为在网图中,求点A 到点B 的所有路径中,边的权值之和最短的那一条路径。这条路径就是两点之间的最短路径,并称路径上的第一个顶点为源点(Sourse),最后一个顶点为终点(Destination)。 单源点的最短路径问题:给定带权有向图G=(V,E)和源点v∈V,求从v 到G 中其余各顶点的最短路径。在下面的讨论中假设源点为v0。 解决问题的迪杰斯特拉算法: 即由迪杰斯特拉(Dijkstra)提出的一个按路径长度递增的次序产生最短路径的算法。首先求出长度最短的一条最短路径,然后参照它求出长度次短的一条最短路径,依次类推,直到从顶点v到其它各顶点的最短路径全部求出为止。 算法的基本思想是: 设置两个顶点的集合S 和T=V-S,集合S 中存放已找到最短路径的顶点,集合T 存放当前还未找到最短路径的顶点。 初始状态时,集合S 中只包含源点v0,然后不断从集合T 中选取到顶点v0 路径长度最短的顶点u 加入到集合S 中,集合S 每加入一个新的顶点u,都要修改顶点v0 到集合T 中剩余顶点的最短路径长度值,集合T 中各顶点新的最短路径长度值为原来的最短路径长度值与顶点u 的最短路径长度值加上u 到该顶点的路径长度值中的较小值。此过程不断重复,直到集合T 的顶点全部加入到S 中为止。 Dijkstra 算法的实现: 首先,引进一个辅助向量D,它的每个分量D[i] 表示当前所找到的从始点v 到每个终点vi 的最短路径的长度。它的初态为:若从v 到vi 有弧,则D[i]为弧上的权值;否则置D[i]为∞。显然,长度为: D[j]=Min{D[i]| vi∈V} 那么,下一条长度次短的最短是哪一条呢?假设该次短路径的终点是vk ,则可想而知,这条路径或者是(v,vk),或者是(v,vj,vk)。它的长度或者是从v 到vk 的弧上的权值,或者是D[j]和从vj 到vk 的弧上的权值之和。 依据前面介绍的算法思想,在一般情况下,下一条长度次短的最短路径的长度必是: 根据以上分析,可以得到如下描述的算法: 如图8.26 所示一个有向网图G8 的带权邻接矩阵为: 有向网图G8 的带权邻接矩阵 用C 语言描述的Dijkstra 算法: void ShortestPath_DIJ(MGraph G,int v0,PathMatrix &P,ShortPathTable &D){ //用Dijkstra算法求有向网G的v0顶点到其余顶点v的最短路径P[v]及其带权长度D[v]。 //若P[v][w]为TRUE,则w是从v0到v当前求得最短路径上的顶点。 //final[v]为TRUE当且仅当v∈S,即已经求得从v0到v的最短路径。 for(v=0; v<G.vexnum; ++v) { final[v]=FALSE; D[v]=G.arcs[v0][v]; for(w=0; w<G.vexnum; ++w) P[v][w] = FALSE;//设空路径 if (D[v]<INFINITY) { P[v][v0]=TRUE; P[v][v]=TRUE;} }//for D[v0] = 0; final[v0] = TRUE; //初始化,v0顶点属于S集 //开始主循环,每次求得v0到某个v顶点的最短路径,并加v到s集。 for(i=1; i<G.vexnum;++i){ //其余G.vexnum-1个顶点 min = INFINITY; //当前所知离v0顶点的最近距离 for(w=0;w<G.vexnum;++w) if(!final[w]) //w顶点在V-S中 if(D[w]<min){v=w;min=D[w];} //w顶点离v0顶点更近 final[v]=TRUE; //离v0顶点最近的v加入S集 for(w=0;w<G.vexnum;++w) //更新当前最短路径及距离 if(!final[w]&&(min+G.arcs[v][w]<D[w])){ //修改D[w]和P[w] D[w]=min+G.arcs[v][w];P[w]=P[v]; P[w][w]=TRUE; //P[w]=P[v]+[w] }//if }//for }//ShortestPath_DIJ 以上就是图的应用全部详细介绍,希望对大家的学习有所帮助。 (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |