a. 问题分析
在关键路径方法中,我们需要找到项目中的关键路径,即影响项目总持续时间的关键活动序列。为了解决这个问题,我们首先构建了一个表示项目活动的数据结构 Activity
,并设计了一个 Project
类来处理项目的计算和输出。
关键路径的计算主要依赖于两个关键步骤:
- 计算最早开始时间(ES):从首事件开始,通过拓扑排序和动态规划,计算每个活动的最早开始时间。
- 计算最晚开始时间(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. 数据结构设计
我们使用了两个关键的数据结构:
Activity
结构体:用于表示项目中的活动,包括活动编号、持续时间和后续活动的信息。// 表示项目活动的结构体 struct Activity { int id; // 活动编号 int duration; // 持续时间 vector<int> next; // 后续活动 };
Project
类:用于管理项目,包括添加活动、计算最早开始时间、计算最晚开始时间、打印关键路径和时间信息等方法。
d. 调试过程
在调试过程中,我们主要关注以下方面:
- 数据结构的正确性:确保
Activity
结构体和Project
类正确地表示了项目和活动的关系。 - 算法的正确性:验证计算最早开始时间和最晚开始时间的算法是否正确。
- 输出结果的准确性:检查打印的关键路径和时间信息是否符合预期。
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;
}