数据结构之拓扑排序与关键路径

620 阅读9分钟

AOV 网定义

AOV 网:若用 DAG 图表示一个工程,其顶点表示活动,用有向边<Vi,Vj><V_i,V_j>表示活动ViV_i必须先于活动VjV_j进行的这样一种关系,则将这种有向图称为顶点表示活动的网络,记为AOV网。

拓扑排序:在图论中,由一个有向无环图的顶点组成的序列,当且仅当满足下列条件时,称为该图的一个拓扑排序:

①每个顶点出现且只出现一次。

②若顶点A在序列中排在顶点B的前面,则在图中不存在从顶点B到顶点A的路径。

或定义为:拓扑排序是对有向无环图的顶点的一种排序,它使得若存在一条从顶点A到顶点B的路径, 则在排序中顶点B出现在顶点A的后面。每个AOV网都有一个或多个拓扑排序序列。

对一个AOV网进行拓扑排序的算法有很多, 下面介绍比较常用的一种方法的步骤:

①从AOV网中选择一个没有前驱的顶点并输出.

②从网中删除该顶点和所有以它为起点的有向边.

③重复①和②直到当前的AOV网为空或当前网中不存在无前驱的顶点为止.后一种情况说
明有向图中必然存在环。

拓扑排序算法的实现如下:

图、顶点及边的定义

define Maxvertexnum 100     //图中顶点数目的最大值
typedef struet Arenode{     //边表结点
    int adivex;                //该弧所指向的顶点的位置

    struct Arenode *nextarc;   //指向下一条弧的指针
    //Infotype info;           //网的边权值

}Arcnode;

typedef struct VNode{      //顶点表结点
    VertexType data;           //顶点信息
    Arcnode *firstarc;         ///指向第一条依附该顶点的弧的指针
}Vnode, Adjlist[Maxvertexnum]

typedef struct{
    Adlist vertices;            //邻接表
    int vexnum, arcnum:         //图的顶点数和弧数
}Graph;                     //Graph是以邻接表存储的图类型
bool Topologicalsort(Graph G)
 Initstack (S);        //初始化栈,存储入度为0的顶点
 for(int i=0;i<G.vexnum;i++)
    if(indegree[i] == 0)
        Push(S,i);     //将所有入度为0的顶点进栈
 int count=0;          //计数,记录当前已经输出的顶点数

 while(!IsEmpty()){     //栈不空,则存在入度为0的顶点
    Pop(S,i)          //栈顶元素出栈
    
    print[count++]=i; //输出顶点i,print数组用于保存顶点的输出顺序
    for(p=G.vertices[i].firstarc; p;p=p->nextarc){
    //将所有1指向的顶点的入度减1,并且将入度减为0的顶点压入栈S
        v=p->adjvex;
        if(!(--indegree[v]))
            Push(S,v)   //入度为0,则入栈
    }
    
    if(count<G.vexnum)
        return false;   //排序失败,有向图中有回路
    else
        return true;    //拓扑排序成功
 }

由于输出每个顶点的同时还要删除以它为起点的边,故拓扑排序的时间复杂度为O(V+E)O(|V|+|E|)

对一个AOV网,如果采用下列步骤进行排序,则称之为逆拓扑排序

①从AOV网中选择一个没有后继(出度为0)的顶点并输出

②从网中删除该顶点和所有以它为终点的有向边

③重复①和②直到当前的AOV网为空

逆拓扑排序的实现(DFS算法):

void Dfstraverse(Graph G){      //对图G进行深度优先遍历
    for(v=0;v<G.vexnum;++v)
        visited[v]=FALSE;       //初始化已访问标记数据
    
    for(v=0;v<G.vexnum;++v)     //本代码中是从v=0开始遍历
        if(!visited[v])
            DFS(G,v);
}
void DFS(Graph G,int v){        //从顶点出发,深度优先遍历图G
    visit(v);                   //访问顶点
    visited[v]=TRUE;            //设置已访问标记
    
    for(w=Firstneighbor(G, v);w>=0; Nextneighor(G,, w))
        if(!visited[w]){           //w为u的尚未访问的邻接顶点
            DFS(G,w);
        }
    printf(v)                   //输出顶点
}

用拓扑排序算法处理AOV网时,应注意以下问题:

①入度为零的顶点,即没有前驱活动的或前驱活动都已经完成的顶点,工程可以从这个顶
点所代表的活动开始或继续。

②若一个顶点有多个直接后继,则拓扑排序的结果通常不唯一:但若各个顶点已经排在一
个线性有序的序列中,每个顶点有唯一的前驱后继关系,则拓扑排序的结果是唯一的。

③由于AOV网中各顶点的地位平等,每个顶点编号是人为的,因此可以按拓扑排序的结果
重新编号,生成AOV网的新的邻接存储矩阵,这种邻接矩阵可以是三角矩阵;但对于
般的图来说,若其邻接矩阵是三角矩阵,则存在拓扑序列;反之则不一定成立

关键路径

在带权有向图中,以顶点表示事件,以有向边表示活动,以边上的权值表示完成该活动的开 销(如完成活动所需的时间),称之为用边表示活动的网络,简称AOE网。AOE网和AOV网都 是有向无环图,不同之处在于它们的边和顶点所代表的含义是不同的,AOE网中的边有权值;而 AOV网中的边无权值,仅表示顶点之间的前后关系。

AOE网具有以下两个性质:

①只有在某顶点所代表的事件发生后,从该顶点出发的各有向边所代表的活动才能开始;

②只有在进入某顶点的各有向边所代表的活动都已结束时,该顶点所代表的事件才能发生。

在AOE网中仅有一个入度为0的顶点,称为开始顶点(源点),它表示整个工程的开始网中也仅存在一个出度为0的顶点,称为结束顶点(汇点),它表示整个工程的结束。

在AOE网中,有些活动是可以并行进行的。从源点到汇点的有向路径可能有多条,并且这些路径长度可能不同。完成不同路径上的活动所需的时间虽然不同,但是只有所有路径上的活动都已完成,整个工程才能算结束。因此,从源点到汇点的所有路径中,具有最大路径长度的路径称为关键路径,而把关键路径上的活动称为关键活动

完成整个工程的最短时间就是关键路径的长度,即关键路径上各活动花费开销的总和。这是因为关键活动影响了整个工程的时间即若关键活动不能按时完成,则整个工程的完成时间就会延长。因此,只要找到了关键活动,就找到了关键路径,也就可以得出最短完成时间。

事件vkv_k的最早发生时间ve(k)ve(k)---决定了所有从vkv_k开始的活动能够开工的最早时间

Lewis: 每个顶点(事件)的最早发生时间可以理解为,所有前驱节点开始的时间+相对应边的权值中最大的时间即为最早开工时间

如图黄色的数字即代表了每个事件最早能发生的时间:

分析:

  1. V1节点的开始时间默认为0
  2. V2节点的开始时间为ve(v1)+a20+1=1ve(v2)=1ve(v1) + a_2 \rArr 0 + 1 = 1 \rArr ve(v2) = 1
  3. V3节点的开始时间为max(ve(v2)+a3=4,ve(v1)+a1=2)ve(v3)=4max(ve(v2) + a_3 = 4, ve(v1) + a_1 = 2) \rArr ve(v3) = 4
  4. V4节点的开始时间为ve(v3)+a44+2=6ve(v4)=6ve(v3) + a_4 \rArr 4 + 2 = 6 \rArr ve(v4) = 6

活动aia_i的最早开始时间e(i)e(i)---指该活动弧的起点所表示的事件的最早发生时间

如图红色数字表示了每个活动的最早发生时间:

分析:每个活动所代表的弧的弧尾对应事件的最早发生事件,即e(1)=ve(v1)=0,e(2)=ve(v1)=0,e(3)=ve(v2)=1,e(4)=ve(v3)=4e(1)=ve(v1)=0,e(2)=ve(v1)=0,e(3)=ve(v2)=1,e(4)=ve(v3)=4

事件vkv_k的最迟发生时间vl(k)vl(k)---指在不推迟整个工程完成的前提下,该事件最迟必须发生的时间

如图紫色数字表示了每个事件的最迟发生时间:

假设已知整个工程完成所需的最短时间为6分钟,则v4事件的最迟发生时间即为6分钟。最后往前倒退可知vl(v3)=vl(v4)a4=62=4vl(v3)=vl(v4) - a_4 = 6 - 2 = 4

vl(v2)=vl(v3)a3=43=1vl(v2)=vl(v3) - a_3 = 4 - 3 = 1

vl(v1)=min(vl(v3)a1=2,vl(v2)a2=0)=0vl(v1)=min(vl(v3) - a_1 = 2, vl(v2) - a_2 = 0) = 0

活动aia_i的最迟开始时间l(i)l(i)---指该活动弧的终点所表示事件的最迟发生时间与该活动所需时间之差

如图绿色数字表示每个活动的最迟开始时间

分析:

  1. l(4)=vl(v4)a4=62=4l(4) = vl(v4) - a_4 = 6 - 2 = 4
  2. l(3)=vl(v3)a3=43=1l(3) = vl(v3) - a_3 = 4 - 3 = 1
  3. l(2)=vl(v2)a2=11=0l(2) = vl(v2) - a_2 = 1 - 1 = 0
  4. l(1)=vl(v3)a1=42=2l(1) = vl(v3) - a_1 = 4 - 2 = 2

关键路径的求解思路

将每个活动的最早开始时间和最迟开始时间放在一起,如下图所示:

活动aia_i的最早开始时间e(i)e(i)---指该活动弧的起点所表示的事件的最早发生时间

活动aia_i的最迟开始时间l(i)l(i)---指该活动弧的终点所表示事件的最迟发生时间与该活动所需时间之差

活动aia_i时间余量di=l(i)e(i)d_i=l(i)-e(i),表示在不增加完成整个工程所需总时间的情况下,活动aia_i可以拖延的时间。

若一个活动的时间余量为零,则说明该活动必须要如期完成,di=0d_i=0l(i)=e(i)l(i)=e(i)的活动aia_i关键活动。由关键活动组成的路径称作关键路径

下面给出在寻找关键活动时所用到的几个参量的定义。

1.事件v的最早发生时间ve(k)

它是指从源点v1到顶点v的最长路径长度。事件v的最早发生时间决定了所有从v开始的活动能够开工的最早时间。可用下面的递推公式来计算:

ve(源点)=0

ve(k)=max(ve(j)+Weight(vj,vk)ve(k)=max(ve(j)+ Weight(v_j,v_k) ,vkv_kvjv_j的任意后继, Weight(vj,vk)Weight(v_j,v_k)表示<vj,vk><v_j,v_k>上的权值

计算ve()ve()值时,按从前往后的顺序进行,可以在拓扑排序的基础上计算:

①初始时,令ve[1...n]=0ve[1...n]=0

②输出一个入度为0的顶点vjv_j时,计算它所有直接后继顶点vkv_k的最早发生时间,若ve(j)+Weight(vj,vk)>ve(k)ve(j) + Weight(v_j,v_k) > ve(k),则ve(k)=ve(j)+Weight(vj,vk)ve(k) = ve(j) + Weight(v_j,v_k).以此类推,直至输出全部顶点

2.事件v的最迟发生时间vl(k)

它是指在不推迟整个工程完成的前提下,即保证它的后继事件y在其最迟发生时间v能够 发生时,该事件最迟必须发生的时间。可用下面的递推公式来计算:

vl(汇点)=ve(汇点)

vl(k)=Min{vl(j)Weight(vj,vk)}vl(k) = Min\{vl(j) - Weight(v_j,v_k)\},v为v的任意前驱

注意:在计算vl(k)时,按从后往前的顺序进行,可以在逆拓扑排序的基础上计算

计算vl()vl()值时,按从后往前的顺序进行,在上述拓扑排序中,增设一个栈以记录拓扑序列, 拓扑排序结束后从栈顶至栈底便为逆拓扑有序序列。过程如下:

①初始时,令vl[1..n]=ve[n]vl[1..n] = ve[n]

②栈顶顶点vjv_j出栈,计算其所有直接前驱顶点VKV_K的最迟发生时间,若vl[j]Weight(vk,vj)<vl[k]vl[j] - Weight(v_k,v_j) < vl[k],则vl[k]=vl[j]Weight(vk,vj)vl[k] = vl[j] - Weight(v_k,v_j).以此类推,直至输出全部栈中顶点.

3.活动a1的最早开始时间e(i)

它是指该活动弧的起点所表示的事件的最早发生时间。若边<vk,vj><v_k,v_j>表示活动aja_j,则有e(i)=ve(k)e(i) = ve(k).

4.活动a1的最迟开始时间l(i)

它是指该活动弧的终点所表示事件的最迟发生时间与该活动所需时间之差。若边<vk,vj><v_k,v_j>表示活动aia_i,则有l(i)=vl(j)Weight(vk,vj)l(i) = vl(j) - Weight(v_k,v_j)

5一个活动aia_i的最迟开始时间l(i)l(i)和其最早开始时间e(i)e(i)的差额d(i)=l(i)e(i)d(i) = l(i) - e(i)

它是指该活动完成的时间余量,即在不增加完成整个工程所需总时间的情况下,活动aia_i可以拖延的时间。若一个活动的时间余量为零,则说明该活动必须要如期完成,否则就会拖延整个工程的进度,所以称l(i)e(i)=0l(i) - e(i) = 0l(i)=e(i)l(i) = e(i)的活动aia_i是关键活动。

求关键路径的算法步骤

  1. 从源点出发,令ve(源点)=0,按拓扑有序求其余顶点的最早发生时间ve()
  2. 从汇点出发,令vl(汇点)=ve(汇点),按逆拓扑有序求其余顶点的最迟发生时间vl()
  3. 根据各顶点的ve()值求所有弧的最早开始时间e()
  4. 根据各顶点的vl()值求所有弧的最迟开始时间l()
  5. 求AOE网中所有活动的差额d(),找出所有d()=0的活动构成关键路径