Leetcode 剑指 Offer II 115.序列重建

题目难度: 中等

原题链接

今天继续更新 Leetcode 的剑指 Offer(专项突击版)系列, 大家在公众号 算法精选 里回复 剑指offer2 就能看到该系列当前连载的所有文章了, 记得关注哦~

题目描述

给定一个长度为 n 的整数数组 nums ,其中 nums 是范围为 [1,n] 的整数的排列。还提供了一个 2D 整数数组 sequences ,其中 sequences[i] 是 nums 的子序列。 检查 nums 是否是唯一的最短 超序列 。最短 超序列 是 长度最短 的序列,并且所有序列 sequences[i] 都是它的子序列。对于给定的数组 sequences ,可能存在多个有效的 超序列 。

  • 例如,对于 sequences = [[1,2],[1,3]] ,有两个最短的 超序列 ,[1,2,3] 和 [1,3,2] 。
  • 而对于 sequences = [[1,2],[1,3],[1,2,3]] ,唯一可能的最短 超序列 是 [1,2,3] 。[1,2,3,4] 是可能的超序列,但不是最短的。

如果 nums 是序列的唯一最短 超序列 ,则返回 true ,否则返回 false 。 子序列 是一个可以通过从另一个序列中删除一些元素或不删除任何元素,而不改变其余元素的顺序的序列。

示例 1:

  • 输入:nums = [1,2,3], sequences = [[1,2],[1,3]]
  • 输出:false
  • 解释:有两种可能的超序列:[1,2,3]和[1,3,2]。
    • 序列 [1,2] 是[1,2,3]和[1,3,2]的子序列。
    • 序列 [1,3] 是[1,2,3]和[1,3,2]的子序列。
    • 因为 nums 不是唯一最短的超序列,所以返回 false。

示例 2:

  • 输入:nums = [1,2,3], sequences = [[1,2]]
  • 输出:false
  • 解释:最短可能的超序列为 [1,2]。
    • 序列 [1,2] 是它的子序列:[1,2]。
    • 因为 nums 不是最短的超序列,所以返回 false。

示例 3:

  • 输入:nums = [1,2,3], sequences = [[1,2],[1,3],[2,3]]
  • 输出:true
  • 解释:最短可能的超序列为[1,2,3]。
    • 序列 [1,2] 是它的一个子序列:[1,2,3]。
    • 序列 [1,3] 是它的一个子序列:[1,2,3]。
    • 序列 [2,3] 是它的一个子序列:[1,2,3]。
    • 因为 nums 是唯一最短的超序列,所以返回 true。

提示:

  • n == nums.length
  • 1 <= n <= 10^4
  • nums 是 [1, n] 范围内所有整数的排列
  • 1 <= sequences.length <= 10^4
  • 1 <= sequences[i].length <= 10^4
  • 1 <= sum(sequences[i].length) <= 10^5
  • 1 <= sequences[i][j] <= n
  • sequences 的所有数组都是 唯一 的
  • sequences[i] 是 nums 的一个子序列

题目思考

  1. 需要使用什么算法和数据结构?

解决方案

  • 分析题目, nums 要想是唯一的最短超序列, 需要满足三件事情: 1. 所有序列 sequences[i] 都是它的子序列; 2. 它是最短的超序列; 3. 它是唯一的
  • 其中对于第一点题目已经已经给出, 只需要判断后两点即可, 如何判断呢?
  • 我们可以逆向思考, 从给出的子序列 sequences 出发, 从它们开始构建超序列, 如果只能构建出一个, 且跟 nums 完全一致, 则说明 nums 是唯一的最短超序列
  • 既然每个 sequences 都给出了其数字顺序, 我们可以根据它们建立一个有向图, 并维护其入度, 例如子序列[1,2,3], 则1->2, 2->3, 1,2,3 的入度分别为 0,1,1
  • 等找到所有数字的指向关系和入度后, 我们就可以将问题转换成之前类似的拓扑排序的思路, 先加入顺序最小(入度为 0)的数字, 再加入顺序第二小的数字, 以此类推, 大家可以参考上上周文章Leetcode 剑指 Offer II 113.课程表 II的思考过程
  • 不过这里需要保证构造出的序列和 nums 完全一致且唯一, 所以我们在构造过程中, 需要额外维护当前 nums 下标且每次只能加入一个数字: 如果当前数字和 nums 不一致, 说明 nums 不是最短超序列; 如果有多个数字可以加入, 也说明 nums 不是唯一的最短超序列
  • 举个例子, 对于nums = [1,2,3], sequences = [[1,2],[1,3],[2,3]], 我们在遍历子序列后, 可以得到以下的大小关系:1->2,1->3, 2->3
    • 首先 1 的入度为 0, 所以先加入 1;
    • 然后 2 的入度变成 0, 所以加入 2;
    • 最后 3 的入度变成 0, 所以加入 3;
    • 其顺序和 nums 完全一致, 所以 nums 是唯一的最短超序列
  • 再举个例子, 对于nums = [1,2,3], sequences = [[1,2],[1,3],[3,2]], 我们在遍历子序列后, 可以得到以下的大小关系:1->2,1->3, 3->2
    • 首先 1 的入度为 0, 所以先加入 1;
    • 然后 3 的入度变成 0, 所以加入 3;
    • 最后 2 的入度变成 0, 所以加入 2;
    • 其顺序和 nums 不一致, 所以 nums 不是唯一的最短超序列
  • 再再举个例子, 对于nums = [1,2,3], sequences = [[1,2],[1,3]], 我们在遍历子序列后, 可以得到以下的大小关系:1->2,1->3
    • 首先 1 的入度为 0, 所以先加入 1;
    • 然后 2 和 3 的入度都变成 0, 所以既可以先加入 2, 也可以先加入 3;
    • 也就是说存在两个最短超序列[1,2,3][1,3,2], 所以 nums 不是唯一的最短超序列
  • 具体代码实现时, 分为以下四步:
    1. 首先遍历所有子序列, 根据其相邻位置的数字, 建立大小关系和入度, 这里使用集合避免重复添加相同的有向边
    2. 然后遍历 1~n 所有数字, 找出入度为 0 的数字, 加入队列中, 并维护当前 nums 下标, 初始化为 0
    3. 接下来遍历队列中当前可以处理的数字, 如果可用数字多于 1 个, 则说明超序列不唯一, 返回 false; 如果当前数字不等于 nums[i], 则说明 nums 不匹配, 也返回 false
    4. 然后利用大小关系找出这些数字指向的数字, 将它们入度减 1, 如果某个数字入度变成了 0, 则代表它的顺序确定了, 将其加入队列中, 并等待后续遍历处理
    5. 最后遍历结束时, 说明子序列构造出来的最短超序列和 nums 完全一致且唯一, 直接返回 true
  • 下面的代码对必要步骤有详细的解释, 方便大家理解

复杂度

  • 时间复杂度 O(N+M): nums 长度是 N, sequences 的所有子序列长度之和是 M, 构造图和拓扑排序时都需要 O(N+M)
  • 空间复杂度 O(N+M): maps 存 O(N+M), indegree 和 q 都存 O(N)

代码

class Solution:
    def sequenceReconstruction(self, nums: List[int], sequences: List[List[int]]) -> bool:
        ### 拓扑排序
        n = len(nums)
        maps = [set() for _ in range(n + 1)]
        indegree = [0] * (n + 1)
        for seq in sequences:
            for i in range(1, len(seq)):
                pre, cur = seq[i - 1], seq[i]
                if cur not in maps[pre]:
                    # 注意使用集合避免重复添加!!!
                    maps[pre].add(cur)
                    indegree[cur] += 1
        q = []
        for i in range(1, n + 1):
            if indegree[i] == 0:
                q.append(i)
        i = 0
        while q:
            if len(q) != 1:
                # 如果当前可以使用的节点不是1, 则说明有多个最短超序列, 不是唯一
                return False
            if i >= n or q[0] != nums[i]:
                # 如果当前节点不等于nums[i], 说明nums不是最短超序列
                return False
            cur = q.pop()
            i += 1
            for nex in maps[cur]:
                indegree[nex] -= 1
                if indegree[nex] == 0:
                    # nex的入度变成了0, 可以处理它了
                    q.append(nex)
        return True

大家可以在下面这些地方找到我~😊

我的 GitHub

我的 Leetcode

我的 CSDN

我的知乎专栏

我的头条号

我的牛客网博客

我的公众号: 算法精选, 欢迎大家扫码关注~😊

算法精选 - 微信扫一扫关注我

全部评论

相关推荐

bg双非本科,方向是嵌入式。这次秋招一共拿到了&nbsp;8&nbsp;个&nbsp;offer,最高年包&nbsp;40w,中间也有一段在海康的实习经历,还有几次国家级竞赛。写这篇不是想证明什么,只是想把自己走过的这条路,尽量讲清楚一点,给同样背景的人一个参考。一、我一开始也很迷茫刚决定走嵌入式的时候,其实并没有一个特别清晰的规划。网上的信息很零散,有人说一定要懂底层,有人说项目更重要,也有人建议直接转方向。很多时候都是在怀疑:1.自己这种背景到底有没有机会2.现在学的东西到底有没有用3.是不是已经开始晚了这些问题,我当时一个都没答案。二、现在回头看,我主要做对了这几件事第一,方向尽早确定,但不把自己锁死。我比较早就确定了嵌入式这个大方向,但具体做哪一块,是在项目、竞赛和实习中慢慢调整的,而不是一开始就给自己下结论。第二,用项目和竞赛去“证明能力”,而不是堆技术名词。我不会刻意追求学得多全面,而是确保自己参与的每个项目,都能讲清楚:我负责了什么、遇到了什么问题、最后是怎么解决的。第三,尽早接触真实的工程环境。在海康实习的那段时间,对我触动挺大的。我开始意识到,企业更看重的是代码结构、逻辑清晰度,以及你能不能把事情说清楚,而不只是会不会某个知识点。第四,把秋招当成一个需要长期迭代的过程。简历不是一次写完的,面试表现也不是一次就到位的。我会在每次面试后复盘哪些问题没答好,再针对性补。三、我踩过的一些坑现在看也挺典型的:1.一开始在底层细节上纠结太久,投入产出比不高2.做过项目,但前期不会总结,导致面试表达吃亏3.早期有点害怕面试,准备不充分就去投这些弯路走过之后,才慢慢找到节奏。四、给和我背景相似的人一点建议如果你也是双非,准备走嵌入式,我觉得有几件事挺重要的:1.不用等“准备得差不多了”再投2.项目一定要能讲清楚,而不是做完就算3.不要只盯着技术,多关注表达和逻辑很多时候,差的不是能力,而是呈现方式。五、写在最后这篇总结不是标准答案,只是我个人的一次复盘。后面我会陆续把自己在嵌入式学习、竞赛、实习和秋招中的一些真实经验拆开来讲,希望能对后来的人有点帮助。如果你正好也在这条路上,希望你能少走一点弯路。
x_y_z1:蹲个后续
点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务