关键路径及关键路径算法[C/C++]

news2024/11/25 10:55:34

文章目录

    • 关键路径
      • 引例
      • AOE网
      • 关键路径与关键活动
      • 关键路径算法
        • 引例与原理
        • 关键路径算法的实现
          • 边的存储结构
          • 代码实现
          • 运行示例

关键路径

关于拓扑排序的内容见拓扑排序详解

引例

通过拓扑排序我们可以解决一个工程是否可以顺序进行的问题,拓扑排序把一个工程分成了若干个流水级,只有当前流水级的工作完成才能进入下一个流水级,然而有时候我们还要解决工程完成的最短时间问题。比如说,造一辆汽车,我们要先造各种各样的零件、部件,最终再组装成车。外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

这些零部件都是在流水线上同时生产的,假如造一个轮子需要0.5天,造一个发动机需要3天,造一个车底盘需要2天,造一个外壳需要2天,造其他零部件需要2天,全部零部件集中到一处需要0.5天,组装成车需要2天,那么我们生产一辆车最短需要几天呢?

如果回答说把所有时间加起来,假如所有活动是串行的话,那么自然是每个活动时间相加,但是这些零件是分别在流水线上同时生产的,也就是说生产发动机的3天内我们可能已经生产了6个轮子,1.5个外壳,1.5个底盘了,而车的组装是在所有零部件都生产好之后才可以进行的,因此最短时间应该是先导活动中时间最长的发动机3天+集中零部件0.5天+组装车的2天,一共5.5天完成一辆车的生产。

因此,我们如果要对一个流程图求最短完成时间,就要分析它们的拓扑关系,并找到其中的关键流程,这个流程的时间就是最短时间。

AOE网

基于我们拓扑排序中AOV网(Activity On Vertex Network),我们根据求流程的最短时间的需求,提出一个新的概念。在一个表示工程的带权有向图中,用顶点表示事件,用有向边表示活动,边上的权值表示活动的持续时间,这种有向图的边表示活动的网,我们称之为AOE网(Activity On Edge Network).

我们把AOE网中入度为0的点称为始点或源点,出度为0的点称为终点或汇点,它们分别代表工程的开始和结束,所以正常情况下,AOE网只有一个源点和一个汇点。如下图中,顶点vi代表一个事件,弧<vi , vj>则表示一个活动,其权值代表活动的持续时间。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

AOE网有着明显的工程特性,只有某顶点的事件发生后,由该顶点出发的各项活动才能开始。只有进入该顶点的所有活动都已经结束,该顶点的事件才能发生。

AOE网和AOV网有着相似之处,二者都是对工程进行建模,但又有很大差别。AOV网用顶点表示活动,它描述了活动间的偏序关系,AOE网则用边来表示活动边上的权值表示活动的持续时间。仍以我们汽车的流水线生产为例,其AOV网和AOE网的对比体现了二者的差别。AOE网是建立在活动的偏序关系(或制约关系)没有矛盾的基础上,再来分析整个工程至少需要多少时间,以及为了缩短工程时间,可以加快哪些活动,或者对于各个活动ddl的限制等。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

关键路径与关键活动

我们把路径上各个活动所持续的时间之和称为路径长度,从源点到汇点具有最大长度的路径叫关键路径,在关键路径上的活动叫关键活动。显然就上图的AOE网而言,开始→发动机完成→部件集中到位→组装完成就是关键路径,路径长度为5.5。

我们发现,如果我们加快非关键路径上的活动,并不会缩短整个工程的时间,即使一个轮子只需要0.1的生产时间也无济于事,但是如果我们缩短了关键路径上关键活动的时间,如发动机缩短为2.5天,组装车缩短为1.5天,那么我们的关键路径长度就缩短为了4.5天。

所以我们如何去求关键路径呢?

关键路径算法

引例与原理

这是关于关键路径的一个很经典的例子。假设一个学生放学回家,除掉吃饭、洗漱外,到睡觉前有四小时空闲,而家庭作业需要两小时完成。不同的学生会有不同的做法,抓紧的学生,会在头两小时就完成作业,然后看看电视、读读课外书什么的;但也有超过一半的学生(比如我(悲))会在最后两小时才去做作业,要不是因为没时间,可能还要再拖延下去。这也没什么好奇怪的,拖延就是人性几大弱点之一。

这里做家庭作业这一活动的最早开始时间是四小时的开始,可以理解为0,而最晚开始时间是两小时之后马上开始,不可以再晚,否则就是延迟了,此时可以理解为2。显然,当最早和最晚开始时间不相等时就意味着有空闲。接着,老妈发现了孩子拖延的小秘密(太痛了x_x),于是买了很多的课外习题,要求你四个小时,不许有一丝空闲,省得你拖延或偷懒。此时整个四小时全部被占满,最早开始时间和最晚开始时间都是0,因此它就是关键活动了。也就是说,我们只需要找到所有活动的最早开始时间和最晚开始时间,并且比较它们,如果相等就意味着此活动是关键活动活动间的路径为关键路径。如果不等,则就不是。

关键路径算法的实现

那么关键路径的求解过程如下:

  • 用数组etv(earliest time of vertex)和ltv(latest time of vertex)来存储事件的最早/最晚发生时间

  • e t v [ k ] = { 0                    , 当 k = 0 时 m a x { e t v [ i ] + l e n < v i , v k > } . , 当 k ≠ 0 且 < v i , v k > ∈ p [ k ] 时 etv[k] = \left\{\begin{matrix} 0\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ ,当k=0时\\ max\left \{ etv[i]+len<vi,vk>\right \} .,当k≠0且 <vi,vk>∈p[k]时 \end{matrix}\right. etv[k]={0                  ,k=0max{etv[i]+len<vi,vk>}.,k=0<vi,vk>∈p[k]

    其中p[ k ]为所有到达vk的边集合,转移方程保证了事件k在etv[ k ]发生时,其所有先导活动都能完成

  • l t v [ k ] = { e t v [ k ]                    , 当 k = n − 1 时 m i n { l t v [ j ] − l e n < v k , v j > } , 当 k ≠ n − 1 且 < v k , v j > ∈ S [ k ] 时 ltv[k] = \left\{\begin{matrix} etv[k]\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ ,当k=n-1时\\ min\left \{ ltv[j]-len<vk,vj>\right \} ,当k≠n-1且 <vk,vj>∈S[k]时 \end{matrix}\right. ltv[k]={etv[k]                  ,k=n1min{ltv[j]len<vk,vj>},k=n1<vk,vj>∈S[k]

    其中S[k]为vk发出的边集合,转移方程保证了事件k在ltv[ k ]发生时,其发出的所有活动到达的事件都能在其最早开始时间完成

  • 活动的最早开始时间即其先导事件的最早发生时间,因此只有先导事件发生了,活动才能开始

  • 活动的最晚开始时间是其抵达事件的最晚发生时间减去活动时长,活动再晚也不能等其到达的事件发生了才开始,所以要赶在到达的事件发生之前

这里我们使用邻接表来存储边。

边的存储结构
vector<int> etv, ltv; // 事件最早发生时间
stack<int> path;      // 存储拓扑序列
struct Edge
{
    Edge(int a, int w) : adjvex(a), weight(w), _next(nullptr)
    {
    }
    // int in;//边的起点,这里省去
    int adjvex;//边的终点
    int weight;//权值
    Edge *_next;//in发出的下一条边
};
代码实现

求解etv的过程就是一次拓扑排序,只不过加了行对于elv的更新,开始事件的etv显然是0,由开始事件往后由拓扑顺序更新etv

而求解ltv其实就是对于拓扑排序的逆过程,因为结束事件的etv和ltv显然相同,那么对拓扑序列逆向遍历,更新ltv

再根据ltv,etv和lte,ete的关系求寻找我们的关键活动

void TopoLogicalSort(vector<Edge *> &edges)
{
    int n = edges.size();
    etv.resize(n);
    vector<int> ind(n); // 记录入度
    for (auto e : edges)
    {
        while (e)
        {
            ind[e->adjvex]++;
            e = e->_next;
        }
    }
    queue<int> q;
    for (int i = 0; i < n; i++)
        if (!ind[i])
            q.push(i);
    int f;
    while (!q.empty())
    {
        f = q.front();
        q.pop();
        path.push(f); // 存储拓扑序列
        Edge *e = edges[f];
        while (e)
        {
            if (!(--ind[e->adjvex]))
                q.push(e->adjvex);
            if (e->weight + etv[f] > etv[e->adjvex])
                // 如果事件f的etv + 活动e的时间晚于etv[adjvex],则更新事件adjvex的etv,保证adjvex的etv时,事件adjvex的所有先导活动都已完成
                etv[e->adjvex] = e->weight + etv[f];
            e = e->_next;
        }
    }
}
void CriticalPath(vector<Edge *> &edges)
{
    Edge *e = nullptr;
    int ete, lte; // 活动的最早最晚发生时间
    int n = edges.size(), t;
    TopoLogicalSort(edges);
    ltv.resize(n);
    for (int i = 0; i < n; i++)
        ltv[i] = etv[n - 1]; // 初始化最晚发生时间
    while (!path.empty())
    {
        t = path.top();
        path.pop();
        e = edges[t];
        while (e)
        {
            if (etv[e->adjvex] - e->weight < ltv[t]) // 保证事件t如果在etv开始,发出的活动到达的事件都能在其各自etv完成
                ltv[t] = etv[e->adjvex] - e->weight;
            e = e->_next;
        }
    }
    for (int i = 0; i < n; i++)
    {
        e = edges[i];
        ete = etv[i]; // 事件的最早发生时间和其发出活动的最早发生时间一致

        while (e)
        {
            lte = ltv[e->adjvex] - e->weight;
            if (lte == ete)
            {
                printf("<v%d-v%d> length:%d\n", i, e->adjvex, e->weight);
                break;
            }
            e = e->_next;
        }
    }
}
运行示例

运行代码

#include <iostream>
#include <vector>
#include <queue>
#include <stack>
using namespace std;
vector<int> etv, ltv; // 事件最早发生时间
stack<int> path;      // 存储拓扑序列
struct Edge
{
    Edge(int a, int w) : adjvex(a), weight(w), _next(nullptr)
    {
    }
    // int in;
    int adjvex;
    int weight;
    Edge *_next;
};
void TopoLogicalSort(vector<Edge *> &edges)
{
    int n = edges.size();
    etv.resize(n);
    vector<int> ind(n); // 记录入度
    for (auto e : edges)
    {
        while (e)
        {
            ind[e->adjvex]++;
            e = e->_next;
        }
    }
    queue<int> q;
    for (int i = 0; i < n; i++)
        if (!ind[i])
            q.push(i);
    int f;
    while (!q.empty())
    {
        f = q.front();
        q.pop();
        path.push(f); // 存储拓扑序列
        Edge *e = edges[f];
        while (e)
        {
            if (!(--ind[e->adjvex]))
                q.push(e->adjvex);
            if (e->weight + etv[f] > etv[e->adjvex])
                // 如果事件f的etv + 活动e的时间晚于etv[adjvex],则更新事件adjvex的etv,保证adjvex的etv时,事件adjvex的所有先导活动都已完成
                etv[e->adjvex] = e->weight + etv[f];
            e = e->_next;
        }
    }
}
void CriticalPath(vector<Edge *> &edges)
{
    Edge *e = nullptr;
    int ete, lte; // 活动的最早最晚发生时间
    int n = edges.size(), t;
    TopoLogicalSort(edges);
    ltv.resize(n);
    for (int i = 0; i < n; i++)
        ltv[i] = etv[n - 1]; // 初始化最晚发生时间
    while (!path.empty())
    {
        t = path.top();
        path.pop();
        e = edges[t];
        while (e)
        {
            if (etv[e->adjvex] - e->weight < ltv[t]) // 保证事件t如果在etv开始,发出的活动到达的事件都能在其各自etv完成
                ltv[t] = etv[e->adjvex] - e->weight;
            e = e->_next;
        }
    }
    for (int i = 0; i < n; i++)
    {
        e = edges[i];
        ete = etv[i]; // 事件的最早发生时间和其发出活动的最早发生时间一致

        while (e)
        {
            lte = ltv[e->adjvex] - e->weight;
            if (lte == ete)
            {
                printf("<v%d-v%d> length:%d\n", i, e->adjvex, e->weight);
                break;
            }
            e = e->_next;
        }
    }
}
int main()
{
    int n, cnt, adj, w;
    cout << "请输入事件数量" << endl;
    cin >> n;
    Edge *e;
    vector<Edge *> edges(n, nullptr);
    for (int i = 0; i < n; i++)
    {
        cout << "活动数目" << endl;
        cin >> cnt;
        for (int j = 0; j < cnt; j++)
        {
            cout << "输入活动输入顶点和权值" << endl;
            cin >> adj >> w;
            e = new Edge(adj, w);
            e->_next = edges[i];
            edges[i] = e;
        }
    }
    CriticalPath(edges);
    return 0;
}

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1147283.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【计算机网络】(谢希仁第八版)第二章课后习题答案

第二章 1.物理层要解决哪些问题&#xff1f;物理层的主要特点是什么&#xff1f; 答&#xff1a;物理层要解决的主要问题&#xff1a; &#xff08;1&#xff09;物理层要尽可能地屏蔽掉物理设备和传输媒体&#xff0c;通信手段的不同&#xff0c;使数据链路层感觉不到这些差…

【马蹄集】—— 搜索专题

搜索专题 目录 MT2238 数的增殖MT2239 二维矩阵中的最长下降序列MT2240 传染病MT2241 循环空间BD202303 第五维度 MT2238 数的增殖 难度&#xff1a;黄金    时间限制&#xff1a;1秒    占用内存&#xff1a;128M 题目描述 给定一个数 n ( n < 1000 ) n (n<1000) n…

Zabbix监控oxidized备份状态

Zabbix监控oxidized备份状态 原理是利用oxidized的hooks功能调用zabbix_sender推送数据给zabbix_server 参考 https://cloud.tencent.com/developer/article/1657025 https://github.com/clontarfx/zabbix-template-oxidized https://github.com/ytti/oxidized/blob/master/…

Redis原理-IO模型和持久化

高性能IO模型 为什么单线程Redis能那么快 一方面&#xff0c;Redis 的大部分操作在内存上完成&#xff0c;再加上它采用了高效的数据结构&#xff0c;例如哈希表和跳表&#xff0c;这是它实现高性能的一个重要原因。另一方面&#xff0c;就是 Redis 采用了多路复用机制&#…

Arcmap制图绘制显著性区域

类似于下图这种&#xff0c;为分析结果添加显著性区域&#xff0c;该如何实现呢&#xff1f; 实现方式多种多样&#xff0c;比如&#xff1a; 1、代码。Python、R、Matlab都有实现方式&#xff0c;但是绘制一幅优美的地图&#xff0c;用代码绘制&#xff0c;需要添加很多控制语…

广东木模板批发,建筑桥梁工程专用组合木模板

作为广东地区的木模板批发商&#xff0c;我们致力于为建筑行业提供高品质的木模板产品。在众多产品中&#xff0c;我们特别推荐我们的建筑桥梁工程专用组合木模板&#xff0c;为桥梁工程提供卓越的支持和出色的性能。 我们的组合木模板是专为桥梁工程设计的&#xff0c;以满足对…

苍穹外卖-day04-套餐管理

1. 新增套餐 1.1 需求分析和设计 产品原型&#xff1a; 业务规则&#xff1a; 套餐名称唯一套餐必须属于某个分类套餐必须包含菜品名称、分类、价格、图片为必填项添加菜品窗口需要根据分类类型来展示菜品新增的套餐默认为停售状态 接口设计&#xff08;共涉及到4个接口&am…

Redis(windows+Linux)安装及入门

一、概述 Redis是什么&#xff1f; Redis(Remote Dictionary Server)&#xff0c;即远程字典服务 Redis&#xff08;Remote Dictionary Server )&#xff0c;即远程字典服务&#xff0c;是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数…

嵌入式学习笔记(65)野指针问题

3.3.1.神马是野指针&#xff1f;哪里来的&#xff1f;有什么危害&#xff1f; 我的理解&#xff1a;野指针就是定义了指针没有给指针赋值。 (1)野指针&#xff0c;就是指针指向的位置是不可知的&#xff08;随机的、不正确的、没有明确限制的&#xff09; (2)野指针很可能触…

傅立叶级数的意义--傅立叶级数是怎么来的

写这篇文章的起因是14年有道题目&#xff1a; 本题实质上是考察傅立叶级数的意义&#xff0c;因此要求扩大为不能只拘泥于傅里叶级数的计算相关问题&#xff0c;故作此篇。 一、课本上的内容 傅立叶级数&#xff1a; 设函数 f ( x ) f(x) f(x)是周期为 2 l 2l 2l的周期函数&…

Redis队列Stream

1 缘起 项目中处理文件的场景&#xff1a; 将文件处理请求放入队列&#xff0c; 一方面&#xff0c;缓解服务器文件处理压力&#xff1b; 另一方面&#xff0c;可以根据文件大小拆分到不同的队列&#xff0c;提高文件处理效率。 这是Java开发组Leader佳汇提出的文件处理方案&a…

了解性能测试流程

性能测试概念 我们经常看到的性能测试概念&#xff0c;有人或称之为性能策略&#xff0c;或称之为性能方法&#xff0c;或称之为性能场景分类&#xff0c;大概可以看到性能测试、负载测试、压力测试、强度测试等一堆专有名词的解释。 针对这些概念&#xff0c;我不知道你看到…

【数据分析】上市公司半年报数据分析

前言 前文介绍过使用网络技术获取上市公司半年报数据的方法&#xff0c;本文将对获取到的数据进行简要的数据分析。 获取数据的代码介绍在下面的两篇文章中 【java爬虫】使用selenium获取某交易所公司半年报数据-CSDN博客 【java爬虫】公司半年报数据展示-CSDN博客 全量数…

【uniapp】JavaScript基础学习-20231027

今天有找到一个比较好的网站 https://www.w3school.com.cn/js/index.asp 介绍也全面&#xff0c;内容也比较多。我觉得把最基本的语法看看&#xff0c;然后可以上手写代码了。其他的就是需要靠长期的学习和积累了。 基础语法的使用&#xff1a; 1、定义一个变量 2、对变量赋值 …

拿到 phpMyAdmin 如何获取权限

文章目录 拿到 phpMyAdmin 如何获取权限1. outfile 写一句话木马2. general_log_file 写一句话木马 拿到 phpMyAdmin 如何获取权限 1. outfile 写一句话木马 尝试使用SQL注入写文件的方式&#xff0c;执行 outfile 语句写入一句话木马。 select "<?php eval($_REQU…

10.29数算小复习(选择题细节,二路归并,结构体排序)

排序、复杂度、细节&#xff08;选择题&#xff0c;判断题&#xff09; 对于一个已经排好序的序列&#xff0c;直接插入排序的复杂度是O(n)&#xff0c;而归并排序的复杂度是O(nlogn)。这时候归并排序就不比直接插入排序速度快了。 归并排序的最好、最坏、平均时间都是O(nlogn)…

【Spring】Spring MVC请求响应

文章目录 1. 请求1.1 传递单个参数1.2 传递多个参数1.3 传递对象1.4 后端参数重命名1.5 传递数组1.6 传递集合1.7 传递JSON对象1.8 获取URL中参数1.9 上传⽂件1.10 获得Cookie1.11 获得Session1.12 获得Header 2. 响应2.1 返回静态界面2.2 返回数据2.3 返回HTML代码片段2.4 返回…

微机原理:汇编语言程序设计

文章目录 一、汇编格式1、文字简述2、代码表述 二、汇编语言结构说明1、方式选择伪指令2、段定义语句3、段约定语句4、汇编结束语句5、返回DOS语句 三、实例1、例子2、汇编语言程序开发过程 四、功能调用DOS功能调用1、功能号01H2、功能号02H3、功能号09H4、功能号0AH5、举例 B…

操作系统——二级页表(王道视频p50)

1.总体概述&#xff1a; 2.二级页表的工作原理——如何实现一个逻辑地址到物理地址的转换 具体工作原理(有一个地方没有弄明白——就是到底是如何通过顶级页表找到 二级页表项的&#xff1f;)

el-input 给icon图标绑定点击事件

选择suffix-icon&#xff0c;添加点击事件 <temeplate><el-form-item :label"$t(company[Company address])" prop"address"><el-input v-model"enterpriseForm.address"><i slot"suffix" class"el-icon-m…