巧用「序列 DP + 二分」攻克经典 leetcode 难题
2023-10-23 13:36:38
如何规划兼职工作以获得最大收益:一种序列 DP 和二分查找的解法
在瞬息万变的劳动力市场中,兼职工作越来越受欢迎。作为一名精明的求职者,在众多兼职机会中规划出一份能最大化收益的兼职计划至关重要。这就是 LeetCode 1235 题「规划兼职工作」的精髓所在。让我们深入探讨这个难题的最佳解法,并为你提供一个清晰的步骤指南。
问题陈述
假设你有一系列兼职工作,每份工作都有一个开始时间和一个结束时间。一天只能安排一份工作,而且一旦开始就无法中断。你的目标是安排一份兼职计划,使其总收益最大化。
解法:序列 DP 与二分查找
要解决这个问题,我们将采用「序列 DP + 二分」这一经典算法组合。
序列 DP
序列 DP,即序列动态规划,是一种适用于解决「序列型」问题的动态规划变体。在这里,「序列型」问题是指状态转移依赖于序列中前一个或多个状态。
在我们的兼职规划问题中,我们可以定义状态 dp[i]
表示考虑前 i
个工作后的最大收益。
状态转移方程:
dp[i] = max(dp[i-1], dp[j] + p[i])
其中:
i
:当前考虑的工作编号dp[i-1]
:不考虑第i
个工作时的最大收益dp[j]
:考虑了前j
个工作时的最大收益,且第j
个工作与第i
个工作不冲突p[i]
:第i
个工作的收益
二分查找
在状态转移方程中,我们为了找到不与第 i
个工作冲突的最后一个工作 j
,需要遍历前 i-1
个工作。为了优化时间复杂度,我们可以使用二分查找。
二分查找的原理是:对于一个有序序列,每次查找将序列长度缩小一半,直到找到目标元素或确定目标元素不存在。在本题中,我们可以根据工作结束时间对前 i-1
个工作进行排序,然后通过二分查找找到第一个结束时间小于第 i
个工作开始时间的元素。
代码实现
def job_scheduling(jobs):
"""
:type jobs: List[List[int]]
:rtype: int
"""
# 按照结束时间对工作进行排序
jobs.sort(key=lambda x: x[1])
# 初始化 dp 数组
dp = [0] * len(jobs)
# 遍历所有工作
for i in range(1, len(jobs)):
# 找到第一个结束时间小于第 i 个工作开始时间的元素
j = bisect.bisect_left(jobs, [jobs[i][0], float('inf')], lo=0, hi=i) - 1
# 更新 dp 值
dp[i] = max(dp[i-1], dp[j] + jobs[i][2])
# 返回最大的收益
return dp[-1]
示例
让我们以以下兼职工作列表为例:
jobs = [[3, 5, 20], [1, 5, 10], [2, 6, 15], [4, 8, 12]]
应用我们的算法,我们可以安排如下兼职计划:
- 工作 1(5 美元)
- 工作 3(15 美元)
总收益: 20 美元
结论
通过巧妙结合序列 DP 和二分查找,我们开发了一个高效的算法来解决 LeetCode 1235 题「规划兼职工作」。这种算法技巧不仅适用于本题,还可以推广到其他类似的序列型问题中。
常见问题解答
-
为什么使用二分查找来查找不冲突的工作?
二分查找可以将查找时间复杂度从 O(n) 优化到 O(log n),其中 n 是工作数量。 -
序列 DP 的核心思想是什么?
序列 DP 的核心思想是将问题分解为一系列重叠子问题,然后自底向上求解这些子问题,并存储它们的结果。 -
除了兼职规划,序列 DP 还可以在哪些场景中使用?
序列 DP 可用于解决各种问题,例如最长公共子序列、最长递增子序列、背包问题等。 -
二分查找的适用场景有哪些?
二分查找适用于查找有序序列中的目标元素或满足特定条件的元素。 -
如何提高我解决算法问题的技能?
多练习、理解算法的基本原理以及向经验丰富的程序员寻求指导,都可以提高你的算法解决能力。