LeetCode 1235. 规划兼职工作【排序+动规+二分】2022

本文介绍了一种使用动态规划和二分查找解决LeetCode上的最大兼职收益问题的方法。通过将工作按照结束时间排序,计算并选择可以获得的最大累计报酬,同时避免时间冲突。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

本文属于「征服LeetCode」系列文章之一,这一系列正式开始于2021/08/12。由于LeetCode上部分题目有锁,本系列将至少持续到刷完所有无锁题之日为止;由于LeetCode还在不断地创建新题,本系列的终止日期可能是永远。在这一系列刷题文章中,我不仅会讲解多种解题思路及其优化,还会用多种编程语言实现题解,涉及到通用解法时更将归纳总结出相应的算法模板。

为了方便在PC上运行调试、分享代码文件,我还建立了相关的仓库:https://github.com/memcpy0/LeetCode-Conquest。在这一仓库中,你不仅可以看到LeetCode原题链接、题解代码、题解文章链接、同类题目归纳、通用解法总结等,还可以看到原题出现频率和相关企业等重要信息。如果有其他优选题解,还可以一同分享给他人。

由于本系列文章的内容随时可能发生更新变动,欢迎关注和收藏征服LeetCode系列文章目录一文以作备忘。

你打算利用空闲时间来做兼职工作赚些零花钱。

这里有 n 份兼职工作,每份工作预计从 startTime[i] 开始到 endTime[i] 结束,报酬为 profit[i]

给你一份兼职工作表,包含开始时间 startTime,结束时间 endTime 和预计报酬 profit 三个数组,请你计算并返回可以获得的最大报酬。

注意,时间上出现重叠的 2 份工作不能同时进行。

如果你选择的工作在时间 X 结束,那么你可以立刻进行在时间 X 开始的下一份工作。

示例 1:

输入:startTime = [1,2,3,3], endTime = [3,4,5,6], profit = [50,10,40,70]
输出:120
解释:
我们选出第 1 份和第 4 份工作, 
时间范围是 [1-3]+[3-6],共获得报酬 120 = 50 + 70

示例 2:

输入:startTime = [1,2,3,4,6], endTime = [3,5,10,6,9], profit = [20,20,100,70,60]
输出:150
解释:
我们选择第 145 份工作。 
共获得报酬 150 = 20 + 70 + 60

示例 3:

输入:startTime = [1,1,1], endTime = [2,3,4], profit = [5,6,4]
输出:6

提示:

  • 1 <= startTime.length == endTime.length == profit.length <= 5 * 10^4
  • 1 <= startTime[i] < endTime[i] <= 10^9
  • 1 <= profit[i] <= 10^4

相似题目

解法 排序+动态规划+二分优化

将工作按照结束时间排序,以示例 2 为例,得到下图:

手动计算一下,按照结束时间排序后:

  1. 1 1 1 个工作的最大报酬为 20 20 20
  2. 2 2 2 个工作的最大报酬为 20 20 20
  3. 3 3 3 个工作的最大报酬为前 1 1 1 个工作的最大报酬 + 70 = 20 + 70 = 90 +70=20+70=90 +70=20+70=90
  4. 4 4 4 个工作的最大报酬为前 3 3 3 个工作的最大报酬 + 60 = 90 + 60 = 150 +60=90+60=150 +60=90+60=150
  5. 5 5 5 个工作的最大报酬,如果选了第 5 5 5 个工作,那么报酬为前 1 1 1 个工作的最大报酬 + 100 = 20 + 100 = 120 +100=20+100=120 +100=20+100=120 ;但也可以不选第 5 5 5 个工作,报酬为前 4 4 4 个工作的最大报酬,即 150 150 150 。由于 150 > 120 150>120 150>120 ,不选第 5 5 5 个工作更好,因此前 5 5 5 个工作的最大报酬为 150 150 150

示例 2 等价于计算前 5 5 5 个工作的最大报酬,即 150 150 150

总结一下,我们可以分类讨论,求出按照结束时间排序后的前 i i i 个工作的最大报酬

  • 不选第 i i i 个工作,那么最大报酬等于前 i − 1 i−1 i1 个工作的最大报酬(转换成了一个规模更小的子问题);
  • 选第 i i i 个工作,由于工作时间不能重叠,设 j j j 是最大的满足 e n d T i m e [ j ] ≤ s t a r t T i m e [ i ] endTime[j]≤startTime[i] endTime[j]startTime[i] j j j ,那么最大报酬等于前 j j j 个工作的最大报酬加上 p r o f i t [ i ] profit[i] profit[i](同样转换成了一个规模更小的子问题);

这两种决策取最大值。

注意,由于按照结束时间排序,前 j j j 个工作中的任意一个都不会与第 i i i 个工作的时间重叠。这是排序的作用其一;其二是为了用二分优化查找。

怎么实现?上述思路是一个标准的关于递推的描述,定义 f [ i ] f[i] f[i] 表示按照结束时间排序后的前 i i i 个工作的最大报酬,用「选或不选」分类讨论:

  • 不选第 i i i 个工作: f [ i ] = f [ i − 1 ] f[i]=f[i−1] f[i]=f[i1]
  • 选第 i i i 个工作: f [ i ] = f [ j ] + p r o f i t [ i ] f[i]=f[j]+profit[i] f[i]=f[j]+profit[i] ,其中 j j j 是最大的满足 e n d T i m e [ j ] ≤ s t a r t T i m e [ i ] endTime[j]≤startTime[i] endTime[j]startTime[i] j j j ,不存在时为 − 1 -1 1

两者取最大值,即
f [ i ] = max ⁡ ( f [ i − 1 ] , f [ j ] + profit [ i ] ) f[i] = \max(f[i-1], f[j]+\textit{profit}[i]) f[i]=max(f[i1],f[j]+profit[i])
由于 i = 0 i=0 i=0 时会产生 − 1 -1 1 ,可以在 f f f 数组前面插入一个 0 0 0 ,与 f f f 有关的下标都 + 1 +1 +1 ,即
f [ i + 1 ] = max ⁡ ( f [ i ] , f [ j + 1 ] + profit [ i ] ) f[i+1] = \max(f[i], f[j+1]+\textit{profit}[i]) f[i+1]=max(f[i],f[j+1]+profit[i])
初始项 f [ 0 ] = 0 f[0]=0 f[0]=0 ,答案为 f [ n ] f[n] f[n]

代码实现时,由于结束时间是有序的, j j j 可以用二分查找计算出来。

class Solution:
    def jobScheduling(self, startTime: List[int], endTime: List[int], profit: List[int]) -> int:
        jobs = sorted(zip(endTime, startTime, profit)) # 按照结束时间排序
        f = [0] * (len(jobs) + 1)
        for i, (_, st, p) in enumerate(jobs):
            j = bisect_left(jobs, (st + 1, ), hi = i) # hi=i表示二分上界为i(默认为n)
            # 状态转移中,为什么是 j 不是 j+1:上面算的是 > st,-1 后得到 <= st,但由于还要 +1,抵消了
            f[i + 1] = max(f[i], f[j] + p)
        return f[-1]
class Solution {
    public int jobScheduling(int[] startTime, int[] endTime, int[] profit) {
        int n = startTime.length;
        int[][] jobs = new int[n][];
        for (int i = 0; i < n; ++i) 
            jobs[i] = new int[]{ startTime[i], endTime[i], profit[i] };
        Arrays.sort(jobs, (a, b) -> a[1] - b[1]); // 按照结束时间排序

        int[] f = new int[n + 1];
        for (int i = 0; i < n; ++i) {
            int j = search(jobs, i, jobs[i][0]);
            f[i + 1] = Math.max(f[i], f[j + 1] + jobs[i][2]);
        }
        return f[n];
    }
    // 返回 endTime <= upper 的最大下标
    private int search(int[][] jobs, int right, int upper) {
        int left = -1;
        while (left + 1 < right) { // (left,right)
            int mid = (left + right) >>> 1;
            if (jobs[mid][1] <= upper) left = mid;
            else right = mid;
        }
        return left;
    }
}
class Solution {
public:
    int jobScheduling(vector<int>& startTime, vector<int>& endTime, vector<int>& profit) {
        int n = startTime.size();
        vector<array<int, 3>> jobs(n);
        for (int i = 0; i < n; ++i) jobs[i] = { endTime[i], startTime[i], profit[i] };
        ranges::sort(jobs, [](auto &a, auto &b) { return a[0] < b[0]; }); // 按照结束时间排序

        vector<int> f(n + 1);
        for (int i = 0; i < n; ++i) {
            int j = upper_bound(jobs.begin(), jobs.begin() + i, 
                array<int, 3>{ jobs[i][1], INT_MAX }) - jobs.begin();            
            // 状态转移中,为什么是 j 不是 j+1:上面算的是 > 开始时间,-1 后得到 <= 开始时间,但由于还要 +1,抵消了
            f[i + 1] = max(f[i], f[j] + jobs[i][2]);
        }
        return f[n];
    }
};

复杂度分析:

  • 时间复杂度: O ( n log ⁡ n ) \mathcal{O}(n\log n) O(nlogn) ,其中 n n n startTime \textit{startTime} startTime 的长度。排序的时间复杂度为 O ( n log ⁡ n ) \mathcal{O}(n\log n) O(nlogn) ,动态规划部分一共计算了 n n n 次二分,时间复杂度为 O ( n log ⁡ n ) \mathcal{O}(n\log n) O(nlogn) ,因此总的时间复杂度为 O ( n log ⁡ n ) \mathcal{O}(n\log n) O(nlogn)
  • 空间复杂度: O ( n ) \mathcal{O}(n) O(n)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

memcpy0

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值