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