引言

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

    举个例子,我们需要制作一台汽车,我们需要先造各种各样的零件,然后进行组装,这些零件基本上都是在流水线上同时成产的。加入造一个轮子需要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. loadrunner12-用Chrome如何录制脚本

    1.下载Chrome最新版本: 2.打开需要录制的网页,按下F12按钮: 3.在弹出框中,选择Network标签,在该标签下单击红色按钮(开始录制按钮): 4.按下F5刷新页面(不管是否需要录制当前页 ...

  2. 报错提示:mysqli_fetch_array() expects parameter 1 to be mysqli_result, boolean given in..的处理方式

    PHP操作数据库的时候如果出现报错: mysqli_fetch_array() expects parameter 1 to be mysqli_result, boolean given in.. ...

  3. Greeplum 系列(一) Greenplum 架构

    Greeplum 系列(一) Greenplum 架构 Greenplum 可进行海量并行处理 (Massively Parallel Processing) 一.Greenplum 体系架构 Gre ...

  4. Python中where()函数的用法

    where()的用法 首先强调一下,where()函数对于不同的输入,返回的只是不同的. 1当数组是一维数组时,返回的值是一维的索引,所以只有一组索引数组 2当数组是二维数组时,满足条件的数组值返回的 ...

  5. Basic4android v3.50 发布

    这次发布的主要是debug 的增强.说实话,在这一方面B4a 比delphi做的要好.希望delphi 在新的版本里面 能进一步加强. I'm happy to release Basic4andro ...

  6. Android targetSdkVersion 原理

    前几天 Google 官方发布文章解析 compileSdkVersion.minSdkVersion 以及 targetSdkVersion 的含义,以及合理设置各个值的意义,原文 Picking ...

  7. RSA加密解密总结

    简单的控制台程序 #include"stdafx.h" #include <math.h> #include<string.h> /*/求解密密钥d的函数( ...

  8. MFC中的一般经验之谈2

    MFC一般类成员m_iAge,命名原则,且MFC中类定义以C开头原则,这些原则便于理解以及增强代码的可读性.MFC是一个用窗口作为用户交互的方式,一般框架类.视图类.以及窗口上的控件都是继承CWnd类 ...

  9. Zend_Application 流程详解

    本周没什么工作,zend 系统性的东西渐渐忘记,抽时间整理一下代码!Zend_Application 负责加载配置以及初始化资源,所以index.php 会有这行代码 /** Zend_Applica ...

  10. [C#]安装WindowsService的关键步骤

    使用.Net编写好了WindowsService以后,不安装到系统里就没有任何作用. [添加Installer] 在服务的设计器画面,属性页面里,选择[Add Installer]链接. 如此便会生成 ...