[英雄星球六月集训LeetCode解题日报] 第30日 拓扑排序
日报
- 今天的内容是拓扑排序,以前没接触过,第一次接触是上个月的今天。
- 今天凭自己做了1、4题,2、3看的题解才做的。
- 4题之前做过,是上个月的拓扑排序第二天,因此直接就做出来了。结果今天看到题完全不会做了。
- 上次忘了写题解,这次正好补上。
- 拓扑排序经常用来解决依赖关系的问题。
- 若b依赖a,那么a将出现在拓扑排序后b的前边。
题目
一、 2115. 从给定原材料中找到所有可以做出的菜
1. 题目描述
2. 思路分析
- 这题是每道菜有依赖关系,而一开始供给的材料是给出的。
- 因此我们建完图后,起始队列用给出的原材料即可。(正常是从入度为0的节点进入)
- 最后遍历到的节点检查他们是否是菜。
3. 代码实现
class Solution:
def findAllRecipes(self, recipes: List[str], ingredients: List[List[str]], supplies: List[str]) -> List[str]:
n = len(recipes)
indegree = defaultdict(int)
graph = defaultdict(list)
for i in range(n):
recipe = recipes[i]
indegree[recipe] += len(ingredients[i])
for ingredient in ingredients[i]:
graph[ingredient].append(recipe)
indegree[ingredient] += 0
q = deque(supplies)
visited =set(supplies)
while q:
u = q.popleft()
for v in graph[u]:
indegree[v] -= 1
if indegree[v] == 0:
visited.add(v)
q.append(v)
return [r for r in recipes if r in visited]
二、 剑指 Offer II 115. 重建序列
1. 题目描述
2. 思路分析
- 注意,这题节点数字是不重复的。
- 子序列的特点是,数的顺序在原序列里是不变的。
- 转化成依赖关系就是,a、b两个数,b的出现依赖a先出现。
- 因此ab可以建立一条a指向b的有向边。
- 对所有序列相邻数字建立有向边,然后进行拓扑排序,记录访问到的路径。
- 如果路径和nums相同,返回true。
- 特别的:本题要求唯一序列,因此任何时间发现q的长度>2,返回False。(因为q里只保存入度为0的点,有多个说明当前可以任选一个点走,即有不同路径)
3. 代码实现
class Solution:
def sequenceReconstruction(self, nums: List[int], sequences: List[List[int]]) -> bool:
n = len(nums)
g = defaultdict(list)
indegree = [0]*(n+1)
for seq in sequences:
for i in range(1,len(seq)):
g[seq[i-1]].append(seq[i])
indegree[seq[i]] += 1
visited = []
q = deque([i for i in range(1,n+1) if indegree[i] == 0])
while q:
if len(q) >= 2:
return False
u = q.popleft()
visited.append(u)
for v in g[u]:
indegree[v] -= 1
if indegree[v] == 0:
q.append(v)
return visited == nums
三、 1462. 课程表 IV
链接: 1462. 课程表 IV
1. 题目描述
2. 思路分析
- 拓扑排序+树上DP。
- 题目等价于求每个节点的父节点都有谁,因此我们可以给依赖关系建立有向边后,进行拓扑排序。
- 拓扑排序时,储存每个节点的所有长辈节点,最后对每个询问query(u,v),判断u是不是v的长辈即可。
- 复杂度:建图n+m,n是点数,m是边数;拓扑排序过程中,最坏情况图退化成链,每个节点的长辈都是前边所有点,存长辈的复杂度是n,总复杂度O(n(n+m))。
- floyd。
- 我们发现这题其实是询问有向图上任意两点的连通性,即对于每个询问query(u,v),其实是问是否存在u到v的一条路径。
- 询问图上任意两点连通、最短路径的问题,可以用floyd无脑算法解决,但这个算法是 O(n3) 的,一定要注意题目范围。
- 本题n<=100,非常合适。
- floyd算法一定要掌握,编码简单,思路暴力,数据范围小时非常有用!
- 初始化一个二维数组表示任意两点的距离或连通性。
- 然后三重循环暴力处理:
- 最外层是k,内两层是i,j,代表选择k来松弛i,j两个节点。
- 最短路写法通常是:dis[i][j] = min(dis[i][j],dis[i][k]+dis[k][j]). 这代表i到j如果从k走更短,那就从k走.
- 记得初始化dis数组后,要把图上的初始信息装进去
- 当然速度是慢的。
3. 代码实现
树上DP
class Solution:
def checkIfPrerequisite(self, numCourses: int, prerequisites: List[List[int]], queries: List[List[int]]) -> List[bool]:
graph = defaultdict(list)
indegree = [0] * numCourses
for a,b in prerequisites:
graph[a].append(b)
indegree[b] += 1
q = deque([i for i in range(numCourses) if indegree[i] == 0])
fathers = [set() for _ in range(numCourses)]
while q:
u = q.popleft()
for v in graph[u]:
fathers[v].add(u)
fathers[v].update(fathers[u])
indegree[v] -= 1
if indegree[v] == 0:
q.append(v)
return [ u in fathers[v] for u,v in queries]
floyd
class Solution:
def checkIfPrerequisite(self, numCourses: int, prerequisites: List[List[int]], queries: List[List[int]]) -> List[bool]:
dis = [[False]*numCourses for _ in range(numCourses)]
for a,b in prerequisites:
dis[a][a] = True
dis[b][b] = True
dis[a][b] = True
for k in range(numCourses):
for i in range(numCourses):
for j in range(numCourses):
if dis[i][k] and dis[k][j]:
dis[i][j] = True
return [ dis[u][v] for u,v in queries]
四、剑指 Offer II 114. 外星文字典
1. 题目描述
2. 思路分析
这题很气,上个月明明做过,而且是自己做的,今天看到竟然不会了。
不过看了自己的代码后还是重新做了一遍,代码有所精简。
-
我们用words中的顺序来建立有向图,对任意单词w1,w2(w1<w2),有:
-
去除相同前缀后,第一个不同的字符c1,c2,有c1<c2,建立有向边c1->c2;后续字符不需要处理,因为字典序先看第一个不同的字符。
-
特别地,如果相同前缀去除后,发现w2用完了,但w1还存在字符,说明这是个非法的情况,这时返回""。
-
建完图后,进行拓扑排序,用visited数组记录访问顺序。
-
如果有图上有环,说明是非法情况,那么拓扑排序将会遍历不完整个图。
-
就是visited长度会小于之前出现的所有字符总数,那么我们最开始处理words记录一个字符集即可。
-
一定注意如果入度数组的初始化,所有的字符都要初始化,有向边的两头都要初始化!
-
另外,看题解发现Python3.9之后加入了一个库用来拓扑排序,
from graphlib import TopologicalSorter
。 -
这个库的作用就是把点和边都加进去之后,生成拓扑序列,如果有环会给except,因此需要catch一下。
-
试了下,能过。想了下,前几道题都不太好用库,因为这库的作用目前就是生成拓扑序和判断环。
-
好处是减少编码,不用考虑入度细节。不过一定要注意把所有点都要手动加入!
3. 代码实现
class Solution:
def alienOrder(self, words: List[str]) -> str:
n = len(words)
cs = set(''.join(words))
graph = collections.defaultdict(list)
indegree = [0]*26
for word1, word2 in combinations(words,2):
l,r = len(word1),len(word2)
k = 0
# 规则1, 在第一个不同字母处,如果 s 中的字母在这门外星语言的字母顺序中位于 t 中字母之前,那么 s 的字典顺序小于 t 。
while k < min(l,r):
c1,c2 = word1[k],word2[k]
if c1 != c2:
graph[c1].append(c2)
indegree[ord(c2)-ord('a')] += 1
break
k += 1
# 规则2,如果前面 min(s.length, t.length) 字母都相同,那么 s.length < t.length 时,s 的字典顺序也小于 t 。
if k == r and l > r:
return ''
ans = ''
# 拓扑排序
q = deque([chr(ord('a')+i) for i,v in enumerate(indegree) if v==0 and chr(ord('a')+i) in cs])
visited = []
while q:
u = q.popleft() # 取出队列中的点
visited.append(u)
for v in graph[u]:
indegree[ord(v)-ord('a')] -= 1 # u遍历到的所有点v,入度-1
if indegree[ord(v)-ord('a')] ==0: # 如果v的入度变成0,则放入队列。
# visited.append(v)
q.append(v)
if len(visited) == len(cs):
return ''.join(visited)
return ''
from graphlib import TopologicalSorter
class Solution:
def alienOrder(self, words: List[str]) -> str:
n = len(words)
cs = set(''.join(words))
from graphlib import TopologicalSorter
ts = TopologicalSorter()
for c in cs:
ts.add(c)
for word1, word2 in combinations(words,2):
l,r = len(word1),len(word2)
k = 0
# 规则1, 在第一个不同字母处,如果 s 中的字母在这门外星语言的字母顺序中位于 t 中字母之前,那么 s 的字典顺序小于 t 。
while k < min(l,r):
c1,c2 = word1[k],word2[k]
if c1 != c2:
ts.add(c2, c1)
break
k += 1
# 规则2,如果前面 min(s.length, t.length) 字母都相同,那么 s.length < t.length 时,s 的字典顺序也小于 t 。
if k == r and l > r:
return ''
try:
return ''.join(ts.static_order())
except:
return ''