拓扑排序以及求解关键路径
拓扑排序以及求解关键路径都是属于有向无环网的应用
拓扑排序:解决工程能否顺序进行的问题
介绍2个概念
AOV网:在一个表示工程的有向图中,用顶点表示活动,用弧表示活动之间的优先关系,这样的有向图为顶点表示活动的网,我们称之为AOV网。
- 弧表示活动之间的某种制约关系:比如演职人员确定了,场地联系好了,才可以开始进场拍摄。
- AOV网中不能存在回路,即不能存在环,否则某个活动的开始要以自己完成作为先决条件,这是不现实的
拓扑序列:
设G=(V,E)是一个具有n个顶点的有向图,中的顶点序列~,满足若从顶点到有一条路径,则在顶点序列中顶点必定在之前,则我们称这样的顶点序列为一个拓扑序列
拓扑排序:对一个有向图构造拓扑序列的过程。
如果此网的全部顶点都被输出,则说明它是不存在环或回路的AOV网;
如果输出的顶点数少于总共的顶点数,则说明这个网存在环或回路,不是AOV网;
拓扑排序算法:
基本思路:
从AOV网中选择一个入度为0的顶点输出,然后删除此顶点,并且删除以此顶点为尾的弧,继续重复此步骤,直到输出全部顶点或AVO中不存在入度为0的顶点为止。
确定使用的数据结构:由于需要删除顶点和边,所以用邻接表比较方便
typedef struct EdgeNode
{
int adjvex;
int weight;
struct EdgeNode *node;
}EdgeNode;//边结点(带有权值)
typedef struct VertexNode
{
int in;//该顶点入度
int data;//顶点域
EdgeNode *firstedge;//边表头指针
}VertexNode,AdjList[MAXVEX];//顶点表结点
typedef struct
{
AdjList adjList;
int numVertexes,numEdges;//当前图的顶点数和边数
}graphAdjList,*GraphAdjList;
看代码:
//这里使用栈存储入度为0的顶点,避免每个查找都要遍历顶点表找有没有入度为0的顶点
Status TopologicalSort(GraphAdjList G)
{
EdgeNode *e;
int count=0;//统计输出顶点个数
int top=0;//栈指针下标
int *stack;
stack=(int*)malloc(G->numVertexes*sizeof(int));
for(int i=0;i<G->numVertexes;i++)
if(G->adjList[i].in==0)
stack[++top]=i;//将入度为0的顶点入栈
while(top!=0)//top!=0说明栈非空,存在入度为0的顶点
{
int gettop=stack[top--];
print("%d --> ",G->adjList[gettop].data);
count++;//统计输出顶点数
for(e=G->adjList[gettop].firstedge;e;e=e->next)
{//删除以顶点k为尾的弧,并将弧头顶点的入度-1
int k=e->adjvex;
if(!(--G->adjList[k].in)//先自减,然后判断减完是否等于0
stack[++top]=k;//变成入度为0的顶点需要入栈
}
}
if(count<G->numVertexes)
return ERROR;//存在环,无法得到拓扑序列
else
return OK;
}
- 拓扑排序得到的拓扑序列不唯一
- 对于n个顶点和e条边的AOV网,算法时间复杂度为O(n+e)
关键路径:解决工程完成需要的最短时间问题
拓扑排序:解决工程能否顺序进行的问题
求解关键路径:解决工程完成需要的最短时间问题
解决工程完成需要最短时间问题前提是工程一定能顺利进行和完成,故拓扑排序是求解关键路径的基础。不存在拓扑序列的有向图无法求解关键路径,求解关键路径之前需进行拓扑排序。
新概念:AOE网
在一个表示工程的带权有向图中,用顶点表示事件,用有向边表示活动,用边上的权值表示活动的持续时间,这种有向图的边表示活动的网,我们称之为AOE(Activity on Edge Network)。正常情况下AOE有一个源点一个汇点
始点/源点:有向图中没有入边的顶点,表示一个工程的开始 终点/汇点:有向图中没有出边的顶点,表示一个工程的结束
如下图所示:
特别强调:
- 某个顶点代表的事件发生后,从该顶点出发的各活动可以立马开始
- 只有在进入某顶点的各活动都已经结束,该顶点代表的事件才能发生
后面的最早最晚时间点根据此处理解,比如事件的最早发生时间,最晚发生时间等等
再介绍几个概念:
路径长度:路径上各个活动所持续时间之和称为路径长度
关键路径:从源点到汇点具有最大长度的路径叫关键路径
关键活动:关键路径上的活动叫关键活动
我们的目标:解决工程完成需要的最短时间问题,尽可能缩短工程完成的耗时。
缩短工期长度:只有缩短关键路径上关键活动的时间才能减少整个工期长度
所以,我们需要找到关键路径,缩短关键路径上关键活动的时间,以减少整个工程完成的时间
关键活动:活动的最早开始时间和最晚开始时间相等的话就是意味着此活动是关键活动,活动间的路径为关键路径,否则不是
定义几个参数:
- etv(earliest time of vertex):顶点事件最早发生的时间点
- ltv(latest time of vertex):顶点事件最晚发生的时间点,最晚需要开始的时间,超过此时间将会延误工期
- ete(earliest time of edge):活动最早发生的时间点
- lte(latest time of edge):活动最晚发生的时间点,不推迟工期的最晚开工时间
我们由1和2来求得3和4,然后根据相对应的活动的ete和lte是否相等判断是否是关键活动
先求etv,求etv的过程,就是从头到尾找拓扑序列的过程。
因此在求关键路径之前需调用一次拓扑排序算法计算etv和存储得到的拓扑序列。
int *etv,*ltv;//事件发生的最早时间和最迟发生时间数组
int *stack2;//存储拓扑序列
int top2;//stack2的栈顶指针
改进过的拓扑排序算法:
不再打印拓扑序列而是存储拓扑序列到Stack2,我们能得到一个拓扑序列,因此能确定工程的源点和终点
Status TopologicalSort(GraphAdjList G)
{
EdgeNode *e;
int count=0;//统计输出顶点个数
int top=0;//栈指针下标
int *stack;
stack=(int*)malloc(G->numVertexes*sizeof(int));
for(int i=0;i<G->numVertexes;i++)
if(G->adjList[i].in==0)
stack[++top]=i;//将入度为0的顶点入栈
etv=(int*)malloc(G->numVertexes*sizeof(int));
for(int i=0;i<G->numVertexes;i++)
etv[i]=0;//初始化为0
int top2=0;//栈顶指针
stack2=(int*)malloc(G->numVertexes*sizeof(int));
while(top!=0)
{
int gettop=stack[top--];//去stack中入度为0的顶点
count++;//统计顶点个数,用于最后判断有向图是否有环
stack2[++top2]=gettop;//将从stack中出来的入度为0的顶点入stack2,stack2中存放拓扑序列
for(e=G->adjList[gettop].firstedge;e;e=e->next)
{
int k=e->adjvex;
if(!(--G->adjList[k].in)//先自减,然后判断减完是否等于0
stack[++top]=k;//变成入度为0的顶点需要入栈
if((etv[gettop]+e->weight)>etv[k])//求各顶点事件最早发生时间
etv[k]=etv[gettop]+e->weight;
}
}
if(count<G->numVertexes)
return ERROR;//出错,有环
else
return OK;//存在拓扑序列,并存在Stack2中
}
如何理解:
- 当顶点k为源点时,时间最早开始时间等于0;
- 当顶点k不是源点时,已知该顶点的前驱顶点的最早开始时间,只有进入该顶点的各活动已经结束,该顶点代表的事件才能发生。即该顶点k最早发生时间=max(顶点i事件最早发生的时间点+顶点之间活动的耗时)。举个例子:一个老师最早在下午2点的时候就布置了作业,已知小明大概要做2个小时,小李要做1个半小时,小红要做50分钟,现在问老师最早什么时候能收齐作业。答案不言而喻,需要等小明做完才能收齐作业本吧!应该最早4点才能收齐,3点收作业小明做不完的呀!
- 如下图:
接下来看关键路径算法:
void CriticalPath(GraphAdjList G)
{
EdgeNode *e;
int ete,lte;//相应活动的最早发生时间和最迟发生时间变量
TopologicalSort(G);//先进行拓扑排序
int ltv=(int*)malloc(G->numVertexes*sizeof(int));//事件最晚发生的时间
for(int i=0;i<G->numVertexes;i++)//对ltv初始化
ltv[i]=etv[G->numVertexes-1];//为啥初始化成etv[G->numVertexes-1]?
//因为根据之前得到拓扑排序得到终点的最早的发生时间,终点事件的最早发生时间等于最晚发生时间
//为啥呢?既然都到终点了,还分早晚?这个终点的最早发生时间意味着无论如何前面的活动都已经完成
//此刻已经无需等待,最晚时间没有意义,它之前顶点的最晚时间必须小于终点的最晚发生时间
while(top2!=0)//拓扑序列的逆序,我们需要从终点倒推出每个事件的最晚发生时间
{
int gettop=stack2[top2--];//拓扑序列倒序出栈
for(e=G->adjList[gettop].firstedge;e;e=e->next)
{
int k=e->adjvex;//k是gettop的领接顶点,有弧<gettop,k>
if(ltv[k]-e->weight<ltv[gettop])//已知顶点k的最晚发生时间
ltv[gettop]=ltv[k]-e->weight;//求gettop顶点的最晚发生时间
}//已知某顶点后序领接顶点事件的最晚发生时间,求该顶点最晚发生时间。见正文分析
}
for(int j=0;j<G->numVertexes;j++)
{ //对每一条弧表示的活动,求活动的最早开始时间,和活动最晚发生时间
for(e=G->adjList[gettop].firstedge;e;e=e->next)
{
int k=e->adjvex;//k是顶点j的后序领接顶点,即存在弧<j,k>表示一个活动
ete=etv[j];//活动的最早开始时间就等于顶点j表示事件的最早发生时间,
lte=ltv[k]-e->weight;//活动的最晚发生时间就等于顶点k最晚发生时间减活动所需时间
if(ete==lte){ //两者相等则此活动没有任何空闲,其在关键路径上,且为关键活动
printf("<V%d,V%d> length:%d,",//打印输出关键路径
G->adjList[j].data,G->adjList[k].data,e->weight);
}
}
}
}
如何理解以上式子:
- 当k=n-1时,表示顶点k是终点。终点的最晚发生时间和最早发生时间是相等的。至于为什么!既然都到终点了,还分早晚?这个终点的最早发生时间意味着无论如何前面的活动都已经完成,此刻已经无需等待,最晚时间没有意义。工程应尽快完成为好。
- 当k!=n-1时,表示顶点k不是终点,且已知顶点j,并且存在活动<k,j>。顶点k最晚发生时间=Min(顶点j最晚发生时间-活动<k,j>活动耗费时间)。举个例子:假设老师打算布置一次作业,老师最晚下午4点要收上去批改作业,现在已知小明大概要做2个小时,小李要做1个半小时,小红要做50分钟,现在好了,为了到时候最晚4点能都收好所以作业,老师应该最晚几点把题目给大家布置出来。当然是最晚2点!如果是3点布置作业会有同学无法完成,所以最晚2点布置,当然1点布置也可以让大家完成。
见下图:
对于活动的最早发生时间ete和最晚发生时间lte:
总结:
- 算法的时间复杂度为O(n+e)
- AOE网可能存在多条关键路径,单独提高一条关键路径上的关键活动速度并不能导致整个工程缩短工期,需同时提高几条关键路径上活动的速度。
参考资料:大话数据结构