拓扑排序以及求解关键路径都是属于有向无环网的应用
介绍2个概念
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;inumVertexes;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(countnumVertexes)
return ERROR;//存在环,无法得到拓扑序列
else
return OK;
}
拓扑排序:解决工程能否顺序进行的问题
求解关键路径:解决工程完成需要的最短时间问题
解决工程完成需要最短时间问题前提是工程一定能顺利进行和完成,故拓扑排序是求解关键路径的基础。不存在拓扑序列的有向图无法求解关键路径,求解关键路径之前需进行拓扑排序。
新概念:AOE网
在一个表示工程的带权有向图中,用顶点表示事件,用有向边表示活动,用边上的权值表示活动的持续时间,这种有向图的边表示活动的网,我们称之为AOE(Activity on Edge Network)。正常情况下AOE有一个源点一个汇点
始点/源点:有向图中没有入边的顶点,表示一个工程的开始 终点/汇点:有向图中没有出边的顶点,表示一个工程的结束
如下图所示:
特别强调:
后面的最早最晚时间点根据此处理解,比如事件的最早发生时间,最晚发生时间等等
再介绍几个概念:
路径长度:路径上各个活动所持续时间之和称为路径长度
关键路径:从源点到汇点具有最大长度的路径叫关键路径
关键活动:关键路径上的活动叫关键活动
我们的目标:解决工程完成需要的最短时间问题,尽可能缩短工程完成的耗时。
缩短工期长度:只有缩短关键路径上关键活动的时间才能减少整个工期长度
所以,我们需要找到关键路径,缩短关键路径上关键活动的时间,以减少整个工程完成的时间
关键活动:活动的最早开始时间和最晚开始时间相等的话就是意味着此活动是关键活动,活动间的路径为关键路径,否则不是
定义几个参数:
我们由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;inumVertexes;i++)
if(G->adjList[i].in==0)
stack[++top]=i;//将入度为0的顶点入栈
etv=(int*)malloc(G->numVertexes*sizeof(int));
for(int i=0;inumVertexes;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(countnumVertexes)
return ERROR;//出错,有环
else
return OK;//存在拓扑序列,并存在Stack2中
}
如何理解:
接下来看关键路径算法:
void CriticalPath(GraphAdjList G)
{
EdgeNode *e;
int ete,lte;//相应活动的最早发生时间和最迟发生时间变量
TopologicalSort(G);//先进行拓扑排序
int ltv=(int*)malloc(G->numVertexes*sizeof(int));//事件最晚发生的时间
for(int i=0;inumVertexes;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的领接顶点,有弧
if(ltv[k]-e->weightweight;//求gettop顶点的最晚发生时间
}//已知某顶点后序领接顶点事件的最晚发生时间,求该顶点最晚发生时间。见正文分析
}
for(int j=0;jnumVertexes;j++)
{ //对每一条弧表示的活动,求活动的最早开始时间,和活动最晚发生时间
for(e=G->adjList[gettop].firstedge;e;e=e->next)
{
int k=e->adjvex;//k是顶点j的后序领接顶点,即存在弧表示一个活动
ete=etv[j];//活动的最早开始时间就等于顶点j表示事件的最早发生时间,
lte=ltv[k]-e->weight;//活动的最晚发生时间就等于顶点k最晚发生时间减活动所需时间
if(ete==lte){ //两者相等则此活动没有任何空闲,其在关键路径上,且为关键活动
printf(" length:%d,",//打印输出关键路径
G->adjList[j].data,G->adjList[k].data,e->weight);
}
}
}
}
如何理解以上式子:
见下图:
对于活动的最早发生时间ete和最晚发生时间lte:
总结:
参考资料:大话数据结构