图——基本的图算法(四)关键路径_求关键路径是以拓扑排序为基础的为什么-程序员宅基地

图——基本的图算法(四)关键路径

关键路径的算法是建立在拓扑排序的基础之上的,这个算法中用到了拓扑排序,所以在这里先以拓扑排序开篇。

1. 什么是拓扑排序?
举个例子先:一个软件专业的学生学习一系列的课程,其中一些课程必须再学完它的基础的先修课程才能开始。如:在《程序设计基础》和《离散数学》学完之前就不能开始学习《数据结构》。这些先决条件定义了课程之间的领先(优先)关系。这个关系可以用有向图更清楚地表示。图中顶点表示课程,有向边表示先决条件。若课程i是课程j的先决条件,则图中有弧(i,j)。若要对这个图中的顶点所表示的课程进行拓扑排序的话,那么排序后得到的序列,必须是按照先后关系进行排序,具有领先关系的课程必然排在以它为基础的课程之前,若上例中的《程序设计基础》和《离散数学》必须排在《数据结构》之前。进行了拓扑排序之后的序列,称之为拓扑序列。
2. 如何实现拓扑排序?
很简单,两个步骤:
1. 在有向图中选一个没有前驱的顶点且输出。
2. 从图中删除该顶点和以它为尾的弧。
重复上述两步,直至全部顶点均已输出,或者当前图中不存在无前驱的顶点为止。后一种情况则说明有向图中存在环。
3. 什么是关键路径?
在学习关键路径前,先了解一个AOV网和AOE网的概念:
这里写图片描述
用顶点表示活动,用弧表示活动间的优先关系的有向图:
称为顶点表示活动的网(Activity On Vertex Network),简称为AOV网。
与AOV网对应的是AOE(Activity On Edge)网即边表示活动的网。
AOE网是一个带权的有向无环图。
网中只有一个入度为零的点(称为源点)和一个出度为零的点(称为汇点)。
其中,顶点表示事件(Event),弧表示活动,权表示活动持续的时间。
通常,AOE网可用来估算工程的完成时间。
假如汽车生产工厂要制造一辆汽车,制造过程的大概事件和活动时间如上图AOE网:
我们把路径上各个活动所持续的时间之和称为路径长度,从源点到汇点具有最大长度的路径叫关键路径,在关键路径上的活动叫关键活动。
那么,显然对上图AOE网而言,所谓关键路径:
开始–>发动机完成–>部件集中到位–>组装完成。路径长度为5.5。
如果我们试图缩短整个工期,去改进轮子的生产效率,哪怕改动0.1也是无益的。
只有缩短关键路径上的关键活动时间才可以减少整个工期的长度。
例如如果制造发动机缩短为2.5天,整车组装缩短为1.5天,那么关键路径为4.5。
工期也就整整缩短了一天时间。
好吧! 那么研究这个关键路径意义何在?
假定上图AOE网中弧的权值单位为小时,而且我们已经知道黑深色的那一条为关键路径。
假定现在上午一点,对于外壳完成事件而言,为了不影响工期:
外壳完成活动最早也就是一点开始动工,最晚在两点必须要开始动工。
最大权值3表示所有活动必须在三小时之后完成,而外壳完成只需要2个小时。
所以,这个中间的空闲时间有一个小时,为了不影响整个工期,它必须最迟两点动工。
那么才可以保证3点时与发动机完成活动同时竣工,为后续的活动做好准备。
对AOE网有待研究的问题是:
(1)完成整个工程至少需要多少时间?
(2)那些活动是影响工程进度的关键?
今天研究是实例如下图所示:
这里写图片描述
假想是一个有11项活动的AOE网,其中有9个事件(V1,V2,V3…V9)。
每个事件表示在它之前的活动已经完成,在它之后的活动可以开始。
如V1表示整个工程开始,V9表示整个共结束,V5表示a4和a5已经完成,a7和a8可以开始。
关键路径算法
为了更好的理解算法,我们先需要定义如下几个参数:
(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的最晚发生时间,也就是不推迟工期的最晚开工时间。
然后根据最早开工时间ete[k]和最晚开工时间lte[k]相等判断ak是否是关键路径。
将AOE网转化为邻接表结构如下图所示:
这里写图片描述
求事件 的最早发生时间etv的过程,就是我们从头至尾找拓扑序列的过程,因此,在求关键路径之前,需要先调用一次拓扑序列算法来计算etv和拓扑序列列表。为此我们首先在程序开始处声明几个全局变量。

int  *etv,*ltv;//事件最早发生时间和最迟发生时间数组
int  *stack2;//用于存储拓扑序列的栈
int   top2; //用于stack2的指针
//下面是改进过的求拓扑序列算法
status  TopologicalSort(GraphAdjList   GL)
{
      EdgeNode   *e;
      int   i,k,gettop;
      int   top=0;    //用于栈指针下标
      int    count=0;   //用于统计输出顶点的个数
      int     *stack;  //创栈将入度为0的顶点入栈
     stack=(int  *)malloc(GL->numVertexes*sizeof(int));
     for(i=0;i<GL->numVertexes;i++)
          if(0==GL->adjList[i].in) stack[++top]=i;
      top2=0;
     etv=(int  *)malloc(GL-numVertexes*sizeof(int)); //事件最早发生时间
      for(i=0;i<GL->numVertexes;i++)
        etv[i] = 0;
     stack2=(int  *)malloc(GL->numVertexes*sizeof(int));
    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 ERROR;
        else
            return OK;
}
 

第11~15行为初始化全局变量etv数组、top2和stack2的过程。第21行就是将本是要输出的拓扑序列压入全局栈stack2中。第27~28行很关键,它是求etv数组的每一个元素的值。比如说,假如我们已经求得顶点v0对应的etv[0]=0,顶点v1对应的etv[1]=3,顶点v2对应的etv[2]=4,现在我们需要求顶点v3对应的etv[3],其实就是求etv[1]+len(v1,v3)与etv[2]+len(v2,v3)的较大值。显然3+5<4+8,得到etv[3]=12。如图所示,在代码中e->weight就是当前弧的长度。
这里写图片描述
下面我们来看一下求关键路径的算法代码
下面我们来看一下求关键路径的算法代码

void CriticalPath(GraphAdjList GL)

{
        EdgeNode   *e;
        int   i,gettop,k,j;
        int ete,lte;
       TopologicalSort(GL);   //求拓扑序列,计算数组etv和stack2的值
       ltv=(int  *)malloc(GL->numVertexes*sizeof(int));//事件最晚发生时间
       for(i=0;i<GL->numVertexes;i++)
                   ltv[i] =etv[GL->numVertexes-1];//初始化ltv
       while(top2!=0)
       {
                 gettop = stack2[top2-1];
                 for(e=GL->adjList[gettop].firstedge;e;e=e->next)
                 {//求各顶点事件的最迟发生时间ltv值
                       k=e->adjvex;
                      if(ltv[k]-e->weight<ltv[gettop])//各点事件最晚发生时间
                           ltv[gettop] =ltv[k]-e->weight;
                 }
                  for(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(“<v%d,v%d>length:%d,”,GL->adjList[j].data,GL->adjList[k].data,e->weight);
                          }
                  }
        }
}
 

 

1. 基本概念

(1)AOV网(Activity On Vertex Network)
AOV网是一个表示工程的有向图中,其中的顶点用来表示活动,弧则用来表示活动之间的优先关系。举个简单的例子,假定起床后可以边煮水,边刷牙洗脸,但洗脸要在刷牙后,煮好水,刷好牙洗好脸以后,就可以喝水了,这个活动的AOV图如下(其中的每个顶点都表示一个活动,而顶点之间的弧表示了活动之间的执行先后顺序):
在这里插入图片描述

(2)AOE网( Activity On Edge Network)
AOE网是一个表示工程的带权有向图,其中的顶点用来表示某个事件,弧用来表示活动,弧上的权值用来表示活动持续的时间。例如上述例子的AOE网如下:
在这里插入图片描述

(3)AOE网和AOV网的区别
AOV网:其顶点用来表示活动。AOE网是用来表示活动之间的制约关系。
AOE网:顶点表示事件,边表示活动,边上的权值用来表示活动持续的时间。AOV网是用来分析工程至少需要花多少时间完成,或是为了缩短时间需要加快哪些活动等问题。

(4)源点、汇点、路径长度
在AOE网中,
始点源点:入度为0的顶点,它表示一个工程的开始;
终点汇点:出度为0的顶点,它表示一个工程的结束;
路径长度:路径上各个活动所持续的时间之和。

(5)关键路径、关键活动
在AOE网中,从源点到汇点具有最大长度的路径称为关键路径,在关键路径上的活动称为关键活动

2. 关键路径算法

2.1 基本思想

(1)要找出一个AOE网中的关键路径,就要先找出网里的关键活动,这些关键活动间的路径就是关键路径。

(2)判断一个活动是不是关键活动,方法是要先找到所有活动的最早开始时间和最晚开始时间,并且对它们进行比较,如果二者相等(意味着这个活动在该工程中没有时间松动),说明这个活动是关键活动。

(3)对于活动<Vi, Vj>,它最早的开始时间等于事件Vi最早的发生时间earlist[Vi](事件v的最早发生时间用earlist[v])。假设E[Vi]表示所有到达顶点Vi的弧的集合,len<Vi, Vj>表示活动<Vi, Vj>的持续时间,那么:
在这里插入图片描述
注意,这里假设顶点下标从0开始,所以Vi==0,则表示它是源点,因此最早的开始时间为0;当某个顶点不是源点,那么只有在它前面的事件都发生完后,才能轮到这个事件,所以用了max。

(4)对于活动<Vi, Vj>,它最晚的开始时间等于事件Vj最晚的发生时间减去这个活动的持续事件,即latest[Vj]-len<Vi, Vj>(事件v的最晚的发生时间用latest[v])。假设S[Vj]表示所有从顶点Vj出发的弧的集合,len<Vj, Vk>表示活动<Vj, Vk>的持续时间,那么:
在这里插入图片描述

2.2 算法实现

(1)数据结构

typedef struct EdgeListNode{ //边表结点
    int adjId;
    int weight;
    EdgeListNode* next;
};

typedef struct VertexListNode{ //顶点表结点
    int in; //入度
    int data;
    EdgeListNode* firstadj; //指向其边表
};

typedef struct GraphAdjList{ //图结构
    int vertexnumber; //顶点个数
    int edgenumber;
    VertexListNode vertextlist[Maximum];
};

(2)具体实现
1)由基本思路可以知道,要求一个AOE网的关键路径,步骤如下:
A. 首先初始化每个顶点的最早开始为0,然后对AOE网进行拓扑排序,在排序的过程中获取每个顶点的最早开始时间;
B. 获取拓扑排序后,初始化每个顶点的最晚开始时间为汇点的最早开始时间,并从AOE网的汇点开始,从后往前,对每个顶点找到求其最晚开始时间;
C. 遍历图中的每条边(方法是遍历图中每个顶点的边表),求其最早开始时间和最晚开始时间,如果二者相等,则这是个关键活动,将其加入关键路径中。

2)代码

#include<iostream>
#include<stdio.h>
#include<string.h>
#include<map>
#include<vector>
#include<sstream>
#include<list>
#include<stdlib.h>
#include<queue>
using namespace std;

#define Maximum 1000

typedef struct EdgeListNode{
    int adjId;
    int weight;
    EdgeListNode* next;
};

typedef struct VertexListNode{
    int in;
    int data;
    EdgeListNode* firstadj;
};

typedef struct GraphAdjList{
    int vertexnumber;
    int edgenumber;
    VertexListNode vertextlist[Maximum];
};

//拓扑排序,返回拓扑排序结果,earlist存储每个顶点的最早开始时间
vector<int> ToplogicalSort(GraphAdjList g, int *earlist) {
    vector<int>v; //存储拓扑排序结果
    queue<int>q; //存储入度为0的顶点
    EdgeListNode *temp;
    int i, j, k, ans;

    ans = 0;
    v.clear();
    while(!q.empty()) {
        q.pop();
    }

	//找出所有入度为0的顶点
    for(i=1; i<=g.vertexnumber; i++) {
        if(g.vertextlist[i].in == 0) {
            q.push(i);
        }
    }

    while(!q.empty()) {
        i = q.front();
        v.push_back(i);
        q.pop();
        ans++;
        temp = g.vertextlist[i].firstadj;
        while(temp != NULL) {
            k = earlist[i] + temp->weight;
            if(k > earlist[temp->adjId]) {
                earlist[temp->adjId] = k;
            }
            j = --g.vertextlist[temp->adjId].in;
            if(j == 0) {
                q.push(temp->adjId);
            }
            temp = temp->next;
        }
    }

    if(ans < g.vertexnumber) {
        v.clear();
    }
    return v;
}

//求关键路径,返回存储关键路径顶点的vector
vector<int> CriticalPath(GraphAdjList g) {
    vector<int>ans;
    vector<int>path;
    int i, j, k, *earlist, *latest;
    EdgeListNode *temp;

	//earlist存储每个顶点的最早开始时间
	//latest存储每个顶点的最晚开始时间
    earlist = (int*)malloc(sizeof(int) * (g.vertexnumber+1));
    latest = (int*)malloc(sizeof(int) * (g.vertexnumber+1));
    path.clear();
    for(i=1; i<=g.vertexnumber; i++) {
        earlist[i] = 0;
    }

    ans = ToplogicalSort(g, earlist);
    for(i=1; i<=g.vertexnumber; i++) {
        latest[i] = earlist[ans[g.vertexnumber-1]];
    }
    for(i=g.vertexnumber; i>0; i--) {
        temp = g.vertextlist[i].firstadj;
        while(temp!=NULL) {
            if(latest[temp->adjId] - temp->weight < latest[i]) {
                latest[i] = latest[temp->adjId] - temp->weight;
            }
            temp = temp->next;
        }
    }

	//遍历每条边
    for(i=1; i<=g.vertexnumber; i++) {
        temp = g.vertextlist[i].firstadj;
        while(temp != NULL) {
            j = earlist[i];
            k = latest[temp->adjId] - temp->weight;
            if(j == k) { //是关键活动
       			//把该活动的两个顶点加入path
                path.push_back(i);
                path.push_back(temp->adjId);
            }
            temp = temp->next;
        }
    }
    return path;
}

(3)测试
在这里插入图片描述

int main() {
    GraphAdjList g;
    EdgeListNode *e;
    int i, j, k;

    g.vertexnumber = 5;
    g.edgenumber = 7;


    for(i=1; i<=g.vertexnumber; i++) {
        g.vertextlist[i].data = i;
        g.vertextlist[i].firstadj = NULL;
    }

    g.vertextlist[1].in = 0;
    g.vertextlist[2].in = 1;
    g.vertextlist[3].in = 2;
    g.vertextlist[4].in = 2;
    g.vertextlist[5].in = 1;

    e = (EdgeListNode*)malloc(sizeof(EdgeListNode));
    e->adjId = 2; e->weight = 2; e->next = g.vertextlist[1].firstadj; g.vertextlist[1].firstadj = e;

    e = (EdgeListNode*)malloc(sizeof(EdgeListNode));
    e->adjId = 4; e->weight = 1; e->next = g.vertextlist[1].firstadj; g.vertextlist[1].firstadj = e;

    e = (EdgeListNode*)malloc(sizeof(EdgeListNode));
    e->adjId = 5; e->weight = 1; e->next = g.vertextlist[1].firstadj; g.vertextlist[1].firstadj = e;

    e = (EdgeListNode*)malloc(sizeof(EdgeListNode));
    e->adjId = 3; e->weight = 3; e->next = g.vertextlist[2].firstadj; g.vertextlist[2].firstadj = e;

    e = (EdgeListNode*)malloc(sizeof(EdgeListNode));
    e->adjId = 4; e->weight = 5; e->next = g.vertextlist[2].firstadj; g.vertextlist[2].firstadj = e;

    e = (EdgeListNode*)malloc(sizeof(EdgeListNode));
    e->adjId = 3; e->weight = 8; e->next = g.vertextlist[4].firstadj; g.vertextlist[4].firstadj = e;

    e = (EdgeListNode*)malloc(sizeof(EdgeListNode));
    e->adjId = 3; e->weight = 1; e->next = g.vertextlist[5].firstadj; g.vertextlist[5].firstadj = e;

    vector<int>ans;
    ans = CriticalPath(g);

    for(i=0; i<ans.size(); i+=2) {
        cout<<ans[i]<<"->"<<ans[i+1]<<endl;
    }
    cout<<endl;

    return 0;
}
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/sinat_22510827/article/details/109297615

智能推荐

稀疏编码的数学基础与理论分析-程序员宅基地

文章浏览阅读290次,点赞8次,收藏10次。1.背景介绍稀疏编码是一种用于处理稀疏数据的编码技术,其主要应用于信息传输、存储和处理等领域。稀疏数据是指数据中大部分元素为零或近似于零的数据,例如文本、图像、音频、视频等。稀疏编码的核心思想是将稀疏数据表示为非零元素和它们对应的位置信息,从而减少存储空间和计算复杂度。稀疏编码的研究起源于1990年代,随着大数据时代的到来,稀疏编码技术的应用范围和影响力不断扩大。目前,稀疏编码已经成为计算...

EasyGBS国标流媒体服务器GB28181国标方案安装使用文档-程序员宅基地

文章浏览阅读217次。EasyGBS - GB28181 国标方案安装使用文档下载安装包下载,正式使用需商业授权, 功能一致在线演示在线API架构图EasySIPCMSSIP 中心信令服务, 单节点, 自带一个 Redis Server, 随 EasySIPCMS 自启动, 不需要手动运行EasySIPSMSSIP 流媒体服务, 根..._easygbs-windows-2.6.0-23042316使用文档

【Web】记录巅峰极客2023 BabyURL题目复现——Jackson原生链_原生jackson 反序列化链子-程序员宅基地

文章浏览阅读1.2k次,点赞27次,收藏7次。2023巅峰极客 BabyURL之前AliyunCTF Bypassit I这题考查了这样一条链子:其实就是Jackson的原生反序列化利用今天复现的这题也是大同小异,一起来整一下。_原生jackson 反序列化链子

一文搞懂SpringCloud,详解干货,做好笔记_spring cloud-程序员宅基地

文章浏览阅读734次,点赞9次,收藏7次。微服务架构简单的说就是将单体应用进一步拆分,拆分成更小的服务,每个服务都是一个可以独立运行的项目。这么多小服务,如何管理他们?(服务治理 注册中心[服务注册 发现 剔除])这么多小服务,他们之间如何通讯?这么多小服务,客户端怎么访问他们?(网关)这么多小服务,一旦出现问题了,应该如何自处理?(容错)这么多小服务,一旦出现问题了,应该如何排错?(链路追踪)对于上面的问题,是任何一个微服务设计者都不能绕过去的,因此大部分的微服务产品都针对每一个问题提供了相应的组件来解决它们。_spring cloud

Js实现图片点击切换与轮播-程序员宅基地

文章浏览阅读5.9k次,点赞6次,收藏20次。Js实现图片点击切换与轮播图片点击切换<!DOCTYPE html><html> <head> <meta charset="UTF-8"> <title></title> <script type="text/ja..._点击图片进行轮播图切换

tensorflow-gpu版本安装教程(过程详细)_tensorflow gpu版本安装-程序员宅基地

文章浏览阅读10w+次,点赞245次,收藏1.5k次。在开始安装前,如果你的电脑装过tensorflow,请先把他们卸载干净,包括依赖的包(tensorflow-estimator、tensorboard、tensorflow、keras-applications、keras-preprocessing),不然后续安装了tensorflow-gpu可能会出现找不到cuda的问题。cuda、cudnn。..._tensorflow gpu版本安装

随便推点

物联网时代 权限滥用漏洞的攻击及防御-程序员宅基地

文章浏览阅读243次。0x00 简介权限滥用漏洞一般归类于逻辑问题,是指服务端功能开放过多或权限限制不严格,导致攻击者可以通过直接或间接调用的方式达到攻击效果。随着物联网时代的到来,这种漏洞已经屡见不鲜,各种漏洞组合利用也是千奇百怪、五花八门,这里总结漏洞是为了更好地应对和预防,如有不妥之处还请业内人士多多指教。0x01 背景2014年4月,在比特币飞涨的时代某网站曾经..._使用物联网漏洞的使用者

Visual Odometry and Depth Calculation--Epipolar Geometry--Direct Method--PnP_normalized plane coordinates-程序员宅基地

文章浏览阅读786次。A. Epipolar geometry and triangulationThe epipolar geometry mainly adopts the feature point method, such as SIFT, SURF and ORB, etc. to obtain the feature points corresponding to two frames of images. As shown in Figure 1, let the first image be ​ and th_normalized plane coordinates

开放信息抽取(OIE)系统(三)-- 第二代开放信息抽取系统(人工规则, rule-based, 先抽取关系)_语义角色增强的关系抽取-程序员宅基地

文章浏览阅读708次,点赞2次,收藏3次。开放信息抽取(OIE)系统(三)-- 第二代开放信息抽取系统(人工规则, rule-based, 先关系再实体)一.第二代开放信息抽取系统背景​ 第一代开放信息抽取系统(Open Information Extraction, OIE, learning-based, 自学习, 先抽取实体)通常抽取大量冗余信息,为了消除这些冗余信息,诞生了第二代开放信息抽取系统。二.第二代开放信息抽取系统历史第二代开放信息抽取系统着眼于解决第一代系统的三大问题: 大量非信息性提取(即省略关键信息的提取)、_语义角色增强的关系抽取

10个顶尖响应式HTML5网页_html欢迎页面-程序员宅基地

文章浏览阅读1.1w次,点赞6次,收藏51次。快速完成网页设计,10个顶尖响应式HTML5网页模板助你一臂之力为了寻找一个优质的网页模板,网页设计师和开发者往往可能会花上大半天的时间。不过幸运的是,现在的网页设计师和开发人员已经开始共享HTML5,Bootstrap和CSS3中的免费网页模板资源。鉴于网站模板的灵活性和强大的功能,现在广大设计师和开发者对html5网站的实际需求日益增长。为了造福大众,Mockplus的小伙伴整理了2018年最..._html欢迎页面

计算机二级 考试科目,2018全国计算机等级考试调整,一、二级都增加了考试科目...-程序员宅基地

文章浏览阅读282次。原标题:2018全国计算机等级考试调整,一、二级都增加了考试科目全国计算机等级考试将于9月15-17日举行。在备考的最后冲刺阶段,小编为大家整理了今年新公布的全国计算机等级考试调整方案,希望对备考的小伙伴有所帮助,快随小编往下看吧!从2018年3月开始,全国计算机等级考试实施2018版考试大纲,并按新体系开考各个考试级别。具体调整内容如下:一、考试级别及科目1.一级新增“网络安全素质教育”科目(代..._计算机二级增报科目什么意思

conan简单使用_apt install conan-程序员宅基地

文章浏览阅读240次。conan简单使用。_apt install conan