引言

之前所说的拓扑排序是为了解决一个工程能否顺利进行的问题。但在生活中,我们还会经常遇到如何解决工程完成需要的最短时间问题。

    举个例子,我们需要制作一台汽车,我们需要先造各种各样的零件,然后进行组装,这些零件基本上都是在流水线上同时成产的。加入造一个轮子需要0.5天的时间,造一个发动机需要3天的时间,造一个车底盘需要2天的时间,造一个外壳需要2天的时间,其他零部件需要2天的时间,全部零部件集中到一个地方需要0.5天的时间,组装成车需要2天的时间,那么请问汽车厂造一辆车,最短需要多少时间。

    因为这些零部件都是分别在流水线上同时生产的,也就是说在生产发动机的这3天当中,可能已经生产了6个轮子,1.5个外壳,1.5个车底盘,而组装是在这些零部件都生产好之后才能进行,因此最短的时间就是零件生产时间最长的发动机3天+集中零部件0.5天+组成成车2天,以供需要5.5天完成一辆车的生产。

    所以,我们如果对一个流程获得最短时间,就需要分析它的拓扑关系,找到最关键的流程,这个流程的时间就是最短的时间。

AOE网的定义

在一个表示工程的带权有向图中,用顶点表示时间,用有向边表示活动,用边上的权值表示活动的持续时间,这种有向图的边表示活动的网,我们称之为AOE网。

把AOE网中没有入边的顶点称之为始点或源点,没有出边的顶点称之为终点或汇点。

摘自:《大话数据结构》

关键路径的定义

我们把路径上各个活动所持续的时间之和称之为路径长度,从源点到汇点具有最大的长度的路径叫做关键路径,在关键路径上的活动叫关键活动。



对上面的AOE网来说,其关键路径就是:开始->发动机完成->部件集中->组装完成。路径长度为5.5。

关键路径算法原理

对于一个活动来说,有活动的最早开始时间和最晚开始时间,比较这个活动的最早开始时间和最晚开始时间,如果最早开始时间和最晚开始时间相等,那么就意味着该活动是关键活动,活动间的路径为关键路径。如果不相等,那么这个活动就不是关键活动。

名词解释
  1. 事件的最早发生时间etv:即顶点vkv_kvk​的最早发生时间
  2. 事件的最晚发生时间ltv:即顶点vkv_kvk​的最晚发生时间,也就是每个顶点对应的事件最晚需要开始的时间,超过此时间将会延误整个工期。
  3. 活动的最早开工时间ete:即弧aka_kak​的最早发生时间
  4. 活动的最晚开工时间lte:即弧aka_kak​的最晚发生时间,也就是不推迟工期的最晚开工时间。
算法分析

假设起点为v0v_0v0​,则我们从v0v_0v0​到viv_ivi​的最长路径的长度为顶点(事件)viv_ivi​的最早发生时间。

    同时viv_ivi​的最早发生时间也是所有以viv_ivi​为尾的弧所表示的活动的最早开工时间(即ete),

    定义活动最晚开工时间表示在不会延误所有工期(即lte)。通过根据ete[k]是否与lte[k]相等来判断aka_kak​是关键活动。

    同时,因为我们采用邻接表来存储AOE网,并且对于表示活动的每条边都具有活动的持续时间(即该边的权重),所以,我们需要修改边表节点,增加一个weight来存储弧的权值。

首先我们要求顶点vkv_kvk​即求etv[k]的最早发生时间,公式:

etv[k]={0,当k=0时max[etv[i]+len<vi,vk>],当k不等于0且<vi,vk>属于P[k]时etv[k]=
\left \{\begin{array}{cc}
0, &当k=0时\\
max[etv[i]+len<vi, vk>], & 当k不等于0且<vi, vk>属于P[k]时
\end{array}\right.
etv[k]={0,max[etv[i]+len<vi,vk>],​当k=0时当k不等于0且<vi,vk>属于P[k]时​

在计算ltv时,其实就是对拓扑序列倒过来进行,所以我们可以计算顶点vkv_kvk​即求ltv[k]的最晚发生时间。公式:

ltv[k]={etv[k],当k=n−1时min[ltv[j]−len&lt;vk,vj&gt;],当k&lt;n−1且&lt;vk,vj&gt;属于S[k]时ltv[k]=
\left \{\begin{array}{cc}
etv[k], &amp;当k=n-1时\\
min[ltv[j]-len&lt;vk, vj&gt;], &amp; 当k&lt;n-1且&lt;vk, vj&gt;属于S[k]时
\end{array}\right.
ltv[k]={etv[k],min[ltv[j]−len<vk,vj>],​当k=n−1时当k<n−1且<vk,vj>属于S[k]时​

这两个公式怎么理解呢

    我的理解是,对于事件vkv_kvk​的最早发生时间,表现为以vkv_kvk​为弧头,viv_ivi​为弧尾的其他所有弧(注意:i的值可能没有,表示vkv_kvk​的入度为0;可能为n,表示vkv_kvk​的入度为n)的活动持续时间+viv_ivi​的最早开工时间列表中的最大值。以上面的流程图为例,零部件集中这项事件只有等生产时间最长的发动机造好之后才能进行。

对于vkv_kvk​的最晚发生时间,表现为事件vjv_jvj​的最晚发生时间 减去 以vkv_kvk​为弧尾,vjv_jvj​为弧头的其他所有弧(注意:j的值可能没有,表示vkv_kvk​的出度为0;可能为n,表示vkv_kvk​的出度为n)的活动持续时间。

求关键路径的步骤

  1. 根据图的描述建立该图的邻接表
  2. 从源点v0v_0v0​出发,根据拓扑序列算法求源点到汇点每个顶点的etv,如果得到的拓扑序列个数小于网的顶点个数,则该网中存在环,无关键路径,结束程序。
  3. 从汇点vnv_nvn​出发,且ltv[n-1]=etv[n-1],按照逆拓扑序列,计算每个顶点的ltv。
  4. 根据每个顶点的etv和ltv求每条弧的ete和lte,若ete=lte,说明该活动是关键活动。

代码实现

示例AOE网:



该网的邻接表结构:



数据结构

边表节点:

public class EdgeNode {

	public int adjevex;
public int weight;
public EdgeNode next; public EdgeNode(int adjevex, EdgeNode next) {
this.adjevex = adjevex;
this.next = next;
} public EdgeNode(int adjevex, int weight, EdgeNode next) {
this.adjevex = adjevex;
this.weight = weight;
this.next = next;
}
}

顶点节点:

public class VertexNode {

	public int in;
public Object data;
public EdgeNode firstedge; public VertexNode(Object data, int in, EdgeNode firstedge) {
this.data = data;
this.in = in;
this.firstedge = firstedge;
}
}

通过拓扑排序求得etv

	public boolean ToplogicalSort() {
EdgeNode e;
int k, gettop;
int count = 0;
etv = new int[adjList.length];
for (int i = 0; i < adjList.length; i++) {
if(adjList[i].in == 0) {
stack.push(i);
}
}
for (int i = 0; i < adjList.length; i++) {
etv[i] = 0;
} while(!stack.isEmpty()) {
gettop = (int) stack.pop();
count++;
stack2.push(gettop);
for (e = adjList[gettop].firstedge; e != null; e = e.next) {
k = e.adjevex;
if((--adjList[k].in) == 0) {
stack.push(k);
}
if(etv[gettop] + e.weight > etv[k]) {
etv[k] = etv[gettop] + e.weight;
}
}
}
if(count < adjList.length) return false;
else return true; }

关键路径算法:

	public void CriticalPath() {
EdgeNode e;
int gettop, k, j;
int ete, lte;
if(!this.ToplogicalSort()) {
System.out.println("该网中存在回路!");
return;
}
ltv = new int[adjList.length];
for (int i = 0; i < adjList.length; i++) {
ltv[i] = etv[etv.length - 1];
} while(!stack2.isEmpty()) {
gettop = (int) stack2.pop();
for(e = adjList[gettop].firstedge; e != null; e = e.next) {
k = e.adjevex;
if(ltv[k] - e.weight < ltv[gettop]) {
ltv[gettop] = ltv[k] - e.weight;
}
}
}
for (int i = 0; i < adjList.length; i++) {
for(e = adjList[i].firstedge; e != null; e = e.next) {
k = e.adjevex;
ete = etv[i];
lte = ltv[k] - e.weight;
if(ete == lte) {
System.out.print("<" + adjList[i].data + "," + adjList[k].data + "> length: " + e.weight + ",");
}
}
}
}

完整代码:

public class CriticalPathSort {

	int[] etv, ltv;
Stack stack = new Stack(); //存储入度为0的顶点,便于每次寻找入度为0的顶点时都遍历整个邻接表
Stack stack2 = new Stack(); //将顶点序号压入拓扑序列的栈
static VertexNode[] adjList; public boolean ToplogicalSort() {
EdgeNode e;
int k, gettop;
int count = 0;
etv = new int[adjList.length];
for (int i = 0; i < adjList.length; i++) {
if(adjList[i].in == 0) {
stack.push(i);
}
}
for (int i = 0; i < adjList.length; i++) {
etv[i] = 0;
} while(!stack.isEmpty()) {
gettop = (int) stack.pop();
count++;
stack2.push(gettop);
for (e = adjList[gettop].firstedge; e != null; e = e.next) {
k = e.adjevex;
if((--adjList[k].in) == 0) {
stack.push(k);
}
if(etv[gettop] + e.weight > etv[k]) {
etv[k] = etv[gettop] + e.weight;
}
}
}
if(count < adjList.length) return false;
else return true; } public void CriticalPath() {
EdgeNode e;
int gettop, k, j;
int ete, lte;
if(!this.ToplogicalSort()) {
System.out.println("该网中存在回路!");
return;
}
ltv = new int[adjList.length];
for (int i = 0; i < adjList.length; i++) {
ltv[i] = etv[etv.length - 1];
} while(!stack2.isEmpty()) {
gettop = (int) stack2.pop();
for(e = adjList[gettop].firstedge; e != null; e = e.next) {
k = e.adjevex;
if(ltv[k] - e.weight < ltv[gettop]) {
ltv[gettop] = ltv[k] - e.weight;
}
}
}
for (int i = 0; i < adjList.length; i++) {
for(e = adjList[i].firstedge; e != null; e = e.next) {
k = e.adjevex;
ete = etv[i];
lte = ltv[k] - e.weight;
if(ete == lte) {
System.out.print("<" + adjList[i].data + "," + adjList[k].data + "> length: " + e.weight + ",");
}
}
}
} public static EdgeNode getAdjvex(VertexNode node) {
EdgeNode e = node.firstedge;
while(e != null) {
if(e.next == null) break;
else
e = e.next;
}
return e;
}
public static void main(String[] args) {
int[] ins = {0, 1, 1, 2, 2, 1, 1, 2, 1, 2};
int[][] adjvexs = {
{2, 1},
{4, 3},
{5, 3},
{4},
{7, 6},
{7},
{9},
{8},
{9},
{}
};
int[][] widths = {
{4, 3},
{6, 5},
{7, 8},
{3},
{4, 9},
{6},
{2},
{5},
{3},
{}
};
adjList = new VertexNode[ins.length];
for (int i = 0; i < ins.length; i++) {
adjList[i] = new VertexNode("V"+i, ins[i],null);
if(adjvexs[i].length > 0) {
for (int j = 0; j < adjvexs[i].length; j++) {
if(adjList[i].firstedge == null)
adjList[i].firstedge = new EdgeNode(adjvexs[i][j], widths[i][j], null);
else {
getAdjvex(adjList[i]).next = new EdgeNode(adjvexs[i][j], widths[i][j], null);
}
}
}
} CriticalPathSort c = new CriticalPathSort();
c.CriticalPath(); }
}

注意:这个例子中只有唯一一条关键路径,这并不表示不存在多条关键路径的有向无环图。如果是多条关键路径,则单是提高一条关键路径上的关键活动速度并不能导致整个工期缩短,而必须提高同时在几条关键路径上的活动的速度。

图的基础---关键路径理解和实现(Java)的更多相关文章

  1. Java基础之理解Annotation(与@有关,即是注释)

    Java基础之理解Annotation 一.概念 Annontation是Java5开始引入的新特征.中文名称一般叫注解.它提供了一种安全的类似注释的机制,用来将任何的信息或元数据(metadata) ...

  2. 零基础的人怎么学习Java

    编程语言Java,已经21岁了.从1995年诞生以来,就一直活跃于企业中,名企应用天猫,百度,知乎......都是Java语言编写,就连现在使用广泛的XMind也是Java编写的.Java应用的广泛已 ...

  3. java基础知识(四)java内存机制

    Java内存管理:深入Java内存区域 上面的文章对于java的内存管理机制讲的非常细致,在这里我们只是为了便于后面内容的理解,对java内存机制做一个简单的梳理. 程序计数器:当前线程所执行的字节码 ...

  4. Java基础:三步学会Java Socket编程

    Java基础:三步学会Java Socket编程 http://tech.163.com 2006-04-10 09:17:18 来源: java-cn 网友评论11 条 论坛        第一步 ...

  5. 从零基础到拿到网易Java实习offer,谈谈我的学习经验

    微信公众号[程序员江湖] 作者黄小斜,斜杠青年,某985硕士,阿里 Java 研发工程师,于 2018 年秋招拿到 BAT 头条.网易.滴滴等 8 个大厂 offer,目前致力于分享这几年的学习经验. ...

  6. 从零基础到拿到网易Java实习offer,我做对了哪些事

    作为一个非科班小白,我在读研期间基本是自学Java,从一开始几乎零基础,只有一点点数据结构和Java方面的基础,到最终获得网易游戏的Java实习offer,我大概用了半年左右的时间.本文将会讲到我在这 ...

  7. 076 01 Android 零基础入门 02 Java面向对象 01 Java面向对象基础 01 初识面向对象 01 Java面向对象导学

    076 01 Android 零基础入门 02 Java面向对象 01 Java面向对象基础 01 初识面向对象 01 Java面向对象导学 本文知识点:Java面向对象导学 说明:因为时间紧张,本人 ...

  8. Atitit  深入理解命名空间namespace  java c# php js

    Atitit  深入理解命名空间namespace  java c# php js 1.1. Namespace还是package1 1.2. import同时解决了令人头疼的include1 1.3 ...

  9. [原]Java修炼 之 基础篇(二)Java语言构成

    上次的博文中Java修炼 之 基础篇(一)Java语言特性我们介绍了一下Java语言的几个特性,今天我们介绍一下Java语言的构成.        所谓的Java构成,主要是指Java运行环境的组成, ...

随机推荐

  1. strip命令

    去掉文件里调试和符号信息,文件大小变小,一般在发布的时候使用. 主要作用于可执行文件,动态库,目标文件等. 可参考:http://blog.csdn.net/stpeace/article/detai ...

  2. R学习笔记-安装R和RStudio,注意RStudio的版本需要与操作系统版本匹配

    1.安装步骤:先安装R,再安装RStudio RStudio是R的集成开发工具,本身不带R环境. 2.从当前R的官网和RStudio下载的R和RStudio的版本分别为: A .For Windows ...

  3. QTcpSocket-Qt使用Tcp通讯实现服务端和客户端

    版权声明:若无来源注明,Techie亮博客文章均为原创. 转载请以链接形式标明本文标题和地址: 本文标题:QTcpSocket-Qt使用Tcp通讯实现服务端和客户端     本文地址:https:// ...

  4. (轉)Equal height boxes with CSS

    原文:http://www.456bereastreet.com/archive/200405/equal_height_boxes_with_css/ 下面是我翻译的内容,是根据我对文章的理解意译的 ...

  5. ASP.NET Core2基于RabbitMQ对Web前端实现推送功能

    在我们很多的Web应用中会遇到需要从后端将指定的数据或消息实时推送到前端,通常的做法是前端写个脚本定时到后端获取,或者借助WebSocket技术实现前后端实时通讯.因定时刷新的方法弊端很多(已不再采用 ...

  6. postgresql的日志实现机制

    1.事务的概念   事务是从实际生活中引入数据库的一个概念,即事务内的操作,要么全做,要么全不做.就像银行转账一样,当从一个帐户转出一部分钱之后,就必须在另一个帐户中存入相同数目的钱,若是转出钱之后, ...

  7. PHP爬虫(3)PHP DOM开源代码里的大坑和字符编码

    一.开源代码的问题 在PHP爬虫(2)中介绍了开源工程Sunra.PhpSimple.HtmlDomParser.在实际工作中发现一个问题,例如http://www.163.com的网页数据怎么也抓取 ...

  8. wp调用百度服务api

    通过百度开放平台申请api成功后,百度会提供一个application key简称ak和一个security key简称sk. 看一下某个服务url的格式 1. url前缀 2. 服务类型 3. 参数 ...

  9. 通过hive向写elasticsearch的写如数据

    通过hive向写elasticsearch的写如数据 hive 和 elasticsearch 的整合可以参考官方的文档: ES-hadoop的hive整合 : https://www.elastic ...

  10. 解决“找不到请求的 .Net Framework Data Provider。可能没有安装.”错误

    问题: 这几天在装.NET 的开发环境,在装好VS2013和Oracle 11g之后,做了一个测试项目,运行调试没问题 但是涉及到数据库相关操作,如新建数据集.连接数据库等在调试的时候则会出现如下错误 ...