昨天的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] 。
说明:
- 输入的先决条件是由边缘列表表示的图形,而不是邻接矩阵。详情请参见图的表示法。
- 你可以假定输入的先决条件中没有重复的边。
提示: - 这个问题相当于查找一个循环是否存在于有向图中。如果存在循环,则不存在拓扑排序,因此不可能选取所有课程进行学习。
- 通过 DFS 进行拓扑排序 - 一个关于Coursera的精彩视频教程(21分钟),介绍拓扑排序的基本概念。
- 拓扑排序也可以通过 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
有点偏了,下个澡去公司干活了!快乐打工人冉宝,快乐加倍!