冉宝的每日一题--8月6日、8月7日--今天想学会拓扑排序

本文通过分析802找到最终的安全状态、457环形数组是否存在循环等题目,探讨了拓扑排序在解决图论问题中的方法。讲解了如何构建反图、进行拓扑排序,并介绍了快慢指针等技巧。同时,拓扑排序在解决210课程表2和310最小高度树问题中的应用也进行了讨论。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

昨天的leetcode的每日一题是是一题graph,还是困难题,果断放弃。从压缩状态我就看不懂了。

我觉得每日一题我能够一够的也就是 dfs,bfs + dijstra ,做人还是不要太难为自己。

然后昨天就把前天的题目看了一下,因为感觉我的dfs写法还是不对。

先继续前天的题目,几种解法都要学会!

今天继续图的题目吧!加油,

802 找到最终的安全状态

https://leetcode-cn.com/problems/find-eventual-sacfe-states/solution/zhao-dao-zui-zhong-de-an-quan-zhuang-tai-yzfz/

方法二:拓扑排序:

1- 构建反图 rg 以及 入度数组 inDeg
2- 将所有入度为0的点加入队列 – 这里就是最开始的点,不是反图的点。没有前驱的点就是入度为0 的点。
3- 不断取出队首元素,出边相连的点入度减1,如果该点入度-1后为0,该点加入队列,如此直到队列为空。
循环结束后,所有入度为0的恶点都是安全的,遍历入度数组,将入度为0的点加入答案列表。

from collections import deque
from typing import List
class Solution:
    def eventualSafeNodes(self, graph: List[List[int]]) -> List[int]:
        n = len(graph)
        rg = [[] for i in range(n)]
        inGraph = [len(ys) for ys in graph]
        for start,dsts in enumerate(graph):
            for dst in dsts:
                rg[dst].append(start)


        # 把入度为0 的加入队列
        dq = deque([node for node,ing in enumerate(inGraph) if ing == 0])
        while dq:
            node = dq.popleft()
            dsts = rg[node]
            for dst in dsts:
                inGraph[dst] -= 1
                if inGraph[dst] == 0:
                    dq.append(dst)
        return [node for node,ing in enumerate(inGraph) if ing == 0]

感觉根据模版写还是很快的,希望自己能够记住这个模版。

今天就一直拓扑排序吧!

457、 环形数组是否存在循环

存在一个不含0的环形数组nums,每个nums[i]都表示位于下表i的角色应该向前或者向后移动的下标个数:

  • 如果 nums[i] 是正数,向前 移动 nums[i] 步
  • 如果 nums[i] 是负数,向后 移动 nums[i] 步
    因为数组是 环形 的,所以可以假设从最后一个元素向前移动一步会到达第一个元素,而第一个元素向后移动一步会到达最后一个元素。

数组中的 循环 由长度为 k 的下标序列 seq :

遵循上述移动规则将导致重复下标序列 seq[0] -> seq[1] -> … -> seq[k - 1] -> seq[0] -> …
所有 nums[seq[j]] 应当不是 全正 就是 全负
k > 1

如果 nums 中存在循环,返回 true ;否则,返回 false 。

请添加图片描述
用时: 2:15 -
思路: 这道题是个链路题? 即每个节点到达的下一个节点唯一,可能存在多个路径,但是一个点的下面的路径就是很固定的模式了。

所以可以把节点分成三个状态

  • 0 : 没有访问过的
  • 1 : 正在访问的节点
  • 2 : 访问过的节点,

dfs找到环之后还要判断环的长度和环里面的数字是正还是负,所以需要记录环。
node_dict : 到node能不能形成过程环

dfs(node,pre_path): #
if node in node_dict: return node_dict[node]
if node in pre_path: # 形成环了,
# 环的长度:len(pre_path)- pre_path.index(node)
1- 如果环的长度 < 1 : return False
2- 环里面有正有负不行:
for nn in pre_path:
if nums[node] * node[nn] < 0:
return False
return True

next_node = (nums[node] + node ) % len(nums)
return dfs(next_node,pre_path)
class Solution:
    def circularArrayLoop(self, nums: List[int]) -> bool:
        len_num = len(nums)
        node_dict = {}
        def dfs(node,pre_path):
            if node in node_dict:
                return node_dict[node]
            
            if node in pre_path:
                # 判断环的长度
                len_circular = len(pre_path)- pre_path.index(node)
                if len_circular<=1: 
                    node_dict[node] = False
                    return False
                
                for nn in pre_path[pre_path.index(node):]:# 只判断环
                    if nums[node] * nums[nn] < 0:
                        node_dict[node] = False
                        return False
                node_dict[node] = True
                return True
            else:
                next_node =  (nums[node] + node ) % len(nums)
                print("next_node:",next_node)
                node_dict[node] = dfs(next_node,pre_path[:]+[node])
                return node_dict[node]
        
        for node in range(len_num):
            print(node_dict)
            if dfs(node,[]):
                return True 
        return False

dfs解法又超时的一天 —应该是栈的写法的问题:
换一种写法写
– 挣扎了一下,放弃了

官方题解使用快慢指针:
核心思想: 无向图找环: 快慢指针。

检查每个节点,快慢指针从当前节点出发,快指针每次移动两步,慢指针每次移动一步,期间每移动一次,需要检查单向边方向是否和初始方向一致,不一致,停止遍历,当前路径必然不满足条件。 备注:这里的核心思想是不用判断子路径是否合适

为了降低时间复杂度,可以标记每一个节点是否访问过,如果是访问过的节点可以停止遍历。

实际视线中,因为原数组元素不为0,所以只需要将原数组元素置为0,就可以当作访问过,遍历过程中,快慢指针相遇,或者移动防线改变,停止遍历,将快慢指针经过的点全部置为0.

特别 nums[I] 是n的整数倍的时候,循环长度为1,需要跳过这种情况。

from typing import  List
class Solution:
    def circularArrayLoop(self, nums: List[int]) -> bool:
        n = len(nums)
        def next(cur):
            return (cur + nums[cur]) %n

        for i,num in enumerate(nums):
            if num == 0:
                continue
            slow,fast = i,next(i)
            # 判断非0且方向相同
            while nums[slow] * nums[fast] > 0 and nums[slow]* nums[next(fast)] >0:
                if slow == fast:
                    if slow == next(slow):
                        break  # 循环长度为1
                    return True
                slow = next(slow)
                fast = next(next(fast))
            add = i
            while nums[add] * nums[next(add)] > 0:
                tmp = add 
                add = next(add)
                nums[tmp] = 0 
        return False
                        

  • 时间复杂度: O(n) , 其中n是环形数组的长度,我们至多遍历每个点4次,其中快指针两次,慢指针一次,置零1次。
  • 空间复杂度:o(1)

所以这道题的思路是什么呢?
就是从换方向不符合要求开始出发,作为前置条件而不是成环了之后再判断环里面方向。

141. 环形链表

简单的快慢指针题:
https://leetcode-cn.com/problems/linked-list-cycle/

用时: 4:55- 4:57

class Solution:
    def hasCycle(self, head: ListNode) -> bool:
        # 快慢指针,重合为True,结束不重合false
        fast = head 
        slow = head 
        while fast and fast.next and fast.next.next:
            fast = fast.next.next 
            slow = slow.next 
            if fast == slow:
                return True 
        return False

其实有思路的题目,我打代码还是挺快的。

太难了。

然后利用模版完成了之前不会做的课程表2

210 课程表2

https://leetcode-cn.com/problems/course-schedule-ii/submissions/

现在你总共有 n 门课需要选,记为 0 到 n-1。

在选修某些课程之前需要一些先修课程。 例如,想要学习课程 0 ,你需要先完成课程 1 ,我们用一个匹配来表示他们: [0,1]

给定课程总量以及它们的先决条件,返回你为了学完所有课程所安排的学习顺序。

可能会有多个正确的顺序,你只要返回一种就可以了。如果不可能完成所有课程,返回一个空数组。

示例 1:

输入: 2, [[1,0]] 
输出: [0,1]
解释: 总共有 2 门课程。要学习课程 1,你需要先完成课程 0。因此,正确的课程顺序为 [0,1] 。

示例 2:

输入: 4, [[1,0],[2,0],[3,1],[3,2]]
输出: [0,1,2,3] or [0,2,1,3]
解释: 总共有 4 门课程。要学习课程 3,你应该先完成课程 1 和课程 2。并且课程 1 和课程 2 都应该排在课程 0 之后。
     因此,一个正确的课程顺序是 [0,1,2,3] 。另一个正确的排序是 [0,2,1,3] 。

说明:

  1. 输入的先决条件是由边缘列表表示的图形,而不是邻接矩阵。详情请参见图的表示法。
  2. 你可以假定输入的先决条件中没有重复的边。
    提示:
  3. 这个问题相当于查找一个循环是否存在于有向图中。如果存在循环,则不存在拓扑排序,因此不可能选取所有课程进行学习。
  4. 通过 DFS 进行拓扑排序 - 一个关于Coursera的精彩视频教程(21分钟),介绍拓扑排序的基本概念。
  5. 拓扑排序也可以通过 BFS 完成。

代码:

from collections import defaultdict
from typing import List
class Solution:
    def findOrder(self, numCourses: int, prerequisites: List[List[int]]) -> List[int]:
        src_dict = defaultdict(list)
        inGraph = [0 for i in range(numCourses)] # 入度
        for dst, src in prerequisites:
            src_dict[src].append(dst)
            inGraph[dst] += 1 
        res = []
        # 入度为0的入栈
        stack = [node for node,indegree in enumerate(inGraph) if indegree ==0]
        while stack:
            node = stack.pop()
            res.append(node)
            for neigh in src_dict[node]:
                inGraph[neigh] -= 1 
                if inGraph[neigh] ==0:
                    stack.append(neigh)
        return res if len(res)==numCourses else []

核心:不用处理环,因为环永远有减不掉的入度,所以不会进入拓扑排序。

继续努力,再来一题!

310. 最小高度数

树是一个无向图,其中任何两个顶点只通过一条路径连接。 换句话说,一个任何没有简单环路的连通图都是一棵树。

给你一棵包含 n 个节点的树,标记为 0 到 n - 1 。给定数字 n 和一个有 n - 1 条无向边的 edges 列表(每一个边都是一对标签),其中 edges[i] = [ai, bi] 表示树中节点 ai 和 bi 之间存在一条无向边。

可选择树中任何一个节点作为根。当选择节点 x 作为根节点时,设结果树的高度为 h 。在所有可能的树中,具有最小高度的树(即,min(h))被称为 最小高度树 。

请你找到所有的 最小高度树 并按 任意顺序 返回它们的根节点标签列表

树的 高度 是指根节点和叶子节点之间最长向下路径上边的数量

思路一;通过拓扑排序获得数的深度,然后返回
实现时间: 5:24- 5:50

from collections import defaultdict
class Solution:
    def findMinHeightTrees(self, n: int, edges: List[List[int]]) -> List[int]:
        src_dict = defaultdict(list)
        for src,dst in edges:
            src_dict[src].append(dst)
            src_dict[dst].append(src)
    
        res_list = []
        res_value = n 

        for node in range(n):
            stack = [node]
            depth = 0 
            visted = [0 for i in range(n)]
            while stack and depth<= res_value:
                depth += 1 
                next_stack = []
                while stack:
                    cur = stack.pop()
                    visted[cur] = 1
                    neighbors = src_dict[cur] 
                    for neighbor in neighbors:
                        if not visted[neighbor]:
                            next_stack.append(neighbor)
                stack = next_stack[:]

            # print("node:",node,",depth:",depth)
            if depth > res_value:
                continue
            elif depth < res_value:
                res_value = depth
                res_list = [node]
            else:
                res_list.append(node)
        return res_list 
            

bfs 的解法总是死在第65个用例上。

分析: 越是靠里面的节点越有可能是最小高度数,
所以从边缘开始,先找到所有出度为1的节点,然后把所有出度节点为1的进入队列,然后不断bfs,最后找到的是两边同时朝中间靠近的节点,中间节点相当于距离2分,就是到两边距离最小的节点。

拓扑排序的最后一组节点就是生成最小高度树的节点列表
直观理解: 根节点越靠’内侧’(拓扑排序更靠后), 和其他点的最大距离(树的高度)越小, 可参考证明.
感觉不太好想,考试的时候能做出来普通的BFS感觉也挺好的 =_=

感觉略微有点难:

from typing import List
from collections import defaultdict
class Solution:
    def findMinHeightTrees(self, n: int, edges: List[List[int]]) -> List[int]:
        if n == 1: return [0]
        dic_adj = defaultdict(set)
        for u,v in edges:
            dic_adj[u].add(v)
            dic_adj[v].add(u)
        
        # queue的初始值是度为1的节点构成的列表
        que,last_que = [],[]
        for u in dic_adj:
            if len(dic_adj[u]) ==1:que.append(u)
        
        # 拓扑排序
        while que:
            last_que = que.copy()
            for _ in range(len(que)):
                # 1. 弹出degree =1 的节点
                u = que.pop(0)
                # 2。 更新dic_adj
                for v in dic_adj[u]:
                    dic_adj[v].remove(u)
                    # 加入新的一组degree =1 的点:
                    if len(dic_adj[v])==1:
                        que.append(v)
                dic_adj.pop(u)
        return last_que
        

有点偏了,下个澡去公司干活了!快乐打工人冉宝,快乐加倍!
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值