下图是一个软件开发项目的活动图,对于图中每条边的数字表示完成这条边代表的活动的天数。例如,完成终止于里程碑E的活动需要 4 天时间。
对于每个活动,列出它的前驱,并计算最早开始时间、最晚开始时间和时差,然后确定出关键路径。
—— 《软件工程 第 4 版》中的原题
网上的文章大都是对于 “点” 求最早开始时间和最晚开始时间。在我看来,是不准确的。
对于边的解法,有的写得又太复杂,还是自己写吧。顺便写个程序自动化一下,舒服~
需要注意的是,图中的点,并不代表活动,并不能说活动 A A A 用 3 3 3 天到达活动 B B B,这是不准确的,图上的点应该理解为 “里程碑”。如果说到达 里程碑 I I I 的边有两条 D → I D \rightarrow I D→I 和 B → I B \rightarrow I B→I,意思是有两个活动,完成后到达里程碑 I I I,并不能说 I I I 是个活动,如果这么理解会在计算最晚开始时间时出现错误。
还有一点,时间轴从 1 1 1 开始算,即从点 A A A 出发时,时刻为 1 1 1。有些解法是从 0 0 0 开始算的,本文从 1 1 1 开始算。
先求最早开始时间 (Earliest Time Start):
自此,最早开始时间全部算完。
再求最晚开始时间 (Latest Time Start):
(上述过程,看似繁琐,但是考试计算时,在图中对应的边上边写边算,还是挺快的)
根据上述数据,列表如下(其中冗余时间等于最早最晚两者的差):
活动 | 前驱 | 最早开始时间 | 最晚开始时间 | 时差(冗余时间) |
---|---|---|---|---|
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 |
由上述表格可知, A B 、 B D 、 D I 、 I J 、 J L AB、BD、DI、IJ、JL AB、BD、DI、IJ、JL 活动的时差为 0 0 0,即为关键节点,因此关键路径为 A → B → D → I → J → L = 20 A\rightarrow B\rightarrow D\rightarrow I\rightarrow J\rightarrow L=20 A→B→D→I→J→L=20。
诶,写个程序验证一下手算的正确与否吧。
#include
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 许可协议 。转载请注明出处!