什么是拓扑排序?

在图论中,拓扑排序(Topological Sorting)是一个有向无环图(DAG, Directed Acyclic Graph)的所有顶点的线性序列。且该序列必须满足下面两个条件:

每个顶点出现且只出现一次

若存在一条从顶点 A 到顶点 B 的路径,那么在序列中顶点 A 出现在顶点 B 的前面

有向无环图(DAG)才有拓扑排序,非DAG图没有拓扑排序一说

拓扑排序有什么用?

它可以用来解决形如:

有n个任务需要完成,每个任务有一个耗时和一个前置任务列表,前置任务完成后才能开始当前任务。求完成所有任务的最短时间

这样具有依赖关系的问题

拓扑排序的实现

实现拓扑排序的关键就是维护一个入度为0的顶点的集合


Eg:P1113 杂务

题目描述

John的农场在给奶牛挤奶前有很多杂务要完成,每一项杂务都需要一定的时间来完成它。比如:他们要将奶牛集合起来,将他们赶进牛棚,为奶牛清洗乳房以及一些其它工作。尽早将所有杂务完成是必要的,因为这样才有更多时间挤出更多的牛奶。当然,有些杂务必须在另一些杂务完成的情况下才能进行。比如:只有将奶牛赶进牛棚才能开始为它清洗乳房,还有在未给奶牛清洗乳房之前不能挤奶。我们把这些工作称为完成本项工作的准备工作。至少有一项杂务不要求有准备工作,这个可以最早着手完成的工作,标记为杂务$1$。John有需要完成的$n$个杂务的清单,并且这份清单是有一定顺序的,杂务$k(k>1)$的准备工作只可能在杂务$1$至$k-1$中。

写一个程序从$1$到$n$读入每个杂务的工作说明。计算出所有杂务都被完成的最短时间。当然互相没有关系的杂务可以同时工作,并且,你可以假定John的农场有足够多的工人来同时完成任意多项任务。

输入格式

第1行:一个整数$n$,必须完成的杂务的数目($3 \le n \le 10,000$);

第$2$至$(n+1)$行: 共有$n$行,每行有一些用$1$个空格隔开的整数,分别表示:

* 工作序号($1$至$n$,在输入文件中是有序的);

* 完成工作所需要的时间$len(1 \le len \le 100)$;

* 一些必须完成的准备工作,总数不超过$100$个,由一个数字$0$结束。有些杂务没有需要准备的工作只描述一个单独的$0$,整个输入文件中不会出现多余的空格。

输出格式

一个整数,表示完成所有杂务所需的最短时间。

样例 #1

样例输入 #1

1
2
3
4
5
6
7
8
7
1 5 0
2 2 1 0
3 3 2 0
4 6 1 0
5 1 2 4 0
6 8 2 4 0
7 4 3 5 6 0

样例输出 #1

1
23
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
#include <iostream>
#include <vector>
#include <queue>
using namespace std;

#define MAXN 10001
vector<int> followUpTask[MAXN];//邻接表存图,对于下标为i的节点,其存储的为i的后续任务
int inDrgee[MAXN]; //入度
int timeNeed[MAXN]; //任务所需时间
int timeOver[MAXN]; //第i号任务总耗时

int n; //图的节点数

int _max(int a, int b) {
return a > b ? a : b;
}

int tuopu() {
queue<int> q;
for (int i = 1; i <= n; i++) {
if (!inDrgee[i]) { //统计入度为0的点,放入队列,准备拓扑排序
q.push(i);
timeOver[i] = timeNeed[i]; //对于入度为0的点,它的总耗时就是自己任务所需时间
}
}

while (q.size()) {
//pre 存取的是入度为0的点(任务可以独立进行,或任务前置任务已经完成的任务点)
int pre = q.front(); q.pop();

//对于pre的后续任务,我们进行一个遍历
for (auto& crt : followUpTask[pre]) {
//后续任务的耗时等于:max( 后续任务耗时 , pre任务+后续任务耗时 )
timeOver[crt] = _max(timeOver[crt], timeOver[pre] + timeNeed[crt]);

//如果该后续任务的所有前置均完成(入度为0),将该后续任务放入队列,作为拓扑排序的新节点
if (!--inDrgee[crt])q.push(crt);
}
}

int ans = 0;
for (int i = 1; i <= n; i++) {
ans = _max(ans, timeOver[i]);
}


return ans;
}

int main() {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);

cin >> n;

int idx;
for (int i = 1; i <= n; i++) {
cin >> idx >> timeNeed[i];

int preTask;
cin >> preTask;
while (preTask) {
//反向存储
//对于前置任务preTask,存储其后续任务
followUpTask[preTask].push_back(i);
inDrgee[i]++; //后续任务入度加一
cin >> preTask;
}
}

cout << tuopu();

return 0;
}

自问自答:

Q:为什么是取max?

A:因为任务是不限制人数的可并行完成模式,在已经计算出每个任务所需耗时后,所有任务中的最大耗时即为完成所有任务的最短耗时

利用拓扑排序思维的不建图实现(硬性要求1号任务不需要前置任务)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#include <iostream>
#include <vector>
using namespace std;

int itime[10001];
int finishTime[10001];
vector<int> prepare[10001];

int _max(int a, int b) {
return a > b ? a : b;
}

int main() {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int n;
cin >> n;

int idx;
for (int i = 1; i <= n; i++) {
cin >> idx >> itime[i];

int prepa;
cin >> prepa;
while (prepa) {
prepare[i].push_back(prepa);
cin >> prepa;
}
}


int ans = 0;
finishTime[1] = itime[1];
for (int i = 2; i <= n; i++) {
for (auto& t : prepare[i]) {
finishTime[i] = _max(finishTime[i], finishTime[t] + itime[i]);
}
ans = _max(ans, finishTime[i]);
}

cout << ans;

return 0;
}