[英雄星球六月集训LeetCode解题日报] 第30日 拓扑排序

日报

  • 今天的内容是拓扑排序,以前没接触过,第一次接触是上个月的今天。
  • 今天凭自己做了1、4题,2、3看的题解才做的。
  • 4题之前做过,是上个月的拓扑排序第二天,因此直接就做出来了。结果今天看到题完全不会做了。
  • 上次忘了写题解,这次正好补上。
  • 拓扑排序经常用来解决依赖关系的问题。
  • 若b依赖a,那么a将出现在拓扑排序后b的前边。

题目

一、 2115. 从给定原材料中找到所有可以做出的菜

链接: 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. 重建序列

链接: 剑指 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. 思路分析
  1. 拓扑排序+树上DP。
    • 题目等价于求每个节点的父节点都有谁,因此我们可以给依赖关系建立有向边后,进行拓扑排序。
    • 拓扑排序时,储存每个节点的所有长辈节点,最后对每个询问query(u,v),判断u是不是v的长辈即可。
    • 复杂度:建图n+m,n是点数,m是边数;拓扑排序过程中,最坏情况图退化成链,每个节点的长辈都是前边所有点,存长辈的复杂度是n,总复杂度O(n(n+m))
  2. 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. 外星文字典

链接: 剑指 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 ''

参考链接

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值