图论--关键路径

拓扑排序主要是为解决一个工程能否顺序进行的问题,但有时我们还需要解决工程完成需要的最短时间问题。如果我们要对一个流程图获得最短时间,就必须要分析它们的拓扑关系,并且找到当中最关键的流程,这个流程的时间就是最短时间。

在前面讲了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;
}

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值