Leetcode1024. 视频拼接-贪心法


前言

“视频拼接”是一个关于区间重合的问题,不过不是合并区间重合部分,而是求得能覆盖指定长度的最少区间个数。出现了“最少”的字眼,我们就考虑贪心法和动态规划。其中的贪心法还可以引申到“跳跃游戏”上去。
解题方法参考官方题解


一、Leetcode1024 题目描述

题目链接

在这里插入图片描述
从题目中提炼出的数学问题就是,获得能完全覆盖[0,T]这一片段的最少小区间个数,小区间之间允许存在重叠。小区间就是输入的这些视频片段起止时间区间。

二、动态规划法

1.解题思路

在动态规划中,我们首先要确定状态是什么。

在这个问题中,可以令 dp[i] 表示将区间 [0,i] 覆盖所需的最少子区间的数量。由于我们希望子区间的数目尽可能少,因此可以将所有 dp[i] 的初始值设为一个大整数(方便状态转移时取最小值作为更新结果),并将 dp[0](即空区间)的初始值设为 0。因此dp长度是T+1。

接下来要确定状态转移方程,即dp[i] 如何由前面的状态获得。

我们通过遍历这些小区间来更新每个状态。对于dp[i],遍历小区间,设当前小区间是 [a,b],如果 i 的值在这个区间内,那么说明能覆盖 [0,a] 的小区间再加上当前的这个小区间,就一定能覆盖 [0,i] 这段。那么说明dp[i] = dp[a]+1。这里还要注意,我们要找最少区间数,因此不能这么草率地就确定值了。
遍历小区间过程中,可能有很多个小区间都满足上面的条件,因此我们取min(dp[i], dp[a]+1) 来不断更新dp[i],直到遍历了所有小区间。 我们对每个 i 值都重复上面的做法。

2.完整代码

代码如下(示例):

class Solution:
    def videoStitching(self, clips: List[List[int]], T: int) -> int:
        dp = [0]+[float('inf') for _ in range(T)]  # 定初值
        for i in range(1,T+1):
            for a, b in clips:
                if a<i<=b:   
                # i在当前区间内,写成 a<=i<=b 也可以。不过i==a时,下面的式子值一定取dp[i]
                    dp[i] = min(dp[a]+1, dp[i])  # 状态转移
        return dp[-1] if dp[-1]<float('inf') else -1

动规解法中由于有两重循环,因此时间复杂度稍高,是o(T×N),N是小区间个数。

三、贪心算法

1.解题思路

在选取小区间来覆盖片段时,对于所有左端点相同的小区间,其右端点越远越有利。这就是贪心的精髓

于是我们预处理所有的小区间,对于每一个位置 i,我们记录以其为左端点的小区间中最远的右端点位置,记为 maxn[i]。这个列表是关键。

(这里注意:maxn的长度是T,列表最后一个值代表在T-1位置,可以覆盖到的最远右端点。因为小区间中不存在 [i, i] 这种情况,所以我们可以不考虑位置T能覆盖到的最远右端点)

之后,我们枚举每一个位置,假设当枚举到位置 i 时,记左端点不大于 i 的所有子区间的最远右端点为 last。这样 last 就代表了当前能覆盖到的最远的右端点位置。

每次我们枚举到一个新位置,我们都用 maxn[i] 来更新 last。如果更新后 last==i,那么说明当前最远可以覆盖到的就是当前位置了。我们定义的maxn的长度是T,因此哪怕遍历到列表的最后一个位置,当 last==T-1时,也是无法覆盖住 T 这个位置。我们就无法完成目标。所以,不管遍历到哪个位置,只要出现 last==i,就说明无法覆盖,直接返回-1即可。


通过前面的步骤,我们就可以判断这些小区间是否能覆盖给定长度的片段了。 这个思想就可以直接用在跳跃游戏上(题目描述)。
在跳跃游戏中,我们可以根据输入计算每个位置上可以跳到的最远位置,这个结果就是列表maxn。之后重复上面的枚举操作,来判断是否可以跳到最后一个位置。其代码为:

class Solution:
    def canJump(self, nums: List[int]) -> bool:
        maxn = [0 for _ in range(len(nums)-1)]  # 注意maxn的长度,一定要使列表的最后位置,代表跳跃终点的前一个位置
        for i in range(len(nums)-1):
            maxn[i] = i+nums[i]
        last = 0
        for i in range(len(nums)-1):
            last = max(last, maxn[i])
            if i == last: return False
        return True

但是在本题中,我们还要计算出所需最少区间数,因此要继续算法:

用 ans 表示所需小区间个数。在枚举 i 时,我们还需要记录上一个被使用的子区间的结束位置为 pre,每次我们越过一个被使用的子区间,就说明我们要启用一个新子区间,这个新子区间的结束位置即为当前的 last(因为要取最远的右端点)。也就是说,每次我们遇到 i==pre,就说明我们用完了一个被使用的子区间。这种情况下我们让ans加 1,并更新 pre 即可。
pre要初始化为0,因为maxn[0]对应的起始区间是一定要要的。

2.完整代码

class Solution:
    def videoStitching(self, clips: List[List[int]], T: int) -> int:
        maxn = [0] * T
        last = ret = pre = 0
        # 获得以每个位置为左端点的最右右端点位置
        for a, b in clips:
            if a < T:
                maxn[a] = max(maxn[a], b)
        
        for i in range(T):
            last = max(last, maxn[i])
            if i == last:   # 无法覆盖
                return -1
            if i == pre:    # 需要开启一个新区间
                ret += 1
                pre = last
        
        return ret

由于只有两个一重循环,时间复杂度只有o(T+N)。


总结

这道题的贪心解法比较难想通,它的核心在于maxn和之后的循环。这个方法容易泛化到跳跃问题或其他问题上,只要能正确写出maxn就行。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值