图的应用详解-数据结构

概述

最小生成树——无向连通图的所有生成树中有一棵边的权值总和最小的生成树

拓扑排序——由偏序定义得到拓扑有序的操作便是拓扑排序。建立模型是AOV网

关键路径——在AOE-网中有些活动可以并行地进行,所以完成工程的最短时间是从开始点到完成点的最长路径的长度,路径长度最长的路径叫做关键路径(Critical Path)。

最短路径——最短路径问题是图研究中的一个经典算法问题, 旨在寻找图(由结点和路径组成的)中两结点之间的最短路径。

1.最小生成树

1.1 问题背景:
假设要在n个城市之间建立通信联络网,则连通n个城市只需要n—1条线路。这时,自然会考虑这样一个问题,如何在最节省经费的前提下建立这个通信网。在每两个城市之间都可以设置一条线路,相应地都要付出一定的经济代价。n个城市之间,最多可能设置n(n-1)/2条线路,那么,如何在这些可能的线路中选择n-1条,以使总的耗费最少呢?

1.2 分析问题(建立模型):

可以用连通网来表示n个城市以及n个城市间可能设置的通信线路,其中网的顶点表示城市,边表示两城市之间的线路,赋于边的权值表示相应的代价。对于n个顶点的连通网可以建立许多不同的生成树,每一棵生成树都可以是一个通信网。即无向连通图的生成树不是唯一的。连通图的一次遍历所经过的边的集合及图中所有顶点的集合就构成了该图的一棵生成树,对连通图的不同遍历,就可能得到不同的生成树。

图 G5无向连通图的生成树为(a)、(b)和(c)图所示:


G5

G5的三棵生成树:




可以证明,对于有n 个顶点的无向连通图,无论其生成树的形态如何,所有生成树中都有且仅有n-1 条边。

1.3最小生成树的定义:

如果无向连通图是一个网,那么,它的所有生成树中必有一棵边的权值总和最小的生成树,我们称这棵生成树为最小生成树,简称为最小生成树。

最小生成树的性质:
假设N=(V,{ E}) 是个连通网,U是顶点集合V的一个非空子集,若(u,v)是个一条具有最小权值(代价)的边,其中,

则必存在一棵包含边(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 边上的权值。
(1)U={u1},T={};
(2)while (U≠V)do
(u,v)=min{wuv;u∈U,v∈V-U }
T=T+{(u,v)}
U=U+{v}
(3)结束。
按照Prim 方法,从顶点1 出发,该网的最小生成树的产生过程如图:

为实现Prim 算法,需设置两个辅助closedge,用来保存U到集合V-U 的各个顶点中具有最小权值的边的权值。对每个Vi∈(V-U )在辅助数组中存在一个相应的分量closedge[i-1],它包括两个域:

typedefstructArcNode

{

  1. intadjvex;//adjex域存储该边依附的在U中的顶点
  2. VrTypelowcost;//lowcost域存储该边上的权重
  3. }closedge[MAX_VERTEX_NUM];

显然:

初始状态时,U={v1}(u1 为出发的顶点),则到V-U 中各项中最小的边,即依附顶点v1的各条边中,找到一条代价最小的边(u0,v0)= (1,3)为生成树上一条边。

同时将v0(=v3)并入集合U中。然后修改辅助数组的值。

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={};
(2)while (U≠V)do
(u,v)=min{wuv;u∈U,v∈V-U }
T=T+{(u,v)}
U=U+{v}
(3)结束。

当无向网采用二维数组存储的邻接矩阵存储时,Prim 算法的C 语言实现为:

//记录从顶点集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


假设网中有n个顶点,则第一个进行初始化的循环语句的频度为n,第二个循环语句的频度为n-1。其中有两个内循环:其一是在closedge[v].lowcost中求最小值,其频度为n-1;其二是重新选择具有最小代价的边,其频度为n。 由此,普里姆算法的时间复杂度为O(n2),与网中的边数无关,因此适用于求边稠密的网的最小生成树。

2.克鲁斯卡尔(Kruskal) :由点到线,适合边稀疏的网。时间复杂度:O(e * loge)

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 条边为止,就构成了一棵最小生成树。


Kruskal 算法的实现:
算法的框架:

构造非连通图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=0)
t=father[t];
return(t);
}

2. AOV网与拓扑排序

2.1.AOV网(Activity on vertex network)

所有的工程或者某种流程可以分为若干个小的工程或阶段,这些小的工程或阶段就称为活动。若以图中的顶点来表示活动,有向边(弧)表示活动之间的优先关系,则这样活动在顶点上的有向图称为AOV 网(Activity On Vertex Network)。在AOV 网中,若从顶点i顶点j之间存在一条有向路径,称顶点i顶点j的前驱,或者称顶点j顶点i的后继。若是图中的弧,则称顶点i是顶点j 的直接前驱,顶点j 是顶点i 的直接后驱

AOV 网中的弧表示了活动之间存在的制约关系。例如,计算机专业的学生必须完成一系列规定的基础课和专业课才能毕业。学生按照怎样的顺序来学习这些课程呢?这个问题可以被看成是一个大的工程,其活动就是学习每一门课程。这些课程的名称与相应代号如表所示。


课程之间的优先关系图:


该图的拓扑有序系列:


注意:
在AOV-网中不应该出现有向环,因为存在环意味着某项活动应以自己为先决条件。若设计出这样的流程图,工程便无法进行。而对程序的数据流图来说,则表明存在一个死循环。因此,对给定的AOV-网应首先判定网中是否存在环。检测的办法是对有向图构造其顶点的拓扑有序序列,若网中所有顶点都在它的拓扑有序序列中,则该AOV-网中必定不存在环。

2.2.拓扑排序

离散数学基础知识:

首先复习一下离散数学中的偏序集合与全序集合两个概念。

若集合A 中的二元关系R 是自反的、非对称的和传递的,则R 是A 上的偏序关系。集合A 与关系R 一起称为一个偏序集合。

若R 是集合A 上的一个偏序关系,如果对每个a、b∈A 必有aRb 或bRa ,则R 是A上的全序关系。集合A 与关系R 一起称为一个全序集合。

直观地看,偏序指集合中仅有部分成员之间可比较,而全序指集合中全体成员之间均可比较。


[例如],图7.25所示的两个有向图,图中弧(x,y)表示x≤y,则(a)表示偏序,(b)表示全序。若在(a)的有向图上人为地加一个表示v2≤v3的弧(符号“≤”表示v2领先于v3),则(a)表示的亦为全序,且这个全序称为拓扑有序(Topological Order),而由偏序定义得到拓扑有序的操作便是拓扑排序。

3.3 拓扑排序算法

对AOV 网进行拓扑排序的方法和步骤是:
1、从AOV 网中选择一个没有前驱的顶点(该顶点的入度为0)并且输出它;
2、从网中删去该顶点,并且删去从该顶点发出的全部有向边;
3、重复上述两步,直到剩余的网中不再存在没有前驱的顶点为止。

这样操作的结果有两种:一种是网中全部顶点都被输出,这说明网中不存在有向回路;另一种就是网中顶点未被全部输出,剩余的顶点均不前驱顶点,这说明网中存在有向回路。

以下图(a)中的有向图为例,图中v1,和v6没有前驱,则可任选一个。假设先输出v6, 在删除v6及弧之后,只有顶点v1没有前驱,则输出vl且删去vl及弧,之后v3和v4都没有前驱。依次类推,可从中任选一个继续进行。最后得到该有向图的拓扑有序序列为:

v6 - v1 - v4 - v3 - v2 - v5


图AOV-网及其拓扑有序序列产生的过程
(a)AOV-网;(b)输出v6之后;(c)输出v1之后;(d)输出v4之后;(e)输出v3之后;(f)输出v2之后

为了实现上述算法,对AOV 网采用邻接表存储方式,并且邻接表中顶点结点中增加一个记录顶点入度的数据域,即顶点结构设为:

顶点表结点结构的描述改为:
typedef struct vnode{ /*顶点表结点*/
int count /*存放顶点入度*/
VertexType vertex; /*顶点域*/
EdgeNode * firstedge; /*边表头指针*/
}VertexNode;

当然也可以不增设入度域,而另外设一个一维数组来存放每一个结点的入度。算法中可设置了一个堆栈,凡是网中入度为0 的顶点都将其入栈。为此,拓扑排序的算法步骤为:
1、将没有前驱的顶点(count 域为0)压入栈;
2、从栈中退出栈顶元素输出,并把该顶点引出的所有有向边删去,即把它的各个邻接顶点的入度减1;
3、将新的入度为0 的顶点再入堆栈;
4、重复②~④,直到栈为空为止。此时或者是已经输出全部顶点,或者剩下的顶点中没有入度为0 的顶点。

为了避免重复检测入度为零的顶点,可另设一暂存所有入度为零的顶点。

Status Topological Sort(ALGraph G){
//有向图G采用邻接表存储结构。
//若G无回路,则输出G的顶点的1个拓扑序列并返回OK,否则ERROR。
    FindInDegree(G,indegree); //对各顶点求入度indegree[0..vernum-1]
    InitStack(S);
    for(i=0;inextarc) {
            k=p—>adivex; //对i号顶点的每个邻接点的入度减1
            if(!(--indegree[k]))Push(S,k);//若入度减为0,则入栈
        }//for
    }//while
    if(count


3. 关键路径(AOE网)

3.1AOE网:(Activity on edge network)
AOE网示意图若在带权的有向图中,以顶点表示事件,以有向边表示活动,边上的权值表示活动的开销(如该活动持续的时间),则此带权的有向图称为AOE网。


3.2 实际问题:


如果用AOE网来表示一项工程,那么,仅仅考虑各个子工程之间的优先关系还不够,更多的是关心整个工程完成的最短时间是多少;哪些活动的延期将会影响整个工程的进度,而加速这些活动是否会提高整个工程的效率。因此,通常在AOE网中列出完成预定工程计划所需要进行的活动,每个活动计划完成的时间,要发生哪些事件以及这些事件与活动之间的关系,从而可以确定该项工程是否可行,估算工程完成的时间以及确定哪些活动是影响工程进度的关键。

如图是一个假想的有11项活动的AOE-网:


其中有9个事件v1v2v3v9,每个事件表示在它之前的活动已经完成,在它之后的活动可以开始。如v1表示整个工程开始,v9表示整个工程结束,v5表示a4a5已经完成,a7a8可以开始。与每个活动相联系的数是执行该活动所需的时间。比如,活动a1需要6天,a2需要4天等。

和AOV-网不同,对AOE-网有待研究的问题是:
(1)完成整项工程至少需要多少时间?
(2)哪些活动是影响工程进度的关键?

3.3 关键路径

由于在AOE-网中有些活动可以并行地进行,所以完成工程的最短时间是从开始点到完成点的最长路径的长度(这里所说的路径长度是指路径上各活动持续时间之和,不是路径上弧的数目)。路径长度最长的路径叫做关键路径(Critical Path)。

AOE网有关的概念:
1)路径长度:路径上各个活动的持续时间之和

2)完成工程的最短时间:由于AOE网中有活动是并行进行的,所以完成工程的最短时间就是从开始点到完成点的最长路劲长度。
3)活动最早开始时间(earlist time)(e(i)):从开始点到顶点vi的最长路径称为事件vi的最早发生时间。这个时间决定了以vi为尾的弧表示的活动的最早开始时间.

4)活动最晚开始时间(latest time)(l(i)):在不推迟整个工程完成的前提下,活动最迟开始的时间
5)完成活动的时间余量:该活动的最迟开始时间减去最早开始时间
6)关键路径(critical path):路径长度最长的路径称为关键路径
7)关键活动(critical activity):关键路径上的活动称为关键活动,关键活动的特点是:e(i)=l(i)分析关键路径的目的就是辨别在整个工程中哪些是关键活动,以便争取提高关键活动的工作效率,缩短整个工程的工期。

3.4 解决方案:
由上分析可知,辨别关键活动就是要找e(i)=l(i)的活动。为了求得AOE-网中活动的e(i)和l(i), 首先求事件的最早发生时间ve(j)和最迟发生时间vl(j)。如果活动ai由弧表示,其持续时间记为dut(),则有如下关系:

e(i ) = ve(j)

l(i) = vl(k)-dut()

求ve(j)和vl(j)需分两步进行:
(1)从ve(0)开始向前递推

其中,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 关键路径的算法:
(1)输入e条弧,建立AOE-网的存储结构;
(2)从源点v0出发,令ve[0]=0,按拓扑有序求其余各顶点的最早发生时间ve[i] (1≤i≤n-1)。如果得到的拓扑有序序列中顶点个数小于网中顶点数n,则说明网中存在环,不能求关键路径,算法终止;否则执行步骤(3)。
(3)从汇点vn出发,令vl[n-1]=ve[n-1],按逆拓扑有序求其余各顶点的最迟发生时间vl[i](n-2≥i≥0);
(4)根据各顶点的ve和vl值,求每条弧s的最早开始时间e(s)和最迟开始时间 l(s)。若某条弧满足条件e(s)=l(s),则为关键活动。

先将拓扑排序算法: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;inextarc){
            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() 

    }//while 
    if(countnextarc){
            k=p->adjvex; dut=*(p—>info); //dut
            if(vl[k]-dutnextarc){
            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. 最短路径

最短路径问题是图的又一个比较典型的应用问题。例如,某一地区的一个公路网,给定了该网内的n 个城市以及这些城市之间的相通公路的距离,能否找到城市A 到城市B 之间一条举例最近的通路呢?


如果将城市用点表示,城市间的公路用边表示,公路的长度作为边的权值,那么,这个问题就可归结为在网图中,求点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}
的路径就是从v 出发的长度最短的一条最短路径。此路径为(v ,vj)。

那么,下一条长度次短的最短是哪一条呢?假设该次短路径的终点是vk ,则可想而知,这条路径或者是(v, vk),或者是(v, vj, vk)。它的长度或者是从v 到vk 的弧上的权值,或者是D[j]和从vj 到vk 的弧上的权值之和。

依据前面介绍的算法思想,在一般情况下,下一条长度次短的最短路径的长度必是:
D[j]=Min{D[i]| vi∈V-S}
其中,D[i] 或者弧(v, vi)上的权值,或者是D[k]( vk∈S 和弧(vk, vi)上的权值之和。

根据以上分析,可以得到如下描述的算法:
(1)假设用带权的邻接矩阵edges 来表示带权有向图,edges[i][j] 表示弧〈vi, vj〉上的权值。若〈vi, vj〉不存在,则置edges[i][j]为∞(在计算机上可用允许的最大值代替)。S 为已找到从v 出发的最短路径的终点的集合,它的初始状态为空集。那么,从v 出发到图上其余各顶点(终点)vi 可能达到最短路径长度的初值为:
D[i]= edges[Locate Vex(G,v)][i] vi∈V
(2)选择vj,使得
D[j]=Min{D[i]| vi∈V-S}
vj 就是当前求得的一条从v 出发的最短路径的终点。令
S=S∪{j}
(3)修改从v 出发到集合V-S 上任一顶点vk 可达的最短路径长度。如果
D[j]+ edges[j][k]
则修改D[k]为
D[k]=D[j]+ edges[j][k]

重复操作(2)、(3)共n-1 次。由此求得从v 到图上其余各顶点的最短路径是依路径长度递增的序列。


如图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






你可能感兴趣的:(图的应用详解-数据结构)