題目
下圖是一個軟件開發項目的活動圖,對於圖中每條邊的數字表示完成這條邊代表的活動的天數。例如,完成終止於里程碑E的活動需要 4 天時間。
對於每個活動,列出它的前驅,並計算最早開始時間、最晚開始時間和時差,然后確定出關鍵路徑。
—— 《軟件工程 第 4 版》中的原題
寫文緣由
網上的文章大都是對於 "點" 求最早開始時間和最晚開始時間。在我看來,是不准確的。
對於邊的解法,有的寫得又太復雜,還是自己寫吧。順便寫個程序自動化一下,舒服~
誤區在哪
需要注意的是,圖中的點,並不代表活動,並不能說活動 \(A\) 用 \(3\) 天到達活動 \(B\),這是不准確的,圖上的點應該理解為 "里程碑"。如果說到達 里程碑 \(I\) 的邊有兩條 \(D \rightarrow I\) 和 \(B \rightarrow I\),意思是有兩個活動,完成后到達里程碑 \(I\),並不能說 \(I\) 是個活動,如果這么理解會在計算最晚開始時間時出現錯誤。
還有一點,時間軸從 \(1\) 開始算,即從點 \(A\) 出發時,時刻為 \(1\)。有些解法是從 \(0\) 開始算的,本文從 \(1\) 開始算。
解法
- 正推求最早開始時間
- 公式:\(\text{ET}_{B·} = \text{MAX}(\text{ET}_{AB} + w_{AB} )\)
- 已知條件:起點的最早開始時間直接為 1
- 倒推求最晚開始時間
- 公式:\(\text{LT}_{JK} = \text{MIN}(\text{LT}_{K·} - w_{JK} )\)
- 已知條件:終點的最晚開始時間 \(\text{LT}_{L·}=\text{ET}_{L·}\) (因為終點一定在關鍵路徑上,關鍵路徑上的點最早開始時間等於最晚開始時間)
解題示例
解:
先求最早開始時間 (Earliest Time Start):
- \(A\) 是起點,所有由 \(A\) 出發的邊的最早開始時間都為 \(1\)。即 \(\text{ET}_{AB} = \text{ET}_{AE} = \text{ET}_{AC} =\text{ET}_{A·}= 1\)
- 來算 \(B\) 的最早開始時間,\(\text{ET}_{BD} = \text{ET}_{BI} = \text{ET}_{B·} =\text{MAX}(\text{ET}_{AB}+w_{AB}) =4\)。因為只有 \(A\) 才能到達 \(B\),所以 MAX 內只有一個值。
- 來算 \(E\) 的最早開始時間,\(\text{ET}_{EG} =\text{MAX}(\text{ET}_{AE}+w_{AE}) =5\)。因為只有 \(A\) 才能到達 \(E\),所以 MAX 內只有一個值。
- 來算 \(C\) 的最早開始時間,\(\text{ET}_{CF} =\text{MAX}(\text{ET}_{AC}+w_{AC}) =6\)。因為只有 \(A\) 才能到達 \(C\),所以 MAX 內只有一個值。
- 接下來 \(D、G、F\) 同理,省去廢話,結果是:\(\text{ET}_{DI} =\text{MAX}(\text{ET}_{BD}+w_{BD}) =9\) 、 \(\text{ET}_{GJ} =\text{ET}_{GH}=\text{ET}_{G·} =\text{MAX}(\text{ET}_{EG}+w_{EG}) =8\)、\(\text{ET}_{FH} =\text{MAX}(\text{ET}_{CF}+w_{CF}) =9\)
- 看一下 \(I\),入度為 \(2\),\(\text{ET}_{IJ} =\text{MAX}(\text{ET}_{DI}+w_{DI}, \text{ET}_{BI}+w_{BI}) =\text{MAX}(11, 10) =11\),下面的同理
- \(\text{ET}_{HK} =\text{MAX}(\text{ET}_{GH}+w_{GH}, \text{ET}_{FH}+w_{FH}) =\text{MAX}(11, 10) =11\)
- \(\text{ET}_{JL}=\text{ET}_{JK} =\text{MAX}(\text{ET}_{IJ}+w_{IJ}, \text{ET}_{GJ}+w_{GJ}) =\text{MAX}(13, 10) =13\)
- \(\text{ET}_{KL}=\text{MAX}(\text{ET}_{JK}+w_{JK}, \text{ET}_{HK}+w_{HK}) =\text{MAX}(15, 15) =15\)
- \(\text{ET}_{L·}=\text{MAX}(\text{ET}_{JL}+w_{JL}, \text{ET}_{KL}+w_{KL}) =\text{MAX}(21, 18) =21\)
自此,最早開始時間全部算完。
再求最晚開始時間 (Latest Time Start):
- 從終點倒着推,\(\text{LT}_{JL}=\text{MIN}(\text{LT}_{L·}-w_{JL}) =13\),\(\text{LT}_{KL}=\text{MIN}(\text{LT}_{L·}-w_{KL}) =18\)
- \(\text{LT}_{JK}=\text{MIN}(\text{LT}_{KL}-w_{JK}) =16\)
- \(\text{LT}_{IJ}=\text{MIN}(\text{LT}_{JL}-w_{IJ}, \text{LT}_{JK}-w_{IJ}) =11\)
- \(\text{LT}_{GJ}=\text{MIN}(\text{LT}_{JL}-w_{GJ}, \text{LT}_{JK}-w_{GJ}) =11\)
- \(\text{LT}_{HK}=\text{MIN}(\text{LT}_{KL}-w_{HK}) =14\)
- \(\text{LT}_{DI}=\text{MIN}(\text{LT}_{IJ}-w_{DI}) =9\)
- \(\text{LT}_{BI}=\text{MIN}(\text{LT}_{IJ}-w_{BI}) =5\)
- \(\text{LT}_{GH}=\text{MIN}(\text{LT}_{HK}-w_{GH}) =11\)
- \(\text{LT}_{BD}=\text{MIN}(\text{LT}_{DI}-w_{BD}) =4\)
- \(\text{LT}_{EG}=\text{MIN}(\text{LT}_{GJ}-w_{GH}, \text{LT}_{GH}-w_{EG}) =8\)
- \(\text{LT}_{FH}=\text{MIN}(\text{LT}_{HK}-w_{FH}) =13\)
- \(\text{LT}_{CF}=\text{MIN}(\text{LT}_{FH}-w_{CF}) =10\)
- \(\text{LT}_{AB}=\text{MIN}(\text{LT}_{BD}-w_{AB},\text{LT}_{BI}-w_{AB}) =1\)
- \(\text{LT}_{AE}=\text{MIN}(\text{LT}_{EG}-w_{AE}) =4\)
- \(\text{LT}_{AC}=\text{MIN}(\text{LT}_{CF}-w_{AC}) =5\)
(上述過程,看似繁瑣,但是考試計算時,在圖中對應的邊上邊寫邊算,還是挺快的)
根據上述數據,列表如下(其中冗余時間等於最早最晚兩者的差):
活動 | 前驅 | 最早開始時間 | 最晚開始時間 | 時差(冗余時間) |
---|---|---|---|---|
AB | 1 | 1 | 0 | |
BD | AB | 4 | 4 | 0 |
BI | AB | 4 | 5 | 1 |
DI | AB,BD | 9 | 9 | 0 |
IJ | AB,BD,DI,BI | 11 | 11 | 0 |
AE | 1 | 4 | 3 | |
EG | AE | 5 | 8 | 3 |
GJ | AE,EG | 8 | 11 | 3 |
JL | AB,BD,BI,DI,IJ,AE,EG,GJ | 13 | 13 | 0 |
AC | 1 | 5 | 4 | |
CF | AC | 6 | 10 | 4 |
FH | AC,CF | 9 | 13 | 4 |
GH | AE,EG | 8 | 11 | 3 |
HK | AE,EG,GH,AC,CF,FH | 11 | 14 | 3 |
JK | AB,BD,BI,DI,IJ,AE,EG,GJ | 13 | 16 | 3 |
KL | AB,BD,BI,DI,IJ,AE,EG,GJ,JK,GH,AC,CF,FH,HK | 15 | 18 | 3 |
由上述表格可知,\(AB、BD、DI、IJ、JL\) 活動的時差為 \(0\),即為關鍵節點,因此關鍵路徑為 \(A\rightarrow B\rightarrow D\rightarrow I\rightarrow J\rightarrow L=20\)。
程序實現
誒,寫個程序驗證一下手算的正確與否吧。
#include <bits/stdc++.h>
using namespace std;
#define rep(i,s,t) for(int i=s;i<=t;i++)
const int maxn = 105;
const int INF = 0x3f3f3f3f;
int n, m, S, T;
struct Edge {
int u, v, w, ET, LT;
Edge(int _u,int _v,int _w,int _ET,int _LT):u(_u),v(_v),w(_w),ET(_ET),LT(_LT){}
};
vector<Edge*> G[maxn], GT[maxn], Edges; // 正圖和反圖
int calcET(Edge *e)
{
if (e->u == S)
return e->ET = 1;
for(Edge *ee : GT[e->u])
e->ET = max(e->ET, (ee->ET==-1?calcET(ee):ee->ET) + ee->w);
return e->ET;
}
int calcLT(Edge *e)
{
if (e->u == T)
return e->LT = e->ET;
for(Edge *ee : G[e->v])
e->LT = min(e->LT, (ee->LT==INF?calcLT(ee):ee->LT) - e->w);
return e->LT;
}
bool vis[maxn];
vector<int> path;
void dfs(int u)
{
if (u == T)
{
path.push_back(u);
for(int i=0;i<path.size();i++)
printf(i!=path.size()-1?"%c->":"%c\n", path[i]+'A'-1);
path.pop_back();
return;
}
vis[u] = true;
for (Edge *e : G[u])
if(!vis[e->v] && e->ET==e->LT)
{
path.push_back(u);
dfs(e->v);
path.pop_back();
}
vis[u] = false;
return;
}
char s[5];
int main()
{
freopen("out.txt", "w", stdout);
scanf("%d%d",&n,&m);
rep(i,1,m)
{
int u, v, w;
scanf("%s", s); u = s[0]-'A'+1;
scanf("%s", s); v = s[0]-'A'+1;
scanf("%d", &w);
Edge* e = new Edge(u, v, w, -1, INF);
G[u].push_back(e);
GT[v].push_back(e);
Edges.push_back(e);
}
// 默認 1 是起點, n 是終點,起點入度為 0,終點出度為 0,數據合法。不是的話得改造程序求個拓撲之類的。
S = 1;
T = n;
// 算 ET
G[T].push_back(new Edge(T, -1, 0, -1, INF));
calcET(G[T].back());
// 算 LT
calcLT(new Edge(-1, S, 0, -1, INF));
// 輸出表
for(Edge *e : Edges)
printf("%c%c\t%d\t%d\t%d\n", e->u+'A'-1, e->v+'A'-1, e->ET, e->LT, e->LT-e->ET);
printf("%c.\t%d\t%d\t%d\n", G[T].back()->u+'A'-1, G[T].back()->ET, G[T].back()->LT, G[T].back()->LT-G[T].back()->ET);
// 求關鍵路徑
dfs(S);
return 0;
}
/*
12 16
A B 3
A E 4
A C 5
B D 5
B I 6
E G 3
C F 3
D I 2
I J 2
G J 2
G H 3
F H 1
J L 8
J K 2
H K 4
K L 3
12 15
A B 2
B C 3
B F 4
B D 2
C E 5
D G 3
E H 2
E F 3
F I 5
G I 6
I J 2
I K 4
J L 1
K L 2
H L 3
12 17
A B 5
A E 3
A C 4
B D 6
B I 4
E G 4
C F 3
D I 3
I J 3
G I 2
G J 7
F G 6
F H 3
J L 9
H J 3
H K 6
K L 2
12 17
A B 5
A E 3
A C 4
B D 6
B I 4
E G 4
C F 3
D I 5
I J 4
G I 2
G J 7
F G 6
F H 3
J L 9
H J 3
H K 6
K L 2
12 17
A B 6
A E 10
A C 4
B D 6
B I 4
E G 4
C F 3
D I 5
F G 6
G I 2
G J 7
I J 4
F H 3
J L 9
H J 3
H K 6
K L 2
*/
尾巴
好的,感謝你看到這里,對文章有錯誤的地方歡迎指出,謝謝。
如果覺得本文寫得不錯,不妨點贊、評論、收藏、分享,你的三連是對我最大的支持!
我的 Github:zhangt2333's Github
我的 CSDN:zhangt2333's CSDN
我的 博客園:zhangt2333's cnblog
我的 小書房:https://zhangt.top/
本文作者:zhangt2333
版權聲明:本博客所有文章除特別聲明外,均采用 CC BY-NC-SA 4.0 許可協議 。轉載請注明出處!