1203-项目管理

Raphael Liu Lv10

n 个项目,每个项目或者不属于任何小组,或者属于 m 个小组之一。group[i] 表示第 i 个项目所属的小组,如果第 i
个项目不属于任何小组,则 group[i] 等于 -1。项目和小组都是从零开始编号的。可能存在小组不负责任何项目,即没有任何项目属于这个小组。

请你帮忙按要求安排这些项目的进度,并返回排序后的项目列表:

  • 同一小组的项目,排序后在列表中彼此相邻。
  • 项目之间存在一定的依赖关系,我们用一个列表 beforeItems 来表示,其中 beforeItems[i] 表示在进行第 i 个项目前(位于第 i 个项目左侧)应该完成的所有项目。

如果存在多个解决方案,只需要返回其中任意一个即可。如果没有合适的解决方案,就请返回一个 空列表

示例 1:

![](https://assets.leetcode-cn.com/aliyun-lc-
upload/uploads/2019/09/22/1359_ex1.png)

**输入:** n = 8, m = 2, group = [-1,-1,1,0,0,1,0,-1], beforeItems = [[],[6],[5],[6],[3,6],[],[],[]]
**输出:** [6,3,4,1,5,2,0,7]

示例 2:

**输入:** n = 8, m = 2, group = [-1,-1,1,0,0,1,0,-1], beforeItems = [[],[6],[5],[6],[3],[],[4],[]]
**输出:** []
**解释:** 与示例 1 大致相同,但是在排序后的列表中,4 必须放在 6 的前面。

提示:

  • 1 <= m <= n <= 3 * 104
  • group.length == beforeItems.length == n
  • -1 <= group[i] <= m - 1
  • 0 <= beforeItems[i].length <= n - 1
  • 0 <= beforeItems[i][j] <= n - 1
  • i != beforeItems[i][j]
  • beforeItems[i] 不含重复元素

📺 视频讲解

视频时间线

  • 解读题意:00:10
  • 拓扑排序简介:03:36
  • 示例 1 分析:07:50
  • 编码前的思路分析:09:59
  • 参考代码:12:18
  • 复杂度分析、练习:21:22

力扣君温馨小贴士

  • 觉得视频时间长的扣友,可以在视频右下角的「设置」按钮处选择 1.5 倍速或者 2 倍速观看;
  • 如果已经充分了解了题意,对「拓扑排序」这个知识点已经非常熟悉的朋友,可以直接观看「编码前的思路分析」(09:59 开始)。

1203 项目管理.mp4

📖 文字解析

这个问题标注为「困难」,但是解决这个问题用到的知识点其实大家都见过。要点和提示如下:

  1. 从问题的描述(任务计划安排)和结果要求(有可能不存在,如果有,可能不唯一),可以看出思路是「拓扑排序」;
  2. 读懂题目的意思,这一点非常重要,弄清楚输入的每一个变量的含义,弄清楚什么叫「无人接手」以及 group[i] = - 1 的含义;
  3. 安排任务的第一优先级:组(group)相同的项目必须排在一起,第二优先级:项目(item)的先后顺序满足拓扑序;
  4. 关键 1:题目给出的 itembeforItem 可以得到 item 须要满足的先后顺序。而每一个 item 或者对应一个 group,或者不属于任何一个给定的 group根据 itemgroup 的函数关系(从 item 可以得到唯一的 group 编号):可以得到安排组(group)的先后顺序(这一点很关键)
  5. 关键 2:分别对 groupitem 执行拓扑排序,从 item -> group 的(多对一关系)反向得到 group -> item 的一对多关系,由于 item 按照拓扑序排列,建立 groupitem 的一对多关系以后,每一个 group 对应的 item 列表是符合拓扑序的;
  6. group 的拓扑序,和 groupitem 的一对多关系,得到的 item 列表就是满足题目意思的一个结果。

下面是详解。

方法:拓扑排序

1. 如何得到组 group 的先后关系

  • 如果不考虑「同一小组的项目,排序后在列表中彼此相邻」。根据 beforeItems 可以得到项目 item 的拓扑排序结果;
  • 由数组 group 可以得到项目 item 对应的组的编号。

由以上两点,可以得到 group 的先后关系

以示例 1 为例:

img

表格第 2 行(不计算表头):项目 6 (组号 0)必需在项目 1 (不属于任何组,暂时记组号为 2)的前面,可以得出:组号为 0 的所有项目应该位于组号为 2 的所有项目的前面。

有可能出现这种情况:前驱项目和当前项目分在一组,这样的数据需要舍弃,例如第 5 行。项目 3 、项目 6 必需被安排在项目 4 的前面,但是它们的组号都为 0

2. 如何让相同组号的项目在一起

由上一步的结果得到「组的先后关系」,进而得到组 group 的拓扑排序结果。而 同组的项目也必须满足先后顺序,因此必需对项目 item 也执行一次拓扑排序。

按照项目 item 的拓扑排序结果,依次 把它们映射到对应的组 group 里,这一步需要建立一个映射关系:key 是组编号,value 是同一组的项目按照拓扑排序的结果。

再根据组 group 的拓扑排序的结果,以及上一步建立的映射关系,把 group 映射到 item 列表,就得到了题目要求的 ① 同组项目放在一起 ② 且项目之间还满足拓扑序的拓扑排序结果。

3. 细节:为什么须要给 group[i] = -1 的项目赋值为一个新的组号?

根据题意,group[i] = -1 表示编号为 i 的项目,不属于编号为 0m - 1 的这 m 个组,换句话说,完成这些项目不需要协同合作。

又因为我们需要根据「组」的先后顺序得到「组的拓扑排序的结果」。为了避免我们设计的算法认为 group[i] = -1 的这些项目都在同一组,因此给 group[i] = -1 的这些组一个不同于 0m - 1 的编号,从 m 开始逐个给每个项目编号即可。

参考代码

[]
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
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Queue;

public class Solution {

public int[] sortItems(int n, int m, int[] group, List<List<Integer>> beforeItems) {
// 第 1 步:数据预处理,给没有归属于一个组的项目编上组号
for (int i = 0; i < group.length; i++) {
if (group[i] == -1) {
group[i] = m;
m++;
}
}

// 第 2 步:实例化组和项目的邻接表
List<Integer>[] groupAdj = new ArrayList[m];
List<Integer>[] itemAdj = new ArrayList[n];
for (int i = 0; i < m; i++) {
groupAdj[i] = new ArrayList<>();
}
for (int i = 0; i < n; i++) {
itemAdj[i] = new ArrayList<>();
}

// 第 3 步:建图和统计入度数组
int[] groupsIndegree = new int[m];
int[] itemsIndegree = new int[n];

int len = group.length;
for (int i = 0; i < len; i++) {
int currentGroup = group[i];
for (int beforeItem : beforeItems.get(i)) {
int beforeGroup = group[beforeItem];
if (beforeGroup != currentGroup) {
groupAdj[beforeGroup].add(currentGroup);
groupsIndegree[currentGroup]++;
}
}
}

for (int i = 0; i < n; i++) {
for (Integer item : beforeItems.get(i)) {
itemAdj[item].add(i);
itemsIndegree[i]++;
}
}

// 第 4 步:得到组和项目的拓扑排序结果
List<Integer> groupsList = topologicalSort(groupAdj, groupsIndegree, m);
if (groupsList.size() == 0) {
return new int[0];
}
List<Integer> itemsList = topologicalSort(itemAdj, itemsIndegree, n);
if (itemsList.size() == 0) {
return new int[0];
}

// 第 5 步:根据项目的拓扑排序结果,项目到组的多对一关系,建立组到项目的一对多关系
// key:组,value:在同一组的项目列表
Map<Integer, List<Integer>> groups2Items = new HashMap<>();
for (Integer item : itemsList) {
groups2Items.computeIfAbsent(group[item], key -> new ArrayList<>()).add(item);
}

// 第 6 步:把组的拓扑排序结果替换成为项目的拓扑排序结果
List<Integer> res = new ArrayList<>();
for (Integer groupId : groupsList) {
List<Integer> items = groups2Items.getOrDefault(groupId, new ArrayList<>());
res.addAll(items);
}
return res.stream().mapToInt(Integer::valueOf).toArray();
}

private List<Integer> topologicalSort(List<Integer>[] adj, int[] inDegree, int n) {
List<Integer> res = new ArrayList<>();
Queue<Integer> queue = new LinkedList<>();
for (int i = 0; i < n; i++) {
if (inDegree[i] == 0) {
queue.offer(i);
}
}

while (!queue.isEmpty()) {
Integer front = queue.poll();
res.add(front);
for (int successor : adj[front]) {
inDegree[successor]--;
if (inDegree[successor] == 0) {
queue.offer(successor);
}
}
}

if (res.size() == n) {
return res;
}
return new ArrayList<>();
}
}

复杂度分析

说明:这里为了表达严谨,时间复杂度和空间复杂度的描述比较理论化。大家未必须要深究。就当前这个问题而言,拓扑排序就是就是在图中进行一次广度优先遍历,时间复杂度为图的顶点数加边数,假设当前问题的图结构的顶点数为 V、边数为 E,可以简记时间复杂度为 O(V + E)。

  • 时间复杂度:O(m + n^2 + E_{group} + E_{item} ),这里 n 是项目的总数,m 是组的总数(把标记为 -1 的组预处理以后,新的组的总数不会超过 2m);

    • 对组的数据进行预处理 O(m);
    • 构建组的邻接表 O(m)、项目的邻接表 O(n^2),遍历 before,最极端情况下,第 1 个顶点指向所有剩余 n - 1 个顶点,第 2 个顶点指向所有剩余 n - 2 个顶点,……;
    • 构建组的入度数组 O(m)、项目入度数组 O(n);
    • 执行组的拓扑排序O(m + E_{group})、项目的拓扑排序 O(n + E_{item})。这里用 E_{group 表示组的邻接表的边数, E_{item 表示项目的邻接表的边数;
    • 构建组到项目的一对多关系 O(m + n);
    • 输出符合题目要求的结果 O(m + n)。
  • 空间复杂度:O(m + n^2);

    • 组的邻接表 O(m)、项目的邻接表 O(n^2);
    • 组的入度数组 O(m)、项目入度数组 O(n);
    • 组到项目的一对多关系 O(m + n);
    • 符合题目要求的结果 O(m + n)。

练习

  1. 「力扣」第 207 题:课程表 (中等);
  2. 「力扣」第 210 题:课程表 II (中等);
  3. 「力扣」第 301 题:最小高度树 (中等);
  4. 「力扣」第 802 题:找到最终的安全状态 (中等);
  5. 「力扣」第 630 题:课程表 III (困难);
  6. 「力扣」第 329 题:矩阵中的最长递增路径 (困难);
  7. 「力扣」第 1245 题:树的直径 (中等);
  8. 「力扣」第 444 题:序列重建 (中等);
  9. 「力扣」第 1136 题:平行课程 (困难);
  10. 「力扣」第 269 题:火星词典 (困难)。
 Comments