暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

【每日一题】802. 找到最终的安全状态:一题两解:DFS & 拓扑排序,简单易懂!

彤哥来刷题啦 2021-09-01
678

题目描述

在有向图中,以某个节点为起始节点,从该点出发,每一步沿着图中的一条有向边行走。如果到达的节点是终点(即它没有连出的有向边),则停止。

对于一个起始节点,如果从该节点出发,无论每一步选择沿哪条有向边行走,最后必然在有限步内到达终点,则将该起始节点称作是 安全的。

返回一个由图中所有安全的起始节点组成的数组作为答案。答案数组中的元素应当按 升序 排列。

该有向图有 n 个节点,按 0 到 n - 1 编号,其中 n 是 graph 的节点数。图以下述形式给出:graph[i] 是编号 j 节点的一个列表,满足 (i, j) 是图的一条有向边。

示例 1:

输入:graph = [[1,2],[2,3],[5],[0],[5],[],[]] 输出:[2,4,5,6] 解释:示意图如上。

示例 2:

输入:graph = [[1,2,3,4],[1,2],[3,4],[0,4],[]] 输出:[4]

链接:https://leetcode-cn.com/problems/find-eventual-safe-states

方法一、DFS深搜

这里运用的技巧是使用一个visited数组,0表示未访问,1表示访问中,2表示访问完毕且安全的节点,如果DFS的过程中再次遇到同一个节点(即visited[i]==1),则表示这个节点是不安全的。

代码如下:

class Solution {
    public List<Integer> eventualSafeNodes(int[][] graph) {
        // 存储结果集
        List<Integer> resultList = new ArrayList<>();

        int n = graph.length;

        // 0表示未访问过,1表示访问中,2表示是安全点
        int[] visited = new int[n];

        for (int i = 0; i < n; i++) {
            // 如果从当前节点出发是安全的,则加入结果集中
            if (safe(graph, i, visited)) {
                resultList.add(i);
            }
        }

        return resultList;
    }

    private boolean safe(int[][] graph, int index, int[] visited) {
        if (visited[index] != 0) {
            // 不是0,说明被访问过,判断是否为2
            return visited[index] == 2;
        }

        // 说明未被访问过,设置为访问中
        visited[index] = 1;
        for (int next : graph[index]) {
            // DFS过程中再次遇到相同的节点,说明有环,即为不安全,直接返回false
            if (!safe(graph, next, visited)) {
                return false;
            }
        }

        // 下游节点全部遍历完成,都是安全的,说明当前节点安全,返回true
        visited[index] = 2;
        return true;
    }
}

复制

这里visited的作用有三个:1、记录DFS过程中访问过的节点;2、剪枝,防止重复访问;3、记录安全节点。

  • 时间复杂度:O(n+m),n为节点数,m为边数。
  • 空间复杂度:O(n),需要额外的visited数组和结果List

方法二、拓扑排序

拓扑排序是寻找入度为0的节点,而题目给的数据是寻找最终能找到出度为0的节点,所以,我们可以用类似的思想。

我们可以按照以下步骤进行:

  1. 先整理出所有节点的出度(就是graph子数组的长度),
  2. 把出度为0的节点入队,
  3. 从队列中弹出这些节点,将入他们的那些节点的出度减一,若减到0,则把那个节点也入队
  4. 重复上述步骤,直到队列中没有元素了

因为要计算谁入了我,所以,要整理一下给定的数组,而你看到这样整理之后像是把原来的图倒过来了,所以,很多题解又叫他反图。

class Solution {
    public List<Integer> eventualSafeNodes(int[][] graph) {
        int n = graph.length;
        // 记录每个节点的出度
        int[] deg = new int[n];
        // 记录谁入了我
        List<List<Integer>> list = new ArrayList<>();
        for (int i = 0; i < n; i++) {
            list.add(new ArrayList<>());
        }
        // 整理数据
        for (int i = 0; i < n; i++) {
            deg[i] = graph[i].length;
            for (int j : graph[i]) {
                list.get(j).add(i);
            }
        }

        // BFS求拓扑排序的过程(此循环可以与上面的循环合并)
        Queue<Integer> queue = new LinkedList<>();
        for (int i = 0; i < n; i++) {
            if (deg[i] == 0) {
                queue.offer(i);
            }
        }

        // 循环找出所有符合的节点
        while (!queue.isEmpty()) {
            int index = queue.poll();
            // 找出谁入了我,把他们的出度减一
            List<Integer> whos = list.get(index);
            for (Integer who : whos) {
                if (--deg[who] == 0) {
                    queue.offer(who);
                }
            }
        }

        // 最后,deg中为0的元素就是我们要找的安全节点
        List<Integer> resultList = new ArrayList<>();
        for (int i = 0; i < n; i++) {
            if (deg[i] == 0) {
                resultList.add(i);
            }
        }

        return resultList;
    }
}

复制
  • 时间复杂度:O(n+m),n为节点数,m为边数。
  • 空间复杂度:O(n+m),需要额外的空间存储谁入了我,也就是反向图。

虽然时间复杂度与方法一一样,但是,处理过程中使用了更多次的循环,所以,整体运行时间更慢。

结语

如果你觉得我的讲解还不错,点个赞吧。

文章转载自彤哥来刷题啦,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论