拓扑排序
AOV网
在一个表示工程的有向图中,用顶点表示活动,用弧表示活动之间的优先关系,这样的有向图为顶点表示活动的网,称为AOV网(Active On Vertex NetWork)
拓扑序列
设G=(V,E)是一个具有n个顶点的有向图,V中的顶点序列V1,V2...Vn。若满足从顶点Vi到Vj有一条路径,则在顶点序列中Vi必须在Vj之前,则我们称这样的序列为拓扑序列。
拓扑排序
拓扑排序就是一个有向无环图构造拓扑序列的过程。在构建拓扑排序的过程中:
- 如果此图中全部的顶点都被输出,则说明它是不存在环的
AOV网 - 如果输出的顶点不包含全部顶点,则说明这个图存在环,不是
AOV网
因为在有向图中有可能存在没有路径的点,因此一个有向无环图的拓扑序列不是唯一的。
例如上图中:
C1->C2->C3->c4->C5->C6->C7->C8属于一个拓扑序列
C1->C2->C5->c3->C4->C6->C7->C8也属于一个拓扑序列
思路
- 从
AOV网中选择一个入度为0的顶点输出 - 然后删去此顶点,并删除该顶点发出的全部有向边。
- 继续重复此步骤,直到输出全部顶点或者
AOV网中不存在入度为0的顶点为止。
以上图为例,模拟一下执行流程。
C1以及C1发出的三条边。
C2、C3、C4任意一个顶点,我们选择删除C2以及C2发出的一条边。
C3以及C3发出的一条边。
C4以及C4发出的一条边。
C6以及C6发出的一条边。
C5以及C5发出的一条边。
C7以及C7发出的一条边。
C8。
代码
我们采用邻接表的方式来存储图
/* 邻接表结构****************** */
//边表结点
typedef struct EdgeNode
{
//邻接点域,存储该顶点对应的下标
int adjvex;
//用于存储权值,对于非网图可以不需要
int weight;
//链域,指向下一个邻接点
struct EdgeNode *next;
}EdgeNode;
//顶点表结点
typedef struct VertexNode
{
//顶点入度
int in;
//顶点域,存储顶点信息
int data;
//边表头指针
EdgeNode *firstedge;
}VertexNode, AdjList[MAXVEX];
//图结构
typedef struct
{
AdjList adjList;
//图中当前顶点数和边数
int numVertexes,numEdges;
}graphAdjList,*GraphAdjList;
拓扑排序
Status TopologicalSort_1(GraphAdjList GL){
int *stack = malloc(sizeof(int)*GL->numVertexes);
int top = -1;
int i;
for(i = 0; i < GL->numVertexes; i++) {
//入度为0的顶点入栈
if(GL->adjList[i].in == 0) {
stack[++top] = i;
}
}
int gettop;
int count = 0;
EdgeNode *e;
while(top!=-1) {
gettop = stack[top--];
//打印入度为-0的顶点
printf("%d -> ",GL->adjList[gettop].data);
//计算count
count++;
e = GL->adjList[gettop].firstedge;
//遍历顶点的连通顶点并删除边,如果连通顶点入度为0,则加入到栈中
for(; e; e=e->next) {
if(--(GL->adjList[e->adjvex].in) == 0) {
stack[++top] = e->adjvex;
}
}
}
//如果输出顶点个数小于图的顶点个数,则说明有环,不属于AOV网。
if(count < GL->numVertexes) {
return ERROR;
}
return OK;
}
每一个顶点都会入栈一次,出栈一次,每一条边都会被删掉,因此时间复杂度为O(n+e),n为顶点个数,e为边的个数。
关键路径
拓扑排序的图为AOV网,是一种有向无环图,当我们把AOV图的每一条边加上权值,就变成了AOE网。
AOE网
在一个表示工程的带权有向图中,用顶点表示事件,用弧表示活动,用权值表示活动持续的时间,这种有向图的边表示活动的网,我们称之为AOE网(Activity On Edge NetWork)。
- 没有入边的顶点称为始点或者源点
- 没有出边的顶点称为终点或者汇点
- 一般来说一个工程只有一个开始,一个结束,因此一个
AOE网只有一个始点,一个源点
关键路径
从源点到汇点具有最大长度的路径叫做关键路径,在关键路径上的活动叫关键活动。
1+5+5+6=17小时。
思路
我们可以发现,在关键路径上的步骤必须马不停蹄的执行,如果关键路径上的步骤有了延迟,那么必定会影响这个生产的工期。
接下来需要引出四个概念:
-
etv:Earliest Time of Vertex事件最早发生的时间,也就是顶点的最早发生时间 -
ltv:Latest Time of Vertex事件最晚发生的时间,也就是每个顶点对应的事件最晚发生的时间,如果超出时间将会延误整个工期 -
ete:Earliest Time of Edge活动最早开工的时间,就是弧的最早发生时间 -
lte:Latest Time of Edge活动最晚发生时间,就是不推迟工期的最晚开工时间
etv
对于关键路径上的某一活动(Vi,Vj)以及它的下一活动(Vj,Vk)来说,因为关键路径上的每一个操作都不能停歇。因此(Vi,Vj)的最早发生时间etv[i]和(Vj,Vk)的最晚发生时间lte[j]存在如下关系:
etv[i] = w(Vi,Vj)+lte[j]
这样才能确保(Vi,Vj)没有空余时间,持续进行。
接下来就是如何求解etv、ltv的过程了。
etv的值我们可以在拓扑排序的过程中进行求解,拿顶点C5来说,它必须等活动(C2,C5)和(C3,C5)都执行完才能开始。如果只有(C2,C5)执行完很明显需要等待。
由此可以推导出etv的求解公式:
k = 0时
etv[k] = 0;
k != 0时
etv[k] = max(etv[i]+w(i,k)) 其中i为指向k的顶点,w(i,k)为边的权重
求解etv的代码和拓扑排序的代码类似,因为拓扑排序会按顺序遍历所有的边,我们在遍历边的过程中就可以进行etv的求解。同时将拓扑排序的结果按逆序存储在一个新栈stack2中,以供后续使用。
核心代码如下
int *etv,*ltv; /* 事件最早发生时间和最迟发生时间数组,全局变量 */
int *stack2; /* 用于存储拓扑序列的栈 */
int top2; /* 用于stack2的指针*/
//进行一遍拓扑排序,求解etv
Status TopologicalSort_1(GraphAdjList GL){
int *stack = malloc(sizeof(int)*GL->numVertexes);
int top = -1;
int i;
for(i = 0; i < GL->numVertexes; i++) {
//入度为0的顶点入栈
if(GL->adjList[i].in == 0) {
stack[++top] = i;
}
}
int gettop;
int count = 0;
EdgeNode *e;
while(top!=-1) {
gettop = stack[top--];
//计算count
count++;
//栈顶元素入栈stack2
stack2[++top2] = gettop;
e = GL->adjList[gettop].firstedge;
//遍历顶点的连通顶点并删除边,如果连通顶点入度为0,则加入到栈中
for(; e; e=e->next) {
if(--(GL->adjList[e->adjvex].in) == 0) {
stack[++top] = e->adjvex;
}
//计算栈顶顶点一遍元素的各个邻接点的etv值,因为拓扑排序的特性,栈顶的顶点一定是没有入度的顶点。
if(etv[gettop]+e->weight > etv[e->adjvex]) {
etv[e->adjvex] = etv[gettop]+e->weight;
}
}
}
//如果输出顶点个数小于图的顶点个数,则说明有环,不属于AOV网。
if(count < GL->numVertexes) {
return ERROR;
}
return OK;
}
ltv事件最晚发生的时间
stack2。因为stack2中保存的是拓扑序列的逆序,可以确保以栈顶顶点为起点的边的另一顶点肯定已经弹出,就是说ltv值已经求解,通过这一点我们可以来计算ltv。
通过etv的求解我们可以得到C8的最早发生时间为17。初始化ltv数组的值为17。
对于任意顶点k,它的ltv值存在如下公式
k = 8时
ltv[k] = 17;
k < 8时
ltv[k] = min(ltv[k], ltv[i] - w(k,i)),其中i为以k为出发点的边的另一个顶点。
计算出etv和ltv之后,再来看ete和lte。对于边(i,j)来说,最早发生时间和i的最早发生时间etv[i]是一致的,最晚发生时间和j的最晚发生时间ltv[j]的关系为etv[i] = ltv[j]-w(i,j)。这个条件可以来判断一条边是否为关键路径。
代码
void CriticalPath_1(GraphAdjList GL) {
TopologicalSort_1(GL);
//打印etv数组(事件最早发生时间)
printf("etv:\n");
int i;
for(i = 0; i < GL->numVertexes; i++)
printf("etv[%d] = %d \n",i,etv[i]);
printf("\n");
ltv = (int*)malloc(sizeof(int)*GL->numVertexes);
//初始化ltv
for(i = 0; i < GL->numVertexes; i++) {
ltv[i] = etv[GL->numVertexes-1];
}
int gettop;
EdgeNode *e;
while(top2 != 0) {
gettop = stack2[top2--];
e = GL->adjList[gettop].firstedge;
for(; e; e = e->next) {
//求解ltv
if(ltv[gettop] > (ltv[e->adjvex] - e->weight)) {
ltv[gettop] = ltv[e->adjvex] - e->weight;
}
}
}
//打印ltv 数组
printf("ltv:\n");
for (i = 0 ; i < GL->numVertexes; i++) {
printf("ltv[%d] = %d \n",i,ltv[i]);
}
int k,ete,lte;
for(i = 0; i < GL->numVertexes; i++) {
e = GL->adjList[i].firstedge;
for(; e; e = e->next) {
k = e->adjvex;
ete = etv[i];
lte = ltv[k] - e->weight;
if(ete == lte) {
printf("<%d-%d> length:%d\n",GL->adjList[i].data, GL->adjList[k].data, e->weight);
}
}
}
}
时间复杂度:
- etv数组时间复杂度为
O(n+e)。 - ltv数组时间复杂度为
O(n+e)。 - 判断是否为关键路径复杂度为
O(n+e)。 - 时间复杂度为
O(n)