返回

巧用「序列 DP + 二分」攻克经典 leetcode 难题

后端

如何规划兼职工作以获得最大收益:一种序列 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 题「规划兼职工作」。这种算法技巧不仅适用于本题,还可以推广到其他类似的序列型问题中。

常见问题解答

  1. 为什么使用二分查找来查找不冲突的工作?
    二分查找可以将查找时间复杂度从 O(n) 优化到 O(log n),其中 n 是工作数量。

  2. 序列 DP 的核心思想是什么?
    序列 DP 的核心思想是将问题分解为一系列重叠子问题,然后自底向上求解这些子问题,并存储它们的结果。

  3. 除了兼职规划,序列 DP 还可以在哪些场景中使用?
    序列 DP 可用于解决各种问题,例如最长公共子序列、最长递增子序列、背包问题等。

  4. 二分查找的适用场景有哪些?
    二分查找适用于查找有序序列中的目标元素或满足特定条件的元素。

  5. 如何提高我解决算法问题的技能?
    多练习、理解算法的基本原理以及向经验丰富的程序员寻求指导,都可以提高你的算法解决能力。