Leetcode0802. 找到最终的安全状态(medium,DFS,拓扑排序)

目录

1. 题目描述

2. 方法一:深度优先搜索

2.1 思路

2.2 代码实现

3. 方法二:深度优先搜索+三色标记法

3.1 思路

3.2 代码

4. 方法三:拓扑排序


1. 题目描述

有一个有 n 个节点的有向图,节点按 0 到 n - 1 编号。图由一个 索引从 0 开始 的 2D 整数数组 graph表示, graph[i]是与节点 i 相邻的节点的整数数组,这意味着从节点 i 到 graph[i]中的每个节点都有一条边。

如果一个节点没有连出的有向边,则它是 终端节点 。如果没有出边,则节点为终端节点。如果从该节点开始的所有可能路径都通向一个 终端节点 ,则该节点为 安全节点 。

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

示例 1:

输入:graph = [[1,2],[2,3],[5],[0],[5],[],[]]
输出:[2,4,5,6]
解释:示意图如上。
节点5和节点6是终端节点,因为它们都没有出边。
从节点2、4、5和6开始的所有路径都指向节点5或6。

示例 2:

输入:graph = [[1,2,3,4],[1,2],[3,4],[0,4],[]]
输出:[4]
解释:
只有节点4是终端节点,从节点4开始的所有路径都通向节点4。

提示:

  • n == graph.length
  • 1 <= n <= 10^4
  • 0 <= graph[i].length <= n
  • graph[i] 按严格递增顺序排列。
  • 图中可能包含自环。
  • 图中边的数目在范围 [1, 4 * 10^4] 内。

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/find-eventual-safe-states
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

2. 方法一:深度优先搜索

2.1 思路

        2维数组graph为输入图的邻接表(adjacency list)的表示方式。

        根据题意,终端节点本身也是属于安全节点。

        

        首先根据graph很容易确定终端节点,由于是有向图,所以如果graph[i]为空的话就表示它是终端节点。

        对每个非终端节点node_j进行深度优先遍历,看看它能够到达的不同的终端节点的个数。如果是1则为安全节点,否则为非安全节点。        

        一个节点如果不是安全节点,那能够到达这个节点的节点就都不是安全节点。但是反过来不成立,即一个节点是安全节点,并不意味着能到达它的节点也都是安全节点。比如说在以上示例1中,节点2是安全节点,节点1能够到达2,但是它同时还和节点3、节点0形成环路,因此并不是安全节点。

        显而易见的是,如果图中存在环路(包括自环)的话,则该环路中的所有节点都不是终端节点,而这个环是它们的一个公共环路路径,所以它们都不是安全节点。

        因此,可以以以下方式遍历各个节点,针对每个节点进行深度优先路径遍历:

  • (1) 从某个非终端节点出发进行深度优先遍历,存在以下几种结果:
    • (a) 途中到达某个非安全节点,因此属于非安全节点,标记其为非安全节点
    • (b) 只能到达一个终端节点,因此是安全节点,标记其为安全节点
    • (c) 可以到达多个终端节点,但是不在任何环路以内,因此也不是安全节点,标记其为安全节点
    • (d) 存在于某个环路中,因此不是安全节点,而且该环中所有的节点都不是安全节点,将该环路上所有的节点都标记为非安全节点
  • (2) 重复步骤(1)直到所有节点都完成了安全非安全分类

        关于有向无环图(DAG)的深度优先路径搜索参见Leetcode0797. 所有可能的路径(medium, DFS)。本题为有向图但是可能存在环路,所以需要多一个环路的判断处理。

        初始实现后提交出错:[[],[0,2,3,4],[3],[4],[]],预期输出为[0,1,2,3,4]。反复查看代码,并手动画图确认,觉得不可能错啊。。。难道我抓到了leetcode的一个BUG?tan(90)!看了看别人提交的题解,看到有人说看错了题目,再回头读一读题目,确实。。。“所有可能路径都通向一个 终端节点”,这个描述太具有迷惑性了,我的第一感就是所有路径都要通向同一个终端节点,笑哭。这样,其实情况变得简单了。以上处理步骤修订如下:

  • (1) 从某个非终端节点出发进行深度优先遍历,存在以下几种结果:
    • (a) 途中到达某个非安全节点,因此属于非安全节点,标记其为非安全节点
    • (b) 存在于某个环路中,因此不是安全节点,而且该环中所有的节点都不是安全节点,将该环路上所有的节点都标记为非安全节点
    • (c) 每条路径都结束于终端节点,因此是安全节点,标记其为安全节点
  • (2) 重复步骤(1)直到所有节点都完成了安全非安全分类

2.2 代码实现

from typing import List

class Solution:
    def eventualSafeNodes(self, graph: List[List[int]]) -> List[int]:
        # print(graph)
        endnodes = set()
        secure   = set()
        insecure = set()
        for k in range(len(graph)):
            if len(graph[k])==0:
                secure.add(k)
                endnodes.add(k)
            elif k in graph[k]:
                # self-loop
                insecure.add(k)
            
        def dfs(path)->bool:
            # ret: Whether it is judged as insecure. 
            #      Even True, it is still not clear whether secure
            
            # print('dfs(): ', path, secure,insecure)
            # Traverse all neighbor nodes of the last node
            is_insecure = False
            for node in graph[path[-1]]:
                # print('\t\t', node)
                if node in insecure:
                    insecure.add(path[0])
                    return True
                elif node in path:
                    # loop found
                    insecure.update(set(path))
                    return True
                elif node in endnodes:
                    # secure.add(path[0])
                    # return 
                    continue
                else:
                    is_insecure = is_insecure or dfs(path+[node])
            return is_insecure

        for n in range(len(graph)):
            if (n not in insecure) and (n not in secure):
                if not dfs([n]):
                    secure.add(n)
                else:
                    insecure.add(n)                
        return sorted(list(secure))
                
if __name__ == "__main__":
    
    sln = Solution()
    
    graph = [[1,2],[2,3],[5],[0],[5],[],[]]
    print(sln.eventualSafeNodes(graph))
    
    graph = [[1,2,3,4],[1,2],[3,4],[0,4],[]]
    print(sln.eventualSafeNodes(graph))
    
    graph = [[],[0,2,3,4],[3],[4],[]]
    print(sln.eventualSafeNodes(graph))

        遗憾地超时了。 

3. 方法二:深度优先搜索+三色标记法

3.1 思路

        由于不要求安全节点的所有路径都终结于同一个节点,所以,只要是不在环中的节点其实就都是安全节点。所以这个问题简化为搜索图中存在环。可以使用深度优先搜索来找环,并在深度优先搜索时,用三种颜色对节点进行标记,标记的规则如下:

  • 白色(用 0 表示):该节点尚未被访问;
  • 灰色(用 1 表示):该节点位于递归栈中,或者在某个环上;
  • 黑色(用 2 表示):该节点搜索完毕,是一个安全节点。

        当我们首次访问一个节点时,将其标记为灰色,并继续搜索与其相连的节点。

        如果在搜索过程中遇到了一个灰色节点,则说明找到了一个环,此时退出搜索,栈中的节点仍保持为灰色,这一做法可以将「找到了环」这一信息传递到栈中的所有节点上。

        如果搜索过程中没有遇到灰色节点,则说明没有遇到环,那么递归返回前,我们将其标记由灰色改为黑色,即表示它是一个安全的节点。

3.2 代码

class Solution:
    def eventualSafeNodes(self, graph: List[List[int]]) -> List[int]:
        # print(graph)
        
        flag = len(graph)*[0]
        
        def dfs(x)->bool:
            if flag[x]>0:
                # this node has already been visited
                return flag[x] == 2
            flag[x] = 1 # flagged as visited
            for neighbour in graph[x]:
                if not dfs(neighbour):
                    return False
            flag[x] = 2
            return True
        
        return [k for k in range(len(graph)) if dfs(k)]

        执行用时:116 ms, 在所有 Python3 提交中击败了70.22%的用户

        内存消耗:20.7 MB, 在所有 Python3 提交中击败了25.21%的用户

4. 方法三:拓扑排序

        官解还给出了基于拓扑排序的一种解法,思路摘抄如下,代码略。

        根据题意,若一个节点没有出边,则该节点是安全的;若一个节点出边相连的点都是安全的,则该节点也是安全的。

        根据这一性质,我们可以将图中所有边反向,得到一个反图,然后在反图上运行拓扑排序。

        具体来说,首先得到反图 rg 及其入度数组 inDeg。将所有入度为 0 的点加入队列,然后不断取出队首元素,将其出边相连的点的入度减一,若该点入度减一后为 0,则将该点加入队列,如此循环直至队列为空。循环结束后,所有入度为 0 的节点均为安全的。我们遍历入度数组,并将入度为 0 的点加入答案列表。

        回到主目录:笨牛慢耕的Leetcode每日一解题解笔记(动态更新。。。)

猜你喜欢

转载自blog.csdn.net/chenxy_bwave/article/details/124209742