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;
}

何時一樽酒,重與細論文。