今天来一个有意思的题目,昨天看了任素汐的电影《寻汉记》,她确实有个问题,就是演谁都像它自己。
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(4∗step∗m∗n),每个step会查4个点,然后到达的范围时m*n的点之内。
- 空间复杂度: o ( m ∗ n ) o(m*n) o(m∗n) ,用了两个字典,大小都约莫 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)