冉宝的每日一题-8月15日-- 拓扑排序。

该博客主要介绍了LeetCode 851题——喧闹和富有,通过拓扑排序寻找在有钱人群中最安静的人。作者首先解释了题意,接着详细阐述了解题思路,包括如何构建拓扑排序和使用深度优先搜索。最后,作者讨论了官方题解中的动态规划方法,并分析了时间复杂度和空间复杂度。

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

今天来一个有意思的题目,昨天看了任素汐的电影《寻汉记》,她确实有个问题,就是演谁都像它自己。

851. 喧闹和富有

题目描述

https://leetcode-cn.com/problems/loud-and-rich/
在一组 N 个人(编号为 0, 1, 2, …, N-1)中,每个人都有不同数目的钱,以及不同程度的安静(quietness)。

为了方便起见,我们将编号为 x 的人简称为 "person x "。

如果能够肯定 person x 比 person y 更有钱的话,我们会说 richer[i] = [x, y] 。注意 richer 可能只是有效观察的一个子集。

另外,如果 person x 的安静程度为 q ,我们会说 quiet[x] = q 。

现在,返回答案 answer ,其中 answer[x] = y 的前提是,在所有拥有的钱不少于 person x 的人中,person y 是最安静的人(也就是安静值 quiet[y] 最小的人)。

示例:
输入:richer = [[1,0],[2,1],[3,1],[3,7],[4,3],[5,3],[6,3]], quiet = [3,2,5,4,6,1,7,0]
输出:[5,5,2,5,4,5,6,7]
解释
answer[0] = 5,
person 5 比 person 3 有更多的钱,person 3 比 person 1 有更多的钱,person 1 比 person 0 有更多的钱。
唯一较为安静(有较低的安静值 quiet[x])的人是 person 7,
但是目前还不清楚他是否比 person 0 更有钱。

answer[7] = 7,
在所有拥有的钱肯定不少于 person 7 的人中(这可能包括 person 3,4,5,6 以及 7),
最安静(有较低安静值 quiet[x])的人是 person 7。

其他的答案也可以用类似的推理来解释。
提示

  • 1 <= quiet.length = N <= 500
  • 0 <= quiet[i] < N,所有 quiet[i] 都不相同。
  • 0 <= richer.length <= N * (N-1) / 2
  • 0 <= richer[i][j] < N
  • richer[i][0] != richer[i][1]
  • richer[i] 都是不同的。
  • 对 richer 的观察在逻辑上是一致的。

思路:

用时: 14:25-15:20
这道题解析下来可以理解为:
输入:

  • richer --> 可以构成一个拓扑排序
  • quiet --> 一个绝对序列,也可以构成一个拓扑排序,就是可能构成的序列比较多。
    输出: answer
    answer[x] = y,
    按照:所有钱不少于 person x 的人中,person y 是最安静的人, 而且0 <= richer.length <= N * (N-1) / 2 , 说明有一些人的财富关系不明朗。可能构成好几条路线,
    如例题的财富关系:
    在这里插入图片描述

所以 按照安静程度升序,所以安静程度不相同且从0-1,那么quiet[x]就是安静的顺序。

遍历richer 构建 richer_dict:
richer_dict[x]= set() 钱不少于x的人。

所以构建answer只用顺次遍历,先写自己,然后如果有更有钱且更安静的就更新,否则不更新。
有个问题就是可能嵌套有钱,a比b 有钱, b比 c 有钱,可能不直接存在ac关系。
所以需要先使用拓扑排序 把确定的关系维护好。

拓扑排序模板:
参考博客:
(1)拓扑排序模板: https://blog.youkuaiyun.com/ciecus_csdn/article/details/119430673?spm=1001.2014.3001.5501
请添加图片描述
(2)拓扑排序例题 https://blog.youkuaiyun.com/ciecus_csdn/article/details/119481723?spm=1001.2014.3001.5501

所以我们可以将拓扑排序总结如下:
单向边
(1)前期准备: 构建每个节点的入度列表 inDegree,以及每个节点的出边
(2)准备遍历: stack = [入度为0的点的列表] ,res =[]
每次往res + 1 点,这个点的出边的顺序为0.
每个节点的入度-1,可以添加进res 。

所以这道题可以这样,构建 richer_dict
从入度为0 的开始更新答案,然后顺次更新。

from collections import defaultdict
class Solution:
    def loudAndRich(self, richer: List[List[int]], quiet: List[int]) -> List[int]:
        n = len(quiet)
        answer = [None for i in range(n)]
        # 入度列表和出边
        in_degree = [0] * n 
        out_graph = defaultdict(list)
        in_graph = defaultdict(list)
        for start,end in richer:
            in_degree[end] += 1
            out_graph[start].append(end)
            in_graph[end].append(start)
        stack = [i for i,in_d in enumerate(in_degree) if in_d == 0]
        while stack:
            node = stack.pop(0)
            if answer[node] is not None:continue

            richer_list = in_graph[node]
            ans = node 
            for rich in richer_list:
                # 这里要放他们之前的结果,肯定有钱人已经出了结果了
                if quiet[answer[rich]] < quiet[ans]:
                    ans = answer[rich] 
            answer[node] = ans 

            for nn in out_graph[node]:
                in_degree[nn] -= 1 
                if in_degree[nn] == 0:
                    stack.append(nn)
        return answer


缺点用时比较长,还是不太行,写完总结之后还是要提升速度,加油。但是要先把周总结写了。加油呀
请添加图片描述

阅读一下官方题解吧,差点忘了进步。
在这里插入图片描述
这题的解法,肯定是从 4,5,6,2 开始更新是最节省计算资源的,也就是我上面的写法,从底层开始更新。

方法一: 缓存深度优先搜索

题解的这个思路也是,认为这个题其实就是个图的题目。
如果 y 比 x 富有, y 就在x的子树中。
利用dfs(persion) 获得person子树上最安静的人。然后这道题目又保证了有向无环图(DAG)。
–> dfs(person) 的结果有两种:(1)person本身 (2) min(dfs(child))
执行完图的后序遍历时,可以将dfs(person) 的值缓存为 answer[person], 可以将算法复杂度从平方阶降到线性阶。
官方题解:

class Solution():
	def loudAndRich(self,richer,quiet):
		N = len(quiet)
		graph = [[] for _ in range(N)]
		for u,v in richer:
			graph[v].append(u). # 只有入图
		answer = [None] * N 
		def dfs(node):
			# want least quiet person in this subtree
			if answer[node] is None:
				answer[node] = node 
				for child in graph[node]:
					cand = dfs(child)
					if quiet[cand] < quiet[answer[node]]:
						answer[node] = cand 
			return answer[node]
		return map(dfs,range(N))
  • 时间复杂度: O ( N 2 ) O(N^2) O(N2) , 其中 N 为总人数,遍历 richer 数组,在每个新遍历到的人比前一饿都更需要的情况下,最多包含 N(n-1)/2 个元素。
  • 空间复杂度: O ( N 2 ) O(N^2) O(N2),维护边的图。

今天的每日一题 576. 出界的路径数

https://leetcode-cn.com/problems/out-of-boundary-paths/
给你一个大小为 m x n 的网格和一个球。球的起始坐标为 [startRow, startColumn] 。你可以将球移到在四个方向上相邻的单元格内(可以穿过网格边界到达网格之外)。你 最多 可以移动 maxMove 次球。

给你五个整数 m、n、maxMove、startRow 以及 startColumn ,找出并返回可以将球移出边界的路径数量。因为答案可能非常大,返回对 109 + 7 取余 后的结果。

示例 1:
在这里插入图片描述

输入:m = 2, n = 2, maxMove = 2, startRow = 0, startColumn = 0
输出:6
示例 2:

在这里插入图片描述

输入:m = 1, n = 3, maxMove = 3, startRow = 0, startColumn = 1
输出:12

提示:

1 <= m, n <= 50
0 <= maxMove <= 50
0 <= startRow < m
0 <= startColumn < n

思考

时间: 21: 22 - 21:36
每个点可以走到4个位置,合法的话下一步还可以走,不合法,下一步就去掉。
写完了dfs解法:

class Solution:
    def findPaths(self, m: int, n: int, maxMove: int, startRow: int, startColumn: int) -> int:
        def is_valid(i,j):
            if i < 0 or i >= m:
                return False
            if j < 0 or j >= n:
                return False 
            return True 
        def next_node(i,j):
            return [(i+1,j),(i-1,j),(i,j-1),(i,j+1)]

        res = 0 
        step_stack = [(startRow,startColumn)]
        for step in range(maxMove):
            next_step = []
            for node in step_stack:
                nn = next_node(node[0],node[1])
                for nn_ in nn:
                    if is_valid(nn_[0],nn_[1]):
                        next_step.append(nn_)
                    else:
                        res += 1
            step_stack = next_step[:]

        return res % (10**9 + 7)
  • 时间复杂度: o ( 4 s t e p ) o(4^step) o(4step),每次会过滤一部分点,但是也是有限的。
  • 空间复杂度: o ( 4 s t e p ) o(4^step) o(4step) ,因为里面会有重复的点。

结果: 73 / 94 个通过测试用例
果然不出所料,超时了,
如果说哪里可以优化,就是如何判断 从 A 点 走到 B 点的种类数。
next_step 列表里面有重复的元素,重复的次数就是不同路径的次数。

还是看题解吧,时间不多啦,今天还要总结这两周做的题目,还有下周工作计划。

看到题解看到一半有了优化思路: 使用字典记录第step到点的次数,然后注意深copy

from collections import defaultdict
from copy import deepcopy
class Solution:
    def findPaths(self, m: int, n: int, maxMove: int, startRow: int, startColumn: int) -> int:
        def is_valid(i,j):
            if i < 0 or i >= m:
                return False
            if j < 0 or j >= n:
                return False 
            return True 
        def next_node(i,j):
            return [(i+1,j),(i-1,j),(i,j-1),(i,j+1)]

        res = 0 
        step_stack = {(startRow,startColumn):1}
        for step in range(maxMove):
            next_step = defaultdict(int)# 对应的点出现的次数
            for node,times in step_stack.items():
                nn = next_node(node[0],node[1])
                for nn_ in nn:
                    if is_valid(nn_[0],nn_[1]):
                        next_step[nn_] += times # 这里需要注意记录次数
                    else:
                        res += times
            step_stack = deepcopy(next_step)

        return res % (10**9 + 7)

请添加图片描述

  • 时间复杂度: o ( 4 ∗ s t e p ∗ m ∗ n ) o(4*step*m*n) o(4stepmn),每个step会查4个点,然后到达的范围时m*n的点之内。
  • 空间复杂度: o ( m ∗ n ) o(m*n) o(mn) ,用了两个字典,大小都约莫 m*n,记录到达次数。

官方题解思路

思路一: 动态规划

利用动态规划计算出界的路径数。
三阶动态规划:动态规划的状态由移动次数、行、列决定。
dp[i][j][k] 代表球移动i次之后,位于坐标(j,k)的路径数量,
当 i = 0 的时候,球坐标一定位于起始坐标 (startRow,startColumn),
因此动态规划的边界

  • I=0, dp[0][startRow][startColumn] = 1 , 其他的 都为0.
    转移方程:
    如果球移动了i次之后位于(j,k), 下一次的坐标一定是 (j-1,k) | (j+1,k) | (j,k-1) | (j,k+1) 中的一个,
    我们假设下一步的坐标为(j1,k1)
    如果(j1,k1)合法,可以将 dp[i+1][j1][k1] += dp[i][j][k]
    否则, res += dp[i][j][k]

根据dp[i][][] 计算 dp[i+1][][] 所以,时间空间复杂度都是 O(maxMovemn)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值