数据结构 _ 基础练习 _7-11 关键活动 _ 非递归解法

1. 原题

点此链接1

2. 解题思路

写在前面,参考博文2
  本题其实考察的就是课本(高等教育出版社 - 陈越 - 《数据结构》)6.8节-关键路径的内容。课本中给出了三个公式,以分别计算三个要素:最早完成时间,最迟完成时间,机动时间。下述内容将从这三个要素来分析及说明程序实现的方法。

2.1 最早完成时间

  摘自《数据结构》P250:

  为了能够确定工程的最早完成时间,只需在开始事件到完成事件间寻找最长有向路径,它的长度就是答案。路径长度是指这条路径上所有活动时间的总和。
  可以通过求解树中每个结点(事件)的最早完成时间,来计算整个工程的最早完成时间。若 E a r l i e s t [ i ] Earliest[i] Earliest[i]表示结点 i i i 的最早完成时间, C v , w C_{v,w} Cv,w表示 < v , w > <v,w> <v,w>边的权重,则有:

E a r l i e s t [ 1 ] = 0 E a r l i e s t [ w ] = max ⁡ < v , w > ∈ E ( E a r l i e s t [ v ] + c v , w ) Earliest[1] = 0\\Earliest[w]=\max_{<v,w>\in E}(Earliest[v]+c_{v,w}) Earliest[1]=0Earliest[w]=<v,w>Emax(Earliest[v]+cv,w)

  上式即求取最早完成时间的递推公式。如何使用程序语言实现之?可以考虑这样一种情况,在这个问题中,一棵正常的树必然有起始点,然后定义一个动作:擦除起始点S。
  “擦除起始点S”应该包括以下动作:

  1. 更新起始点S周围所有邻接点的最早完成时间,即上述递推公式
  2. 消除起始点S对邻接点的影响,即将所有邻接点的入度-1

  在“擦除起始点S”之后的树就是消除了起始点S影响的新树,然后不断作这个动作直到树空(或者找不到起始点)为止。

  代码如下(如果你不清除变量是什么意思,查阅最后的代码):

	/*
	 * 所有的起始点都要压入q中
	 * 入度为0的点就是起始点
	 */
    queue<int> q;
    /* 1.计算最早时间 ********************************************************/
    auto sumMax = 0;
    // 将所有起始点压入
    for (auto i = 0; i < num; i++)
        if (!inDegree[i])
            q.push(i);
    int cnt = 0;
    while (!q.empty())
    {
        auto index = q.front();
        q.pop();
        ++cnt;
        for (auto &r : forwardData[index])
        {
            earliest[r.first] = max(earliest[r.first], earliest[index] + r.second);
            sumMax = max(sumMax, earliest[r.first]);
            if (!--inDegree[r.first])
                q.push(r.first);
        }
    }
    // 判断图是否符合要求
    if (cnt != num)
        return Error();

2.2 最晚完成时间

  摘自《数据结构》P251

  同样,还可以求解每一事件 i i i 在不影响整个工程完成情况下的允许最晚完成时间 L a t e s t [ i ] Latest[i] Latest[i] 。计算是从结束事件开始,设结束顶点为事件 n n n,按事件拓扑的相反次序逐个顶点推算,直到工程的初始顶点为止。结束顶点的最晚完成事件等于它的最早完成时间,其他顶点按下式计算:
L a t e s t [ n ] = L a t e s t [ n ] L a t e s t [ v ] = min ⁡ < v , w > ∈ E ( L a t e s t [ w ] − C v , w ) Latest[n] = Latest[n]\\ Latest[v]=\min_{<v,w>\in E} (Latest[w] - C_{v,w}) Latest[n]=Latest[n]Latest[v]=<v,w>Emin(Latest[w]Cv,w)

  上式就是求解最晚完成时间的递推公式,可以按照2.1节的思路编写代码,相比之下有这样几个点需要考虑:

  1. L a t e s t [ i ] 0 ≤ i ≤ N − 1 Latest[i]_{0\le i \le N-1} Latest[i]0iN1的初始值怎么定
  2. 环路判断

  代码如下(如果你不清除变量是什么意思,查阅最后的代码):

/* 2.计算最迟时间 ********************************************************/
    latest = vector<int>(num, sumMax);
    // 将所有终止点压入
    for (auto i = 0; i < num; i++)
        if (!outDegree[i])
            q.push(i);
    while (!q.empty())
    {
        auto index = q.front();
        q.pop();
        for (auto &r : reverseData[index])
        {
            latest[r.first] = min(latest[r.first], latest[index] - r.second);
            if (!--outDegree[r.first])
                q.push(r.first);
        }
    }

2.3 求取机动时间

  摘自《数据结构》P251

  各顶点的最早和最晚完成时间被求出以后,能够很容易地确定在不影响工程进度的前提下,每一活动最多能耽误的时间长短。这个时间是否为 0 0 0 决定了该活动是否为关键路径。求 < v , w > <v,w> <v,w> 边上的允许耽误的最大时间 D e l a y v , w Delay_{v,w} Delayv,w 采用的计算公式为:
D e l a y v , w = L a t e s t [ w ] − E a r l i e s t [ v ] − C v , w Delay_{v,w} = Latest[w] - Earliest[v] - C_{v,w} Delayv,w=Latest[w]Earliest[v]Cv,w

  上式就是机动时间的求取公式。

2.4 输出问题

   原题的输出描述比较难理解,实际上题目意思就是从所有的边中按照“输出要求”输出所有的关键路径。
  输出要求:将一条边定义为
E d g e ( n u m ) : S t a r t → T i m e E n d Edge(num):Start \xrightarrow[]{ Time } End\\ Edge(num):StartTime End
  上式中 n u m num num 输入的顺序, S t a r t Start Start 为起点索引, E n d End End 为终点索引, T i m e Time Time 为这条路径花费的时间。输出中 S t a r t Start Start 小者优先,如果 S t a r t Start Start相同, n u m num num 大者优先。

2.5 最后一个问题

  程序中并没有加入连通判断,不过最后也胜利过去了。
  一个简单的想法:只剩下一个起始点,擦除其余起始点,然后求出最早开始时间后判断所有的终止点(出度为0)是否都被访问。

代码

/*
 * 解题思路
 * 采用非递归算法解题
 * 
 * 1.求取每个顶点的最早时间
 * 2.求取每个顶点的最迟时间
 * 3.按照输出要求对每一条判断其机动时间以输出
 * 对于 a->b这样一个活动
 *      如果b的最迟时间-a的最早时间 = c(a->b) 即机动时间为0,那就是关键路径
 *      否则就不是
 * 
 * 1.求取最早时间
 *      将所有的起点压入队列Q
 *      从队列Q中的点V开始,考虑S的每一个邻接点W
 *      队列非空
 *      {
 *          擦除 S 对 W 的影响 -> 更新W的权重(最大值即最早时间),更新W的入度
 *              如果W的入度为0,表明这是一个新起点,则压入队列Q
 *      }
 *      如果所有点都已访问则图无回路
 * 
 * 2.求取最迟时间
 *      与 1 相同,反向即可,注意更新权重时需要考虑最小值
 *
 */
 
#include <iostream>
#include <algorithm>
#include <vector>
#include <queue>
#include <map>
#include <set>
#include <cmath>

using namespace std;

int Error()
{
    cout << 0 << endl;
    return 0;
}

using std::cout;

class Edge
{
public:
    int start = 0;
    int end = 0;
    int time = 0;
    Edge(int s, int e, int t) : start(s), end(e), time(t) {}
};

typedef pair<int, int> TPair;

int main()
{
    int num, M;
    cin >> num >> M;

    vector<vector<TPair>> forwardData(num); // 原数据,正向存储
    vector<vector<TPair>> reverseData(num); // 原数据,反向存储
    vector<int> inDegree(num);              // 入度
    vector<int> outDegree(num);             // 出度
    vector<Edge> edges;                     // 存储所有的边

    vector<int> earliest(num); // 最早时间
    vector<int> latest(num);   // 最迟时间

    for (auto i = 0; i < M; i++)
    {
        //Todo: 将原本的1->N的排序转变为 0->N-1的排序方式
        int s, e, time;
        cin >> s >> e >> time;
        forwardData[s - 1].emplace_back(e - 1, time);
        reverseData[e - 1].emplace_back(s - 1, time);
        edges.emplace_back(s - 1, e - 1, time);
        ++outDegree[s - 1];
        ++inDegree[e - 1];
    }

    queue<int> q;
    /* 1.计算最早时间 ********************************************************/
    auto sumMax = 0;
    // 将所有起始点压入
    for (auto i = 0; i < num; i++)
        if (!inDegree[i])
            q.push(i);
    int cnt = 0;
    while (!q.empty())
    {
        auto index = q.front();
        q.pop();
        ++cnt;
        for (auto &r : forwardData[index])
        {
            earliest[r.first] = max(earliest[r.first], earliest[index] + r.second);
            sumMax = max(sumMax, earliest[r.first]);
            if (!--inDegree[r.first])
                q.push(r.first);
        }
    }
    // 判断图是否符合要求
    if (cnt != num)
        return Error();

    /* 2.计算最迟时间 ********************************************************/
    latest = vector<int>(num, sumMax);
    // 将所有终止点压入
    for (auto i = 0; i < num; i++)
        if (!outDegree[i])
            q.push(i);
    while (!q.empty())
    {
        auto index = q.front();
        q.pop();
        for (auto &r : reverseData[index])
        {
            latest[r.first] = min(latest[r.first], latest[index] - r.second);
            if (!--outDegree[r.first])
                q.push(r.first);
        }
    }

    /* 3.按照题目要求输出 ****************************************************/
    cout << sumMax << endl;
    // 边集排序,以下排序的方式是起始点从大到小排,存储时的读取顺序从开始到结束排,所以输出时从末尾开始就行
    std::sort(edges.begin(), edges.end(), [](const Edge &l, const Edge &r) -> bool { return (l.start) > (r.start); });
    for (auto iter = edges.rbegin(); iter != edges.rend(); ++iter)
        // 判断机动时间是否为0
        if (!(latest[iter->end] - earliest[iter->start] - iter->time))
            cout << iter->start + 1 << "->" << iter->end + 1 << endl;

    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值