1235. 规划兼职工作
大约 4 分钟
---
1235. 规划兼职工作
- 标签:数组、二分查找、动态规划、排序
- 难度:困难
题目链接
题目大意
描述:给定 个兼职工作,每个工作有开始时间 、结束时间 和报酬 。
要求:计算最大能获得的报酬总和。每个工作必须在开始时间开始,在结束时间结束。你同一时间只能做一项工作,但可以按任意顺序安排工作(即工作不能重叠,结束时间和开始时间相同可以接续)。
说明:
- 。
- 。
- 。
示例:
- 示例 1:
输入:startTime = [1,2,3,3], endTime = [3,4,5,6], profit = [50,10,40,70]
输出:120
解释:选择工作 1(1→3,50)和工作 4(3→6,70),报酬总和为 50+70=120。- 示例 2:
输入:startTime = [1,2,3,4,6], endTime = [3,5,10,6,9], profit = [20,20,100,70,60]
输出:150
解释:选择工作 1(1→3,20)、工作 4(4→6,70)和工作 5(6→9,60),报酬总和为 20+70+60=150。解题思路
思路 1:动态规划 + 二分查找
1. 阶段划分
将 个工作按结束时间排序。排序后,按顺序依次考虑每个工作,决定选或不选。阶段就是排序后的工作下标 (从 到 )。
按结束时间排序的原因:在考虑第 个工作时,我们已经考虑完了所有结束时间早于它的工作,这样我们可以方便地找到与第 个工作不冲突的最后一个工作。
2. 定义状态
定义 表示考虑前 个工作(即排序后的前 个)时能获得的最大报酬。
3. 状态转移方程
对于第 个工作,有两种选择:
- 不选第 个工作:。
- 选第 个工作:先找到"上一个不冲突的工作"——即结束时间 的最后一个工作。假设这个工作的下标为 (如果存在),则选第 个工作能得到的报酬为 。
综合两种情况:
其中 是满足 的最大下标(使用二分查找在已排序的结束时间数组中查找)。
4. 初始条件
- (没有工作时报酬为 )。
- 为了方便,下标从 开始。
5. 最终结果
即为最大能获得的报酬总和。
6. 结合示例走一遍
按结束时间排序后(正好已经排好序):
| i | startTime | endTime | profit |
|---|---|---|---|
| 1 | 1 | 3 | 50 |
| 2 | 2 | 4 | 10 |
| 3 | 3 | 5 | 40 |
| 4 | 3 | 6 | 70 |
计算 :
- :,二分查找结束时间 的工作 → 不存在
- 不选:
- 选:
- :,二分查找结束时间 的工作 → 不存在
- 不选:
- 选:
- :,二分查找结束时间 的工作 → 工作 1(结束时间 ,)
- 不选:
- 选:
- :,二分查找结束时间 的工作 → 工作 1(结束时间 ,)
- 不选:
- 选:
结果为 。
思路 1:代码
class Solution:
def jobScheduling(self, startTime: List[int], endTime: List[int], profit: List[int]) -> int:
n = len(startTime)
# 将三个数组合并并按结束时间排序
jobs = sorted(zip(endTime, startTime, profit))
# 提取排序后的结束时间数组(用于二分查找)
end = [jobs[i][0] for i in range(n)]
# dp[i] 表示考虑前 i 个工作的最大报酬,dp[0]=0
dp = [0] * (n + 1)
for i in range(1, n + 1):
_, s, p = jobs[i - 1]
# 二分查找:结束时间 <= s 的最大下标
# 在 end[0..i-2] 中找最后一个 <= s 的位置
j = bisect_right(end, s, 0, i - 1) # 返回的是第一个 > s 的位置
# 不选 vs 选
dp[i] = max(dp[i - 1], dp[j] + p)
return dp[n]思路 1:复杂度分析
- 时间复杂度:,排序需要 ,每个状态转移使用二分查找需要 ,共 次,总时间 。
- 空间复杂度:,需要 的空间存储排序后的工作和 dp 数组。
思路 2:基于堆的贪心(选读)
另一种思路是用最小堆维护当前可安排的工作。按时间顺序处理每个工作,将已经结束的工作从堆中移除,但这种方法处理思路 1 中的"不重叠"约束比较绕,不如 DP + 二分查找直观。建议掌握思路 1,这是区间调度类问题的标准解法。