a. 问题分析

在关键路径方法中,我们需要找到项目中的关键路径,即影响项目总持续时间的关键活动序列。为了解决这个问题,我们首先构建了一个表示项目活动的数据结构 Activity,并设计了一个 Project 类来处理项目的计算和输出。

关键路径的计算主要依赖于两个关键步骤:

  1. 计算最早开始时间(ES):从首事件开始,通过拓扑排序和动态规划,计算每个活动的最早开始时间。
  2. 计算最晚开始时间(LS):从末事件开始,通过反向遍历拓扑排序和动态规划,计算每个活动的最晚开始时间。

通过比较最早开始时间和最晚开始时间,我们可以确定关键路径上的活动,这些活动对项目总持续时间具有关键影响。

b. 算法设计

1. 计算最早开始时间

我们使用拓扑排序和动态规划来计算最早开始时间。首先,找到所有首事件(没有前置事件的活动),然后从这些首事件开始遍历图,更新每个活动的最早开始时间。

// 计算最早开始时间
void calculateEarliestStart() {
    queue<int> q;
    for (const Activity& activity : activities) {// 遍历拓扑排序
        if (activity.next.empty()) {//如果某节点邻接表为空(即为首事件)
            q.push(activity.id);//入队
            earliestStart[activity.id] = 0;//设置最早开始时间为0
        }
    }

    while (!q.empty()) {//队不为空
        int currentId = q.front();//取出最优先事件currentId
        q.pop();

        for (int nextId : activities[currentId].next) {//遍历currentId的邻接表
            earliestStart[nextId] = max(earliestStart[nextId], earliestStart[currentId] + activities[currentId].duration);
            // nextId的最早开始时间==max{当前记录最早开始时间,前置节点currentId最早开始时间+currentId事件时间}
            q.push(nextId);
        }
    }
}

2. 计算最晚开始时间

通过反向遍历拓扑排序,我们可以计算最晚开始时间。从末事件开始,逐步更新每个活动的最晚开始时间。

// 计算最晚开始时间
void calculateLatestStart() {
    latestStart = earliestStart;//初始化最晚开始时间为最早开始时间

    for (int i = activities.size() - 1; i >= 0; --i) {//从最后的事件i开始反向遍历
        for (int nextId : activities[i].next) {
            latestStart[i] = min(latestStart[i], latestStart[nextId] - activities[i].duration);
            // 当前事件最晚开始时间==min{已记录最晚开始时间,下一事件nextId最晚开始时间-当前事件i所需时间}
        }
    }
}

c. 数据结构设计

我们使用了两个关键的数据结构:

  1. Activity 结构体:用于表示项目中的活动,包括活动编号、持续时间和后续活动的信息。
    // 表示项目活动的结构体
    struct Activity {
     int id;      // 活动编号
     int duration; // 持续时间
     vector<int> next; // 后续活动
    };
  2. Project 类:用于管理项目,包括添加活动、计算最早开始时间、计算最晚开始时间、打印关键路径和时间信息等方法。

d. 调试过程

在调试过程中,我们主要关注以下方面:

  1. 数据结构的正确性:确保 Activity 结构体和 Project 类正确地表示了项目和活动的关系。
  2. 算法的正确性:验证计算最早开始时间和最晚开始时间的算法是否正确。
  3. 输出结果的准确性:检查打印的关键路径和时间信息是否符合预期。

e. 输出结果

运行程序后,我们得到以下输出结果:

Critical Path: 0 1 2 3 4 5 
Activity 0: ES=0, LS=0
Activity 1: ES=2, LS=2
Activity 2: ES=6, LS=6
Activity 3: ES=9, LS=9
Activity 4: ES=14, LS=14
Activity 5: ES=16, LS=16

从结果中可以看到,关键路径上的活动是 0、1、2、3、4、5,并且每个活动的最早开始时间和最晚开始时间都正确计算。这表明我们的算法和数据结构设计是有效的。

f. 源代码

#include <iostream>
#include <vector>
#include <queue>
#include <algorithm>

using namespace std;

// 表示项目活动的结构体
struct Activity {
    int id;      // 活动编号
    int duration; // 持续时间
    vector<int> next; // 后续活动
};

class Project {
private:
    vector<Activity> activities; // 存储项目活动的向量
    vector<int> earliestStart;   // 最早开始时间
    vector<int> latestStart;     // 最晚开始时间

public:
    // 添加活动
    void addActivity(int id, int duration, const vector<int>& next) {
        activities.push_back({id, duration, next});
    }

    // 计算最早开始时间
    void calculateEarliestStart() {
        queue<int> q;
        for (const Activity& activity : activities) {// 遍历拓扑排序
            if (activity.next.empty()) {//如果某节点邻接表为空(即为首事件)
                q.push(activity.id);//入队
                earliestStart[activity.id] = 0;//设置最早开始时间为0
            }
        }

        while (!q.empty()) {//队不为空
            int currentId = q.front();//取出最优先事件currentId
            q.pop();

            for (int nextId : activities[currentId].next) {//遍历currentId的邻接表
                earliestStart[nextId] = max(earliestStart[nextId], earliestStart[currentId] + activities[currentId].duration);
                // nextId的最早开始时间==max{当前记录最早开始时间,前置节点currentId最早开始时间+currentId事件时间}
                q.push(nextId);
            }
        }
    }

    // 计算最晚开始时间
    void calculateLatestStart() {
        latestStart = earliestStart;//初始化最晚开始时间为最早开始时间

        for (int i = activities.size() - 1; i >= 0; --i) {//从最后的事件i开始反向遍历
            for (int nextId : activities[i].next) {
                latestStart[i] = min(latestStart[i], latestStart[nextId] - activities[i].duration);
                // 当前事件最晚开始时间==min{已记录最晚开始时间,下一事件nextId最晚开始时间-当前事件i所需时间}
            }
        }
    }

    // 打印关键路径
    void printCriticalPath() {
        cout << "Critical Path: ";
        for (const Activity& activity : activities) {//正序遍历拓扑排序
            if (earliestStart[activity.id] == latestStart[activity.id]) {//关键路径上事件最早开始时间==最晚开始时间
                cout << activity.id << " ";
            }
        }
        cout << endl;
    }

    // 打印最早开始时间和最晚开始时间
    void printTimeInfo() {
        for (const Activity& activity : activities) {
            cout << "Activity " << activity.id << ": ES=" << earliestStart[activity.id]
                 << ", LS=" << latestStart[activity.id] << endl;
        }
    }

    // 构造初始化函数
    Project(int numActivities) : earliestStart(numActivities), latestStart(numActivities) {}

};

int main() {
    Project project(6); // 假设有6个活动

    // 添加活动,每个活动的格式是:活动编号,持续时间,后续活动编号
    project.addActivity(0, 2, {1});
    project.addActivity(1, 4, {2});
    project.addActivity(2, 3, {3});
    project.addActivity(3, 5, {4});
    project.addActivity(4, 2, {5});
    project.addActivity(5, 1, {});

    // 计算最早开始时间和最晚开始时间
    project.calculateEarliestStart();
    project.calculateLatestStart();

    // 打印关键路径和时间信息
    project.printCriticalPath();
    project.printTimeInfo();

    return 0;
}
Last modification:December 13, 2023
我很可爱,请给我钱。