题目来源:https://leetcode.cn/problems/maximum-profit-in-job-scheduling/
大致题意:
给三个数组,分别表示兼职工作的开始时间、结束时间和薪资,求在兼职时间不冲突的情况下能获得的最大薪资
思路
这类按照时间段求最多的不冲突时间段数量,或者给时间段加上权值的题目,一般都先按照结束时间排序,优先选择结束时间早的时间段。比如在 课程表III 中的证明
那么该题可以先将三个数组合并为一个二维数组,然后将二维数组按照结束时间排序
然后使用 dp[i] 表示从第 1 个工作到第 i 个工作,最多能赚多少钱,于是有
- 对于当前工作,考虑到选择当前工作可能会与之前的工作冲突,所以在不选择当前工作时,dp[i] = dp[i - 1]
- 如果要选择当前工作,那么需要找到之前最后一个未发生冲突的第 k 个工作,即第 k 个工作的结束时间小于等于当前工作开始时间,且 k 是满足条件的工作中离 i 最近的。那么有 dp[i] = dp[k] + 第 i 个工作的薪资
于是状态转移方程为:
- dp[i] = max(dp[i - 1], dp[k] + profit[i])
在具体实现时,可以通过二分法比较之前工作的结束时间与当前工作的开始时间,这样可以在 O(log n) 的时间复杂度找到满足条件的 k。二分的规则如下
- 假设当前遍历到第 i 个工作,那么二分的左边界为 0,右边界为 i,目标值为第 i 个工作的开始时间
- 每次取二分区间中间值对应工作的结束时间与目标值比较
- 如果当前结束时间大于目标值,表示当前区间右半部分的结束时间都大于目标值,更新右边界
- 如果当前结束时间小于等于目标值,表示当前区间左部分的结束时间都小于等于目标值,更新左边界
- 重复 2~4 步骤,直至左右边界相等
那么解题步骤可以概括为
- 将给定的三个数组合并为一个数组,并按照结束时间排序
- 使用数组 dp[i] 表示从第 1 个工作到第 i 个工作,最多能赚多少钱。初始时 dp[0] 为 0
- 遍历排序后的数组,通过二分法比较之前工作的结束时间与当前工作的开始时间找到满足条件的 k,按照状态转移方程更新 dp[i]
代码:
public int jobScheduling(int[] startTime, int[] endTime, int[] profit) {
int n = startTime.length;
int[][] nums = new int[n][3];
// 合并数组
for (int i = 0; i < n; i++) {
nums[i][0] = startTime[i];
nums[i][1] = endTime[i];
nums[i][2] = profit[i];
}
// 按照结束时间排序
Arrays.sort(nums, (a, b) -> a[1] - b[1]);
// 表示从第 1 个工作到第 i 个工作,最多能赚多少钱
int[] dp = new int[n + 1];
// 遍历数组
// 遍历索引从 1~n,对应工作数组的索引需要 -1
for (int i = 1; i <= n; i++) {
// 当前工作开始时间
int target = nums[i - 1][0];
// 初始化二分边界
int l = 0;
int r = i - 1;
while (l < r) {
int mid = (l + r) >> 1;
// 如果当前中值对应工作结束时间大于当前工作开始时间,更新右边界
if (nums[mid][1] > target) {
r = mid;
} else { // 如果当前中值对应工作结束时间小于等于当前工作开始时间,更新左边界
// 更新左边界时,会导致更新后的左边界对应工作结束时间大于当前工作开始时间
// 但是由于 dp 数组的索引 i 表示截至第 i-1 个工作,所以并不影响答案
l = mid + 1;
}
}
// 状态转移方程
dp[i] = Math.max(dp[i - 1], dp[l] + nums[i - 1][2]);
}
return dp[n];
}