问题解构 :LeetCode 1235 "规划兼职工作"是一个典型的加权区间调度问题 (Weighted Interval Scheduling)。给定一系列兼职工作 [startTime[i], endTime[i], profit[i]],目标是选择一组时间上互不冲突 的工作,使得总收益最大化。其核心挑战在于如何在 O(n log n) 时间复杂度内高效求解,而非暴力枚举所有子集(O(2^n))。
方案推演 :标准解法结合动态规划(DP) 与二分查找 。首先将所有工作按结束时间 升序排序,确保在考虑每个工作时,其之前的所有可能不冲突的工作都已处理完毕。定义 dp[i] 为考虑前 i 个工作(按排序后顺序)时能获得的最大收益。对于第 i 个工作,有两种选择:不选它,则收益为 dp[i-1];选它,则收益为 profit[i] + dp[k],其中 k 是结束时间小于等于 startTime[i] 的最后一个工作的索引(即不冲突的前一个工作)。寻找 k 的过程可用二分查找加速。最终答案为 dp[n]。
以下是具体的 Java 代码实现,包含详细注释。
java
import java.util.Arrays;
class Solution {
public int jobScheduling(int[] startTime, int[] endTime, int[] profit) {
int n = startTime.length;
// 1. 将每个工作的信息封装成一个 Job 对象,便于排序
Job[] jobs = new Job[n];
for (int i = 0; i < n; i++) {
jobs[i] = new Job(startTime[i], endTime[i], profit[i]);
}
// 2. 按结束时间升序排序
Arrays.sort(jobs, (a, b) -> a.end - b.end);
// 3. 初始化动态规划数组,dp[i] 表示考虑前 i 个工作的最大收益
int[] dp = new int[n + 1];
// dp[0] = 0,表示没有工作可选时收益为 0
for (int i = 1; i <= n; i++) {
// 当前考虑的工作索引为 i-1(因为数组从0开始)
int currentStart = jobs[i - 1].start;
int currentProfit = jobs[i - 1].profit;
// 选项1:不选当前工作,收益等于前 i-1 个工作的最大收益
int profitWithoutCurrent = dp[i - 1];
// 选项2:选当前工作,需要找到结束时间 <= currentStart 的最后一个工作
// 通过二分查找在已排序的 jobs[0...i-2] 中寻找
int k = binarySearch(jobs, i - 1, currentStart);
int profitWithCurrent = currentProfit + dp[k + 1]; // dp索引从1开始,所以 k+1
// 取两种选择中的较大值
dp[i] = Math.max(profitWithoutCurrent, profitWithCurrent);
}
return dp[n];
}
// 二分查找:在 jobs[0...right] 中找到结束时间 <= targetStart 的最后一个工作的索引
// 返回的是索引值(从0开始),如果没找到,返回 -1
private int binarySearch(Job[] jobs, int right, int targetStart) {
int left = 0;
int result = -1; // 初始化为-1,表示没找到
while (left <= right) {
int mid = left + (right - left) / 2;
if (jobs[mid].end <= targetStart) {
// 如果 mid 满足条件,记录位置,并继续在右侧查找可能更靠后的
result = mid;
left = mid + 1;
} else {
right = mid - 1;
}
}
return result;
}
// 内部类,用于封装工作信息
class Job {
int start;
int end;
int profit;
public Job(int start, int end, int profit) {
this.start = start;
this.end = end;
this.profit = profit;
}
}
}
关键步骤与优化解释:
- 排序 :按结束时间升序排序是动态规划正确性的基础。它确保了当我们处理
jobs[i]时,所有可能在其之前结束(即可能不冲突)的工作jobs[0...i-1]都已经被考虑过,并且它们的dp值已经计算完成 。 - 状态定义与转移 :
dp[i]:考虑排序后的前i个工作(即jobs[0...i-1])时能获得的最大收益。- 转移方程 :
dp[i] = max(dp[i-1], profit[i-1] + dp[k+1])。其中k是通过二分查找找到的、与第i-1个工作不冲突的最后一个工作的索引。dp[k+1]即表示考虑前k+1个工作的最大收益(因为dp索引从1开始)。
- 二分查找的作用 :在已排序的数组中,快速定位
endTime <= currentStart的最后一个位置。这步将寻找不冲突前驱工作的时间复杂度从O(n)降低到O(log n),是整个算法达到O(n log n)的关键 。 - 初始化与答案 :
dp[0]默认为 0。最终dp[n]即为考虑所有工作后的最大收益。
复杂度分析:
- 时间复杂度 :
O(n log n)。排序O(n log n),动态规划循环n次,每次二分查找O(log n)。 - 空间复杂度 :
O(n)。用于存储jobs数组和dp数组。
举例说明 :
假设输入为:
startTime = [1,2,3,4,6]
endTime = [3,5,10,6,9]
profit = [20,20,100,70,60]
排序后工作为(按end排序):
1: (1,3,20)
2: (2,5,20)
3: (4,6,70)
4: (6,9,60)
5: (3,10,100)
动态规划过程:
i=1(工作1): 选,收益20;不选,收益0。dp[1]=20。i=2(工作2): 选,需找end <= 2的工作,找到工作1 (k=0),收益=20+dp[1]=40;不选,收益=dp[1]=20。dp[2]=40。i=3(工作3): 选,找end <= 4的工作,找到工作2 (k=1),收益=70+dp[2]=110;不选,收益=dp[2]=40。dp[3]=110。i=4(工作4): 选,找end <= 6的工作,找到工作3 (k=2),收益=60+dp[3]=170;不选,收益=dp[3]=110。dp[4]=170。i=5(工作5): 选,找end <= 3的工作,找到工作1 (k=0),收益=100+dp[1]=120;不选,收益=dp[4]=170。dp[5]=170。
最终最大收益为dp[5] = 170,对应选择工作1、工作3和工作4(或工作2、工作3和工作4)。