拓扑排序主要是为解决一个工程能否顺序进行的问题,但有时我们还需要解决工程完成需要的最短时间问题。如果我们要对一个流程图获得最短时间,就必须要分析它们的拓扑关系,并且找到当中最关键的流程,这个流程的时间就是最短时间。
在前面讲了AOV网的基础上,来介绍一个新的概念。在一个表示工程的带权有向图中,用顶点表示事件,用有向边表示活动,用边上的权值表示活动的持续时间,这种有向图的边表示活动的网,称之为AOE网(Activity On edge Network)。由于一个工程,总有一个开始,一个结束,在正常情况下,AOE网只有一个源点一个汇点。
既然AOE网是表示工程流程的,所以就具有明显的工程属性。只有在某顶点代表的事件发生后,从该顶点出发的各活动才能开始。只有在进入某顶点的各活动都已经结束,该顶点代表的事件才能发生。
尽管AOV网和AOE网都是用来对工程建模的,但它们还是有很大的区别,主要体现在AOV网是顶点表示活动的网,它只描述活动之间的制约关系,而AOE网是用边表示活动的网,边上的权值表示活动持续的时间,如图7-9-3所示两图的对比。因此,AOE网是要建立在活动之间制约关系没有矛盾的基础之上,再来分析完成整个工程需要多少时间,或者为缩短完成工程所需时间,应当加快哪些活动等问题。
我们把路径上各个活动所持续的时间之后称为路径长度,从源点到汇点具有最大长度的路径叫关键路径,在关键路径上完成的活动叫关键活动。显然就图7-9-3的AOE网而言,开始->发动机完成->部件集中到位->组装完成就是关键路径,路径长度为5.5。
如果我们需要缩短整个工期,去改进轮子的生产效率,哪怕改动成0.1也无益于整个工期的变化,只有缩短关键路径上的关键活动时间才才可以减少整个工期长度。例如如果发动机制造缩短为2.5,整车组装缩短为1.5,那么关键路径就为4.5,整整缩短了一天的时间。
如果某项活动的最早开始时间和最晚开始时间一样,表示中间没有空隙,则此项活动就为关键活动。为此,我们需要定义以下几个参数。
1、事件的最早发生时间 etv(earliest time of vertex):即顶点vk 的最早发生时间。
2、事件的最晚发生时间 ltv(latest time of vertex):即顶点vk 的最短发生时间。也就是每个顶点对应的事件最晚需要开始的时间,超出此时间将会延误整个工期。
3、活动的最早开工时间 ete (earliest time of edge):即弧ak 的最早发生时间。
4、活动的最晚开工时间 lte (latest time of edge ):即弧ak 的最晚发生时间,也就是不推迟工期的最晚开工时间。
我们首先求得1和2,而 ete 本来是表示活动<vk, vj> 的最早开工时间,是针对弧来说的,但只有此弧的弧尾顶点vk的事件发生了,它才可以开始,因此ete = etv[k]。
而lte 表示的是活动<vk, vj> 的最晚开工时间,但此活动再晚也不能等vj 事件发生才开始,所以lte = ltv[j] - len<vk, vj> 。
最终,我们再来判断ete 和 lte 是否相等,相等意味着活动没有任何空闲,是关键路径,否则就不是。
现在来谈谈如何求etv 和 ltv,以下图为例:
假设我们现在已经求得顶点v0对应的 etv[0] = 0,顶点v1对应的etv[1] = 3, 顶点v2对应的etv[2] = 4, 现在我们需要求顶点v3对应的etv[3],要开始v3那么前面耗时最长的工程应该刚好完成,其实就是求etv[1] + len<v1, v3> 与 etv[2] + len<v2, v3> 的较大值。显然 3+5 < 4+8, 得到etv[3] = 12, 如图所示。
由此我们也可以得出计算顶点vk的最早发生时间即求etv[k]的公式是:
求事件的最早发生时间etv的过程,就是按照拓扑排序的顺序依次求。
假如我们现在已经求得v6, v7 顶点的ltv值,现在要求v4 的ltv 值,由邻接表可得到v4 有两条弧<v4, v6>, <v4, v7>,可以得到
ltv[4] = min(ltv[7] - 4, ltv[6] - 9) = 15(再延迟工程v7就要耽搁了),如图7-9-8所示。
可以发现,在计算ltv时,其实是把拓扑序列倒过来进行而已,因此可以得到计算顶点vk最晚发生时间即求ltv[k] 的公式是:
以上部分信息来自大话数据结构
接下来看看怎么实现:
#include <iostream>
#include <stdio.h>
#include <stack>
using namespace std;
const int MAXSIZE=100;
typedef struct EdgeNode //边表结点
{
int adjvex; //存储该顶点的下标
int weight; //对应弧的权值
struct EdgeNode *next;
}EdgeNode;
typedef struct VertexNode //顶点表结点
{
int in; //入度
int data; //顶点值
EdgeNode *firstedge; //边表头指针
}VertexNode,AdjList[MAXSIZE];
typedef struct
{
AdjList adjList;//顶点表结点合集数组
int numVertexes,numEdges; //顶点数和边数
}graphAdjList,*GraphAdjList;
int *etv,*ltv; //事件最早发生时间和最迟发生时间数组
int *stack2; //用于逆拓扑排序的栈
int top2; //用于stack2的栈指针
bool TopologicalSort(GraphAdjList &GL)
{
EdgeNode *e;
int k,gettop;
int top=0;
int count=0;
int *stack; //拓扑排序栈
stack=(int *)malloc(GL->numVertexes*sizeof(int));
for (int i=0; i<GL->numVertexes; i++)
{
if(GL->adjList[i].in==0) //入度为0的元素进栈
{
stack[++top]=i;
}
}
top2=0;
etv=(int*)malloc(sizeof(int)*GL->numVertexes);
for (int i=0; i<GL->numVertexes; i++)
{
etv[i]=0; //初始化
}
stack2=(int*)malloc(sizeof(int)*GL->numVertexes);
while (top!=0)
{
gettop=stack[top--];
count++; //元素个数计数,判断是否存在环
stack2[++top2]=gettop; //存入逆拓扑排序栈中
for (e=GL->adjList[gettop].firstedge; e; e=e->next)
{
k=e->adjvex;
if(!(--GL->adjList[k].in))
{
stack[++top]=k;
}
if(etv[gettop]+e->weight>etv[k]) //更新最早发生时间
{
etv[k]=etv[gettop]+e->weight;
}
}
}
if(count<GL->numVertexes)
{
return false;
}
return true;
}
void CriticalPath(GraphAdjList &GL)
{
EdgeNode *e;
int gettop,k;
int ete,lte;
TopologicalSort(GL);
ltv=(int*)malloc(sizeof(int)*GL->numVertexes); //最迟发生时间数组
for (int i=0; i<GL->numVertexes; i++)
{
ltv[i]=etv[stack2[GL->numVertexes]]; //初始化终点的最早发生时间,终点的最早发生时间和最迟发生时间是一致的(终点不一定是n-1,而是拓扑排序的最后一个数)
}
while (top2!=0)
{
gettop=stack2[top2--];
for (e=GL->adjList[gettop].firstedge; e; e=e->next)
{
k=e->adjvex;
if(ltv[k]-e->weight<ltv[gettop]) //更新最迟发生时间
{
ltv[gettop]=ltv[k]-e->weight; //可以达到的最晚时间
}
}
}
for (int j=0; j<GL->numVertexes; j++)
{
for (e=GL->adjList[j].firstedge; e; e=e->next)
{
k=e->adjvex;
ete=etv[j];
lte=ltv[k]-e->weight; //此条路线的最晚时间
if(ete==lte) //判断是否是关键路径
{
printf("%d %d %d\n",GL->adjList[j].data,GL->adjList[k].data,e->weight);
}
}
}
}
void Init(GraphAdjList &GL)
{
GL=(GraphAdjList)malloc(sizeof(graphAdjList));
}
int main()
{
GraphAdjList GL;
EdgeNode *p;
int u,v,w;
Init(GL);
printf("请输入顶点个数和边的个数:");
scanf("%d%d",&GL->numVertexes,&GL->numEdges);
printf("请输入边的顶点,及其权值 u,v,w:\n");
for (int i=0; i<GL->numVertexes; i++)
{
GL->adjList[i].in=0;
GL->adjList[i].data=i;
GL->adjList[i].firstedge=NULL;
}
for (int i=0; i<GL->numEdges; i++)
{
p=(EdgeNode *)malloc(sizeof(EdgeNode));
scanf("%d%d%d",&u,&v,&w);
p->adjvex=v;
p->weight=w;
p->next=GL->adjList[u].firstedge;
GL->adjList[u].firstedge=p;
GL->adjList[v].in++;
}
printf("关键路径及其权值:\n");
printf("-----\n");
CriticalPath(GL);
return 0;
}
按照上图运行结果:
这就是按照数据结构这本书的要求来安排数据存储形式,感觉好庞大,把邻接表搞的很复杂,其实可以写简略一点的,也来练练手吧。
数组形式的邻接表+先求出拓扑序列
#include <stdio.h>
#include <iostream>
#include <stack>
using namespace std;
const int MAXSIZE=100;
int head[MAXSIZE],etv[MAXSIZE],ltv[MAXSIZE],cnt,n,m; //head为表头
bool vis[MAXSIZE];
stack<int>s; //顺拓扑序列
stack<int>s2; //逆拓扑序列
struct node
{
int v,w;
int next;
}EdgeNode[MAXSIZE];
void AddEdge(int u,int v,int w) //建表
{
EdgeNode[cnt].v=v;
EdgeNode[cnt].w=w;
EdgeNode[cnt].next=head[u];
head[u]=cnt++;
}
void DFS(int u) //求拓扑序列
{
vis[u]=true;
for (int i=head[u];i!=-1; i=EdgeNode[i].next)
{
if(!vis[EdgeNode[i].v])
{
DFS(EdgeNode[i].v);
}
}
s.push(u);
}
void Etv() //求最早发生时间
{
for (int i=0; i<n; i++)
{
etv[i]=0;
}
while (!s.empty())
{
int top=s.top();
s2.push(top);
s.pop();
for (int i=head[top]; i!=-1; i=EdgeNode[i].next)
{
int v=EdgeNode[i].v;
if(etv[v]<etv[top]+EdgeNode[i].w)
{
etv[v]=etv[top]+EdgeNode[i].w;
}
}
}
}
void Ltv()//最迟发生时间
{
for (int i=0; i<n; i++)
{
ltv[i]=etv[s2.top()]; //初始化为终点的最早时间
}
while (!s2.empty())
{
int top=s2.top();
s2.pop();
for (int i=head[top]; i!=-1; i=EdgeNode[i].next)
{
int v=EdgeNode[i].v;
if(ltv[top]>ltv[v]-EdgeNode[i].w)
{
ltv[top]=ltv[v]-EdgeNode[i].w;
}
}
}
}
void CriticalPath()
{
Etv();
Ltv();
for (int i=0; i<n; i++)
{
for (int j=head[i]; j!=-1; j=EdgeNode[j].next)
{
int ete,lte,v;
v=EdgeNode[j].v;
ete=etv[i];
lte=ltv[v]-EdgeNode[j].w;
if(ete==lte) //判断是否是关键路径
{
printf("%d %d %d\n",i,v,EdgeNode[j].w);
}
}
}
}
int main()
{
int degree[MAXSIZE];
printf("请输入顶点个数和边的个数:");
scanf("%d%d",&n,&m);
printf("请输入边的顶点,及其权值 u,v,w:\n");
memset(head, -1, sizeof(head));
memset(vis, false, sizeof(vis));
memset(degree, 0, sizeof(degree));
cnt=0;
for (int i=0;i<m; i++)
{
int u,v,w;
scanf("%d%d%d",&u,&v,&w);
AddEdge(u,v,w);
degree[v]++;
}
for (int i=0; i<n; i++)
{
if(!degree[i])
{
DFS(i); //从入度为0的点出发
break;
}
}
printf("-------\n");
CriticalPath();
return 0;
}