力扣1235:加权区间调度最优解

问题解构 :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;
        }
    }
}

关键步骤与优化解释

  1. 排序 :按结束时间升序排序是动态规划正确性的基础。它确保了当我们处理 jobs[i] 时,所有可能在其之前结束(即可能不冲突)的工作 jobs[0...i-1] 都已经被考虑过,并且它们的 dp 值已经计算完成 。
  2. 状态定义与转移
    • 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开始)。
  3. 二分查找的作用 :在已排序的数组中,快速定位 endTime <= currentStart 的最后一个位置。这步将寻找不冲突前驱工作的时间复杂度从 O(n) 降低到 O(log n),是整个算法达到 O(n log n) 的关键 。
  4. 初始化与答案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]=20dp[2]=40
  • i=3 (工作3): 选,找 end <= 4 的工作,找到工作2 (k=1),收益= 70+dp[2]=110;不选,收益=dp[2]=40dp[3]=110
  • i=4 (工作4): 选,找 end <= 6 的工作,找到工作3 (k=2),收益= 60+dp[3]=170;不选,收益=dp[3]=110dp[4]=170
  • i=5 (工作5): 选,找 end <= 3 的工作,找到工作1 (k=0),收益= 100+dp[1]=120;不选,收益=dp[4]=170dp[5]=170
    最终最大收益为 dp[5] = 170,对应选择工作1、工作3和工作4(或工作2、工作3和工作4)。

参考来源

相关推荐
想不到ID了1 小时前
第八篇: 登录注册功能实现
java·javascript
耶叶1 小时前
餐厅出入最少人数问题:贪心算法
算法·贪心算法
gihigo19981 小时前
基于小波框架与稀疏表示的SAR图像目标识别系统(MATLAB实现)
算法
郑洁文1 小时前
基于Python的网络入侵检测系统
网络·python·php
AIMath~1 小时前
python中的uv命令揭秘
开发语言·python·uv
码语智行2 小时前
shp文件生成
java
弹简特2 小时前
【零基础学Python】06-Python模块和包、异常处理、文件常用操作
开发语言·python
plainGeekDev2 小时前
AlertDialog → DialogFragment
android·java·kotlin
吴可可1232 小时前
CAD2004自定义实体开发环境配置
c++·算法