图论(4):关键路径概念与算法(Graph实现)

概念

AOE网对应研究实际问题是工程的工期问题:(1)完成一项工程至少需要多少时间?(2)哪些活动是影响整个工程进度的关键?

如果在有向图中用顶点表示事件,用弧表示活动,用弧上的权表示活动持续时间,则称该带权有向图(即有向网)为边表示活动的网(activity on edge network),简称AOE网。如下图所示:

AOE网-活动与事件关系图

AOE网中的事件与活动有如下两个重要性质:
1、只有在某顶点所代表的事件发生后,从该顶点出发的各活动才能开始;
2、只有在进入某顶点的各活动都结束,该顶点所代表的事件才能发生。

由于一个工程只可能有一个开始点和一个完成点,故正常情况(无环)下,网中只可能有一个入度为0的节点(称为源点)和一个出度为0的节点(称为汇点)。

通常在一个工程中一些活动可以并行地进行,另一些活动则需要等待前面所有的活动完成后才能开始(因此,这里还涉及到事件的拓扑排序),所以完成整个工程的最短时间应该是从开始点到结束点的最长路径的长度(指的是活动持续时间之和,而非弧的数目)。路径长度最长的那条路径叫做关键路径,关键路径上的活动称为关键活动。因此,提前完成非关键活动并不能加快工程的进度。

与关键活动有关的量

如果我们用e(i)标识活动a(i)的最早开始时间,l(i)表示活动a(i)的最迟开始时间(这是在不推迟整个工程完成的前提下活动a(i)的最迟必须开始的时间),两者之差l(i) - e(i)意味着活动a(i)的时间余量。仅当活动a(i)满足条件l(i) == e(i)(即活动的时间余量为0)时表示为关键活动

因此,要获得工程的关键路径就是找出满足条件l(i) == e(i)的所有活动(一个工程中可能存在多条关键路径)。

为了求得活动的e(i)和l(i),首先应求得事件的最早发生时间ve(j),和最迟发生时间vl(j)。如果活动a(i)由弧<j, k>表示,其持续时间记为dut(<j, k>),则满足如下关系式:
e(i) = ve(j)
l(i) = vl(k) - dut(<j, k>)

事件的最早发生时间ve(j):是指从始点开始到顶点(事件)j的最大路径长度。这个长度决定了所有从顶点j出发的活动能够开工的最早时间。
从开始点往后递推求取:
ve(0) = 0; ve(j) = Max{ve(i) + dut(<i, j>) }, 其中<i, j>∈T, j = 1, 2, ..., n -1; T为节点j的入度边集合。

事件最迟发生时间vl(j):是指在不推迟整个工期的前提下,事件j允许的最晚发生时间。
从结束点往前递推求取:
vl(n - 1) = ve(n - 1); vl(i) = Min{vl(j) - dut(<i, j>)},其中<i, j>∈S, i = n - 2, ..., 0; S为节点i的出度边集合。

示例

利用Guava的graph数据结构求得如下所示工程图的关键路径。graph的使用相关介绍请参考:图论(2):Guava中Graph模块(wiki翻译)

关键路径示例图

1、构建示例图AOE网的数据结构:

MutableValueGraph<String, Integer> graph = ValueGraphBuilder.directed()
    .nodeOrder(ElementOrder.insertion())
    .expectedNodeCount(10)
    .build();

graph.putEdgeValue(V1, V2, 6);
graph.putEdgeValue(V1, V3, 4);
graph.putEdgeValue(V1, V4, 5);
graph.putEdgeValue(V2, V5, 1);
graph.putEdgeValue(V3, V5, 1);
graph.putEdgeValue(V4, V6, 2);
graph.putEdgeValue(V5, V7, 9);
graph.putEdgeValue(V5, V8, 7);
graph.putEdgeValue(V6, V8, 4);
graph.putEdgeValue(V7, V9, 2);
graph.putEdgeValue(V8, V9, 4);
Log.i(TAG, "graph: " + graph);

输出:

nodes: [v1, v2, v3, v4, v5, v6, v7, v8, v9], 
edges: {<v1 -> v4>=5, <v1 -> v2>=6, <v1 -> v3>=4, 
<v2 -> v5>=1, <v3 -> v5>=1, <v4 -> v6>=2, <v5 -> v8>=7, 
<v5 -> v7>=9, <v6 -> v8>=4, <v7 -> v9>=2, <v8 -> v9>=4}

2、获取该有向图的拓扑排序列表:

/**
 * 利用Traverser接口将graph进行拓扑排序topologically,此处返回的逆拓扑排序
 */
Iterable<String> topologicallys = Traverser.forGraph(graph)
        .depthFirstPostOrder(startNode);
Log.i(TAG, "topologically: " + format(topologicallys));

输出:

topologically: {v9,v8,v6,v4,v7,v5,v2,v3,v1} //这里是逆序

3、递推求得ve(j)值:


//获取ve(i)
Map<String, Integer> ves = getVeValues(graph, topologicallys);
Log.i(TAG, "ves: " + format(ves));

/**
 * ve(j) = Max{ve(i) + dut(<i,j>) }; <i,j>属于T,j=1,2...,n-1
 * @param graph
 * @param topologicallys
 * @return
 */
private static Map<String, Integer> getVeValues(ValueGraph<String, Integer> graph, 
                                                Iterable<String> topologicallys) {
    List<String> reverses = Lists.newArrayList(topologicallys.iterator());
    Collections.reverse(reverses); //将逆拓扑排序反向
    Map<String, Integer> ves = new ArrayMap<>(); //结果集
    //从前往后遍历
    for (String node : reverses) {
        ves.put(node, 0); //每个节点的ve值初始为0

        //获取node的前趋列表
        Set<String> predecessors = graph.predecessors(node); 
        int maxValue = 0;
        
        //找前趋节点+当前活动耗时最大的值为当前节点的ve值
        for (String predecessor : predecessors) {
            maxValue = Math.max(ves.get(predecessor) +
                    graph.edgeValueOrDefault(predecessor, node, 0), maxValue);
        }
        ves.put(node, maxValue);
    }
    return ves;
}

输出:

ves: {v1:0, v2:6, v3:4, v4:5, v5:7, v6:7, v7:16, v8:14, v9:18}

4、递推求得vl(j)值:



/**
 * vl(i) = Min{vl(j) - dut(<i,j>}; <i,j>属于S,i=n-2,...,0
 * @param graph
 * @param topologicallys
 * @param vels
 * @return
 */
private static Map<String, Integer> getVlValues(ValueGraph<String, Integer> graph,
    Iterable<String> topologicallys, Map<String, Integer> vels) {
    Map<String, Integer> vls = new ArrayMap<>(); //结果集
    //从后往前遍历
    for (String node : topologicallys) {
        //获取node的后继列表
        Set<String> successors = graph.successors(node);
        int initValue = Integer.MAX_VALUE; //初始值为最大值
        if (successors.size() <= 0) { //表示是结束点,赋值为ve值
            initValue = vels.get(node);
        }
        vls.put(node, initValue);
        int minValue = initValue;
        //找后继节点-当前活动耗时最少的值为当前节点的vl值
        for (String successor : successors) {
            minValue = Math.min(vls.get(successor) -
                    graph.edgeValueOrDefault(node, successor, 0), minValue);
        }
        vls.put(node, minValue);
    }
    return vls;
}

输出:

vls: {v1:0, v2:6, v3:6, v4:8, v5:7, v6:10, v7:16, v8:14, v9:18}

5、根据前面求取的ve(j)和vl(j)来找出关键活动(判断条件:ve(j) == vl(k) - dut(<j,k>)):

/**
 * 判断条件:ve(j) == vl(k) - dut(<j,k>)
 */
//关键活动结果集
List<EndpointPair<String>> criticalActives = new ArrayList<>(); 
//返回图中所有活动(边)
Set<EndpointPair<String>> edgs = graph.edges(); 
//遍历每一条边(活动),过滤出:ve(j) == vl(k) - dut<j, k>
for (EndpointPair<String> endpoint : edgs) {
    final int dut = graph.edgeValueOrDefault(endpoint.nodeU(), endpoint.nodeV(), 0);
    //ve(j) == vl(k) - dut<j, k>
    if (vls.get(endpoint.nodeV()) - dut == ves.get(endpoint.nodeU())) { 
        criticalActives.add(endpoint);
    }
}
Log.i(TAG, "critical actives: " + format(criticalActives));

输出:

critical actives: {<v1 -> v2>, <v2 -> v5>, <v5 -> v8>, <v5 -> v7>,
<v7 -> v9>, <v8 -> v9>}

从输出可知,图中存在两条关键路径:{<v1 -> v2>, <v2 -> v5>, <v5 -> v8>, <v8 -> v9>} 和 {<v1 -> v2>, <v2 -> v5>, <v5 -> v7>, <v8 -> v9>}(在示例图中使用红色线段标注)。因此,缩短这两条路径上活动的工期,将能有效的缩短整个工程的工期。
关键路径详细代码参见Demo:GH-Demo-CriticalPath

参考文档

https://www.cnblogs.com/hongyang/p/3407666.html

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342