7-11 关键活动

目录

题目

输入格式

输出格式

输入样例

输出样例 

题目分析

AOE网

Problem 1:求整个工程项目所需时间

Problem 2:判断哪些是关键路径上的活动

problem 3:如何按题目要求输出

代码


题目

原题链接

https://pintia.cn/problem-sets/1399202744970727424/problems/1418527362277498881

假定一个工程项目由一组子任务构成,子任务之间有的可以并行执行,有的必须在完成了其它一些子任务后才能执行。“任务调度”包括一组子任务、以及每个子任务可以执行所依赖的子任务集。

比如完成一个专业的所有课程学习和毕业设计可以看成一个本科生要完成的一项工程,各门课程可以看成是子任务。有些课程可以同时开设,比如英语和C程序设计,它们没有必须先修哪门的约束;有些课程则不可以同时开设,因为它们有先后的依赖关系,比如C程序设计和数据结构两门课,必须先学习前者。

但是需要注意的是,对一组子任务,并不是任意的任务调度都是一个可行的方案。比如方案中存在“子任务A依赖于子任务B,子任务B依赖于子任务C,子任务C又依赖于子任务A”,那么这三个任务哪个都不能先执行,这就是一个不可行的方案。

任务调度问题中,如果还给出了完成每个子任务需要的时间,则我们可以算出完成整个工程需要的最短时间。在这些子任务中,有些任务即使推迟几天完成,也不会影响全局的工期;但是有些任务必须准时完成,否则整个项目的工期就要因此延误,这种任务就叫“关键活动”。

请编写程序判定一个给定的工程项目的任务调度是否可行;如果该调度方案可行,则计算完成整个工程项目需要的最短时间,并输出所有的关键活动。

输入格式

输入第1行给出两个正整数N(≤100)和M,其中N是任务交接点(即衔接相互依赖的两个子任务的节点,例如:若任务2要在任务1完成后才开始,则两任务之间必有一个交接点)的数量。交接点按1~N编号,M是子任务的数量,依次编号为1~M。随后M行,每行给出了3个正整数,分别是该任务开始和完成涉及的交接点编号以及该任务所需的时间,整数间用空格分隔。

输出格式

如果任务调度不可行,则输出0;否则第1行输出完成整个工程项目需要的时间,第2行开始输出所有关键活动,每个关键活动占一行,按格式“V->W”输出,其中V和W为该任务开始和完成涉及的交接点编号。关键活动输出的顺序规则是:任务开始的交接点编号小者优先,起点编号相同时,与输入时任务的顺序相反。

输入样例

7 8
1 2 4
1 3 3
2 4 5
3 4 3
4 5 1
4 6 6
5 7 5
6 7 2

输出样例 

17
1->2
2->4
4->6
6->7

题目分析

小白在csdn上的第一篇题解,送给这道好不容易弄懂的数构题!!

AOE网

在写这道题之前,先复习一下 AOE网 的相关知识:(大佬自行跳过~)

AOE网,简单来说就是工程的带权有向图,其中:

  • 顶点:活动开始或者结束的事件
  • 边:活动
  • 边的权值:完成该活动所需的时间

在AOE网中,想要完成一项活动,必须要先完成在该活动前面的所有活动,例如下图中,想要完成活动e,必须要先完成活动abcd,完成活动a和c所需时间为3 + 2 = 5,完成活动b和d所需时间为5 + 4 = 9,二者取大,因此任务e的最早开始时间为9。 

由此我们可以知道,整个工程从开始到结束所需要花费的时间是起始点到终止点的最大路径长度(因为这样才可以保证在终止点前的所有任务都完成了),这个有最大路径长度的路径就是关键路径,关键路径上的活动就叫做关键活动。

然后来看题吧!!

题目要求我们输出整个工程项目所需时间和所有关键活动。

Problem 1:求整个工程项目所需时间

首先,题目没有告诉我们从哪个点开始到哪个点结束,所以需要先记录一下每个点的入度和出度,入度为0的点是起始点,出度为0的点就是终止点啦。

然后,用early数组记录每个点(事件)的最早发生时间,假设有事件i和j,它们之间有关系j -> i,那么事件i的最早发生时间early[i] = max(early[i], early[j] + weight<i, j>),(因为i前面的所有活动全部完成才能开始i,所以在前面的路径中选取最大值),这样到最后,early数组中的最大值就是整个工程的最早完成时间啦。

Problem 2:判断哪些是关键路径上的活动

话不多说举个栗子(还是以上面的图为例吧(才不是懒得画图))——

 结点4的最早发生时间上面说过啦是9,因为需要在活动abcd全部完成之后才能到结点4,换一种说法,就是在9的时间里,需要完成1->2->4和1->3->4这两段:

  • 先来看1->3->4这一段,1->3花费时间5,3->4花费时间4,所以想要在9时到达4,必须要在5时到达3,也就是事件3的最晚发生时间是5(如果5时没有到达事件3,那就不可能在9时到达事件4)
  • 再来看1->2->4这一段,1->2花费时间3,2->4花费时间2,我们知道只要在9时到达事件4即可,所以【最迟】可以在7时到达事件2(当然比7时早也没什么关系),也就是在7时到达事件2,可以保证我们9时能够到达事件4,从而不耽误整个工程

综上所述,事件2的最早发生时间是3,最晚发生时间是7,事件3的最早发生时间是5,最晚发生时间还是5.

当同一个事件的【最早发生时间】与【最晚发生时间】相等,就证明这个事件就是关键路径上的结点(很重要!!!)

 在problem1中我们求出了每个结点的最早发生时间early,可以利用early求出每个结点的最晚发生时间late,它们之间的关系是(前提:i->j)事件i的最晚发生时间late[i] = early[j] - weight[i, j]

现有事件 i 和 j (i -> j),当它们满足以下三点:

  1. i 和 j 之间有边
  2. i 和 j 的最早发生时间分别等于各自的最晚发生时间
  3. j的最早发生时间late[j] = i的最早发生时间early[i] + ij之间的边的权值weight[i, j]

就可以判断i->j是一个关键活动啦!!

problem 3:如何按题目要求输出

题目中要求:

关键活动输出的顺序规则是:任务开始的交接点编号小者优先,起点编号相同时,与输入时任务的顺序相反。

 因此输出中两层循环:

  1. 第一层从小到大,保证任务开始的交接点从小到大
  2. 第二层从大到小,保证起点编号相同情况下,与输入的顺序相反

代码

#include <iostream>
#include <queue>
#include <cmath>

using namespace std;

const int N = 210;
const int INF = 0x3f3f3f;

int n, m;

int edge[N][N]; // 记录两边之间的权值
int in[N], out[N]; // 分别记录每个点的入度与出度
int early[N], late[N]; // 记录每个事件的最早发生时间和最晚发生时间

int earlysolve()
{
    int idx = 0;
    queue<int> q; // q中存储入度为0的结点

    for (int i = 1; i <= n; i ++ )
    {
        // i入度为0就将i存入q
        if (in[i] == 0) q.push(i);
    }

    while (!q.empty()) // 当q不为空时进入循环
    {
        int t = q.front(); // 可以将t看作是起点
        idx ++ ; // 记录已经遍历的结点个数
        q.pop(); // 将取出的t从q中删去
        for (int i = 1; i <= n; i ++ )
        {
            if (edge[t][i] != INF)
            {
                in[i] -- ; // 记录下来t->i这条边之后就删去这条边
                early[i] = max(early[i], early[t] + edge[t][i]); // 找到和i相邻的最大边
                if (in[i] == 0) q.push(i); // 记录下i相邻的所有边之后就把i当做入度为0存进q中
            }
        }
    }

    // 遍历点的个数与点的总个数不相等 说明无法完成
    if (idx != n) return -1;
    else
    {
        int res = -2e9; // res是所有事件最早发生时间的最大值 也就是完成整个工程所需的时间
        for (int i = 1; i <= n; i ++ )
            res = max(res, early[i]);
        return res;
    }
}

void latesolve(int res)
{
    queue<int> q; // q存储出度为0的事件
    for (int i = 1; i <= n; i ++ )
    {
        if (out[i] == 0)
        {
            // 如果出度为0,说明是终点,最晚完成时间就等于整个工程的所需时间
            q.push(i);
            late[i] = res;
        }
    }
    while (!q.empty())
    {
        int t = q.front(); // t可以看作是当前子段的终点
        q.pop(); // 从q中删去已经取出的t
        for (int i = n; i >= 1; i -- )
        {
            if (edge[i][t] != INF)
            {
                out[i] -- ;
                late[i] = min(late[i], late[t] - edge[i][t]); // 要保证最长的i->t能完成,所以取min

                if (out[i] == 0) q.push(i); // 记录下i相邻的所有边之后就把i当做入度为0存进q中
            }
        }
    }
}

int main()
{
    cin >> n >> m;
    for (int i = 0; i < N; i ++ )
    {
        late[i] = INF;
        for (int j = 0; j < N; j ++ )
            edge[i][j] = INF;
    }
    for (int i = 1; i <= m; i ++ )
    {
        int a, b, c;
        cin >> a >> b >> c;
        edge[a][b] = c;
        in[b] ++ ;
        out[a] ++ ;
    }
    int res = earlysolve(); // 完成所有任务所需时间
    if (res == -1)
    {
        cout << 0 << endl;
        return 0;
    }
    latesolve(res);
    cout << res << endl;
    for (int i = 1; i <= n; i ++ )
        for (int j = n; j >= 1; j -- )
            if (edge[i][j] != INF && late[j] - early[i] == edge[i][j])
                cout << i << "->" << j << endl;
    return 0;
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Texcavator

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值