1. 原题
点此链接1
2. 解题思路
写在前面,参考博文2
本题其实考察的就是课本(高等教育出版社 - 陈越 - 《数据结构》)6.8节-关键路径的内容。课本中给出了三个公式,以分别计算三个要素:最早完成时间,最迟完成时间,机动时间。下述内容将从这三个要素来分析及说明程序实现的方法。
2.1 最早完成时间
摘自《数据结构》P250:
为了能够确定工程的最早完成时间,只需在开始事件到完成事件间寻找最长有向路径,它的长度就是答案。路径长度是指这条路径上所有活动时间的总和。
可以通过求解树中每个结点(事件)的最早完成时间,来计算整个工程的最早完成时间。若 E a r l i e s t [ i ] Earliest[i] Earliest[i]表示结点 i i i 的最早完成时间, C v , w C_{v,w} Cv,w表示 < v , w > <v,w> <v,w>边的权重,则有:
E a r l i e s t [ 1 ] = 0 E a r l i e s t [ w ] = max < v , w > ∈ E ( E a r l i e s t [ v ] + c v , w ) Earliest[1] = 0\\Earliest[w]=\max_{<v,w>\in E}(Earliest[v]+c_{v,w}) Earliest[1]=0Earliest[w]=<v,w>∈Emax(Earliest[v]+cv,w)
上式即求取最早完成时间的递推公式。如何使用程序语言实现之?可以考虑这样一种情况,在这个问题中,一棵正常的树必然有起始点,然后定义一个动作:擦除起始点S。
“擦除起始点S”应该包括以下动作:
- 更新起始点S周围所有邻接点的最早完成时间,即上述递推公式
- 消除起始点S对邻接点的影响,即将所有邻接点的入度-1
在“擦除起始点S”之后的树就是消除了起始点S影响的新树,然后不断作这个动作直到树空(或者找不到起始点)为止。
代码如下(如果你不清除变量是什么意思,查阅最后的代码):
/*
* 所有的起始点都要压入q中
* 入度为0的点就是起始点
*/
queue<int> q;
/* 1.计算最早时间 ********************************************************/
auto sumMax = 0;
// 将所有起始点压入
for (auto i = 0; i < num; i++)
if (!inDegree[i])
q.push(i);
int cnt = 0;
while (!q.empty())
{
auto index = q.front();
q.pop();
++cnt;
for (auto &r : forwardData[index])
{
earliest[r.first] = max(earliest[r.first], earliest[index] + r.second);
sumMax = max(sumMax, earliest[r.first]);
if (!--inDegree[r.first])
q.push(r.first);
}
}
// 判断图是否符合要求
if (cnt != num)
return Error();
2.2 最晚完成时间
摘自《数据结构》P251
同样,还可以求解每一事件 i i i 在不影响整个工程完成情况下的允许最晚完成时间 L a t e s t [ i ] Latest[i] Latest[i] 。计算是从结束事件开始,设结束顶点为事件 n n n,按事件拓扑的相反次序逐个顶点推算,直到工程的初始顶点为止。结束顶点的最晚完成事件等于它的最早完成时间,其他顶点按下式计算:
L a t e s t [ n ] = L a t e s t [ n ] L a t e s t [ v ] = min < v , w > ∈ E ( L a t e s t [ w ] − C v , w ) Latest[n] = Latest[n]\\ Latest[v]=\min_{<v,w>\in E} (Latest[w] - C_{v,w}) Latest[n]=Latest[n]Latest[v]=<v,w>∈Emin(Latest[w]−Cv,w)
上式就是求解最晚完成时间的递推公式,可以按照2.1节的思路编写代码,相比之下有这样几个点需要考虑:
- L a t e s t [ i ] 0 ≤ i ≤ N − 1 Latest[i]_{0\le i \le N-1} Latest[i]0≤i≤N−1的初始值怎么定
- 环路判断
代码如下(如果你不清除变量是什么意思,查阅最后的代码):
/* 2.计算最迟时间 ********************************************************/
latest = vector<int>(num, sumMax);
// 将所有终止点压入
for (auto i = 0; i < num; i++)
if (!outDegree[i])
q.push(i);
while (!q.empty())
{
auto index = q.front();
q.pop();
for (auto &r : reverseData[index])
{
latest[r.first] = min(latest[r.first], latest[index] - r.second);
if (!--outDegree[r.first])
q.push(r.first);
}
}
2.3 求取机动时间
摘自《数据结构》P251
各顶点的最早和最晚完成时间被求出以后,能够很容易地确定在不影响工程进度的前提下,每一活动最多能耽误的时间长短。这个时间是否为 0 0 0 决定了该活动是否为关键路径。求 < v , w > <v,w> <v,w> 边上的允许耽误的最大时间 D e l a y v , w Delay_{v,w} Delayv,w 采用的计算公式为:
D e l a y v , w = L a t e s t [ w ] − E a r l i e s t [ v ] − C v , w Delay_{v,w} = Latest[w] - Earliest[v] - C_{v,w} Delayv,w=Latest[w]−Earliest[v]−Cv,w
上式就是机动时间的求取公式。
2.4 输出问题
原题的输出描述比较难理解,实际上题目意思就是从所有的边中按照“输出要求”输出所有的关键路径。
输出要求:将一条边定义为
E
d
g
e
(
n
u
m
)
:
S
t
a
r
t
→
T
i
m
e
E
n
d
Edge(num):Start \xrightarrow[]{ Time } End\\
Edge(num):StartTimeEnd
上式中
n
u
m
num
num 输入的顺序,
S
t
a
r
t
Start
Start 为起点索引,
E
n
d
End
End 为终点索引,
T
i
m
e
Time
Time 为这条路径花费的时间。输出中
S
t
a
r
t
Start
Start 小者优先,如果
S
t
a
r
t
Start
Start相同,
n
u
m
num
num 大者优先。
2.5 最后一个问题
程序中并没有加入连通判断,不过最后也胜利过去了。
一个简单的想法:只剩下一个起始点,擦除其余起始点,然后求出最早开始时间后判断所有的终止点(出度为0)是否都被访问。
代码
/*
* 解题思路
* 采用非递归算法解题
*
* 1.求取每个顶点的最早时间
* 2.求取每个顶点的最迟时间
* 3.按照输出要求对每一条判断其机动时间以输出
* 对于 a->b这样一个活动
* 如果b的最迟时间-a的最早时间 = c(a->b) 即机动时间为0,那就是关键路径
* 否则就不是
*
* 1.求取最早时间
* 将所有的起点压入队列Q
* 从队列Q中的点V开始,考虑S的每一个邻接点W
* 队列非空
* {
* 擦除 S 对 W 的影响 -> 更新W的权重(最大值即最早时间),更新W的入度
* 如果W的入度为0,表明这是一个新起点,则压入队列Q
* }
* 如果所有点都已访问则图无回路
*
* 2.求取最迟时间
* 与 1 相同,反向即可,注意更新权重时需要考虑最小值
*
*/
#include <iostream>
#include <algorithm>
#include <vector>
#include <queue>
#include <map>
#include <set>
#include <cmath>
using namespace std;
int Error()
{
cout << 0 << endl;
return 0;
}
using std::cout;
class Edge
{
public:
int start = 0;
int end = 0;
int time = 0;
Edge(int s, int e, int t) : start(s), end(e), time(t) {}
};
typedef pair<int, int> TPair;
int main()
{
int num, M;
cin >> num >> M;
vector<vector<TPair>> forwardData(num); // 原数据,正向存储
vector<vector<TPair>> reverseData(num); // 原数据,反向存储
vector<int> inDegree(num); // 入度
vector<int> outDegree(num); // 出度
vector<Edge> edges; // 存储所有的边
vector<int> earliest(num); // 最早时间
vector<int> latest(num); // 最迟时间
for (auto i = 0; i < M; i++)
{
//Todo: 将原本的1->N的排序转变为 0->N-1的排序方式
int s, e, time;
cin >> s >> e >> time;
forwardData[s - 1].emplace_back(e - 1, time);
reverseData[e - 1].emplace_back(s - 1, time);
edges.emplace_back(s - 1, e - 1, time);
++outDegree[s - 1];
++inDegree[e - 1];
}
queue<int> q;
/* 1.计算最早时间 ********************************************************/
auto sumMax = 0;
// 将所有起始点压入
for (auto i = 0; i < num; i++)
if (!inDegree[i])
q.push(i);
int cnt = 0;
while (!q.empty())
{
auto index = q.front();
q.pop();
++cnt;
for (auto &r : forwardData[index])
{
earliest[r.first] = max(earliest[r.first], earliest[index] + r.second);
sumMax = max(sumMax, earliest[r.first]);
if (!--inDegree[r.first])
q.push(r.first);
}
}
// 判断图是否符合要求
if (cnt != num)
return Error();
/* 2.计算最迟时间 ********************************************************/
latest = vector<int>(num, sumMax);
// 将所有终止点压入
for (auto i = 0; i < num; i++)
if (!outDegree[i])
q.push(i);
while (!q.empty())
{
auto index = q.front();
q.pop();
for (auto &r : reverseData[index])
{
latest[r.first] = min(latest[r.first], latest[index] - r.second);
if (!--outDegree[r.first])
q.push(r.first);
}
}
/* 3.按照题目要求输出 ****************************************************/
cout << sumMax << endl;
// 边集排序,以下排序的方式是起始点从大到小排,存储时的读取顺序从开始到结束排,所以输出时从末尾开始就行
std::sort(edges.begin(), edges.end(), [](const Edge &l, const Edge &r) -> bool { return (l.start) > (r.start); });
for (auto iter = edges.rbegin(); iter != edges.rend(); ++iter)
// 判断机动时间是否为0
if (!(latest[iter->end] - earliest[iter->start] - iter->time))
cout << iter->start + 1 << "->" << iter->end + 1 << endl;
return 0;
}