文章目录
题目
标题和出处
标题:找到最终的安全状态
难度
5 级
题目描述
要求
有一个有 n \texttt{n} n 个结点的有向图,结点按 0 \texttt{0} 0 到 n − 1 \texttt{n} - \texttt{1} n−1 编号。图由一个下标从 0 \texttt{0} 0 开始 的二维整数数组 graph \texttt{graph} graph 表示, graphi \texttt{graphi} graphi 是与结点 i \texttt{i} i 相邻的结点的整数数组,这意味着从结点 i \texttt{i} i 到 graphi \texttt{graphi} graphi 中的每个结点都有一条边。
如果一个结点没有出边,则该结点为终端结点 。如果从一个结点开始的所有可能路径都通向终端结点 ,则该结点为安全结点。
返回一个由图中所有安全结点 组成的数组作为答案。答案数组中的元素应当按升序排列。
示例
示例 1:

输入: graph = \[1,2,2,3,5,0,5,\[\],\[\]] \texttt{graph = \[1,2,2,3,5,0,5,\[\],\[\]]} graph = \[1,2,2,3,5,0,5,\[\],\[\]]
输出: 2,4,5,6 \texttt{2,4,5,6} 2,4,5,6
解释:示意图如上。
结点 5 \texttt{5} 5 和结点 6 \texttt{6} 6 是终端结点,因为它们都没有出边。
从结点 2 \texttt{2} 2、 4 \texttt{4} 4、 5 \texttt{5} 5 和 6 \texttt{6} 6 开始的所有路径都指向结点 5 \texttt{5} 5 或 6 \texttt{6} 6。
示例 2:
输入: graph = \[1,2,3,4,1,2,3,4,0,4,\[\]] \texttt{graph = \[1,2,3,4,1,2,3,4,0,4,\[\]]} graph = \[1,2,3,4,1,2,3,4,0,4,\[\]]
输出: 4 \texttt{4} 4
解释:
只有结点 4 \texttt{4} 4 是终端结点,从结点 4 \texttt{4} 4 开始的所有路径都通向结点 4 \texttt{4} 4。
数据范围
- n = graph.length \texttt{n} = \texttt{graph.length} n=graph.length
- 1 ≤ n ≤ 10 4 \texttt{1} \le \texttt{n} \le \texttt{10}^\texttt{4} 1≤n≤104
- 0 ≤ graphi.length ≤ n \texttt{0} \le \texttt{graphi.length} \le \texttt{n} 0≤graphi.length≤n
- 0 ≤ graphij ≤ n − 1 \texttt{0} \le \texttt{graphij} \le \texttt{n} - \texttt{1} 0≤graphij≤n−1
- graphi \texttt{graphi} graphi 按严格递增顺序排列
- 图中可能包含自环
- 图中边的数目在范围 1, 4 × 10 4 \texttt{1, 4} \\times \\texttt{10}\^\\texttt{4}\\texttt{} 1, 4×104 内
解法
思路和算法
根据题意,以下两类结点是安全结点。
-
出度为 0 0 0 的结点是安全结点。
-
如果从一个结点出发的所有路径都到达出度为 0 0 0 的结点,则该结点是安全结点。
如果一个结点存在一个后继结点不是安全结点,则该结点不是安全结点。因此,一个出度不为 0 0 0 的结点是安全结点,等价于该结点的所有后继结点都是安全结点。
对于出度不为 0 0 0 的结点,判断结点是否为安全结点的依据是该结点的所有后继结点是否都是安全结点,因此可以从出度为 0 0 0 的结点反向遍历图,判断每个结点是否为安全结点。
首先遍历图,得到每个结点的出度和每个结点的所有前驱结点,然后从所有出度为 0 0 0 结点开始,使用广度优先搜索实现拓扑排序。
拓扑排序时,对于每个结点,执行如下操作。
-
得到该结点的所有前驱结点。
-
对于每个前驱结点,将前驱结点的出度减 1 1 1。如果在更新出度之后,前驱结点的出度变为 0 0 0,则继续对该前驱结点执行搜索。
遍历结束之后,所有出度为 0 0 0(包括更新出度之后变为 0 0 0)的结点为安全结点,将所有的安全结点按递增顺序返回。
拓扑排序的正确性说明如下。
-
拓扑排序从出度为 0 0 0 的结点开始遍历,出度为 0 0 0 的结点是安全结点。
-
假设已经遍历过的结点都是安全结点。当一个结点的出度变为 0 0 0 时,当前结点的所有后继结点都遍历过,即当前结点的所有后继结点都是安全结点,因此当前结点是安全结点。
由于拓扑排序只会遍历安全结点,且所有安全结点的出度一定会变为 0 0 0,因此拓扑排序可以得到正确的结果。
代码
java
class Solution {
public List<Integer> eventualSafeNodes(int[][] graph) {
List<Integer> safeList = new ArrayList<Integer>();
int n = graph.length;
int[] outdegrees = new int[n];
List<Integer>[] prevArr = new List[n];
for (int i = 0; i < n; i++) {
prevArr[i] = new ArrayList<Integer>();
}
for (int i = 0; i < n; i++) {
int[] adjacent = graph[i];
outdegrees[i] = adjacent.length;
for (int next : adjacent) {
prevArr[next].add(i);
}
}
Queue<Integer> queue = new ArrayDeque<Integer>();
for (int i = 0; i < n; i++) {
if (outdegrees[i] == 0) {
queue.offer(i);
}
}
while (!queue.isEmpty()) {
int node = queue.poll();
List<Integer> prevList = prevArr[node];
for (int prev : prevList) {
outdegrees[prev]--;
if (outdegrees[prev] == 0) {
queue.offer(prev);
}
}
}
for (int i = 0; i < n; i++) {
if (outdegrees[i] == 0) {
safeList.add(i);
}
}
return safeList;
}
}
复杂度分析
-
时间复杂度: O ( n + m ) O(n + m) O(n+m),其中 n n n 是图中的结点数, m m m 是图中的边数。建立反向图和拓扑排序需要 O ( n + m ) O(n + m) O(n+m) 的时间,拓扑排序之后遍历结点得到所有的安全结点需要 O ( n ) O(n) O(n) 的时间,因此时间复杂度是 O ( n + m ) O(n + m) O(n+m)。
-
空间复杂度: O ( n + m ) O(n + m) O(n+m),其中 n n n 是图中的结点数, m m m 是图中的边数。存储反向图需要 O ( n + m ) O(n + m) O(n+m) 的空间,拓扑排序时的队列需要 O ( n ) O(n) O(n) 的空间,因此空间复杂度是 O ( n + m ) O(n + m) O(n+m)。