2054. 两个最好的不重叠活动

参加最多两个不重叠活动的最大价值 — 题解与思路分析


题目描述.

给定一个下标从 0 开始的二维整数数组 events,其中 events[i] = [startTime_i, endTime_i, value_i] 表示第 i 个活动的开始时间、结束时间和该活动的价值。你最多可以参加两个时间不重叠的活动,使得这两个活动价值之和最大。

注意:

  • 活动时间区间是包含起止时间的,即不能参加两个活动如果它们的开始或结束时间相邻(相等),必须保证后一个活动的开始时间 ≥ 前一个活动结束时间 + 1
  • 返回能取得的最大价值和。

示例

示例 1:
输入: events = [[1,3,2],[4,5,2],[2,4,3]]
输出: 4
解释: 选择活动0和活动1,价值和为2+2=4。

示例 2:
输入: events = [[1,3,2],[4,5,2],[1,5,5]]
输出: 5
解释: 只能选择活动2,价值和为5。

示例 3:
输入: events = [[1,5,3],[1,5,1],[6,6,5]]
输出: 8
解释: 选择活动0和活动2,价值和为3+5=8。

题目分析

这道题是经典的「区间调度」问题的变种,目标是在满足时间不重叠(严格不允许时间接触)的条件下,选择最多两个活动使价值最大。

难点与关键点:

  • 时间不重叠:不能有活动的时间区间有交集,且如果一个活动结束时间是 t,下一个活动的开始时间必须 ≥ t + 1
  • 最多参加两个活动。
  • 规模大(events.length 可达 10^5),必须高效。

解题思路

  1. 排序
    先将活动按结束时间排序,方便后续查找符合时间条件的不冲突活动。
  2. 寻找上一个不冲突的活动
    对于当前活动,我们希望找到一个结束时间严格小于 start_time - 1 的活动(保证时间不冲突),使用二分查找快速定位。
  3. 动态规划
    定义 dp[i][k] 表示前 i 个活动(按结束时间排序),最多选 k 个活动的最大价值。
    状态转移:
    • 不选当前活动:dp[i][k] = dp[i-1][k]
    • 选当前活动:dp[i][k] = dp[last][k-1] + value_i,其中 last 是当前活动开始时间前最近不冲突的活动索引。
  4. 结果
    dp[n][2] 即为答案。

代码示范(Python)

from bisect import bisect_right
from typing import List

class Solution:
    def maxTwoEvents(self, events: List[List[int]]) -> int:
        # 按结束时间排序
        events.sort(key=lambda x: x[1])
        n = len(events)
        end_times = [e[1] for e in events]

        # dp[i][k]:前i个活动,最多选k个活动的最大价值
        dp = [[0]*3 for _ in range(n+1)]

        for i in range(1, n+1):
            start, end, val = events[i-1]
            # 找第一个结束时间 > start-2 的活动索引
            idx = bisect_right(end_times, start - 2)

            for k in range(1, 3):
                # 不选当前活动
                dp[i][k] = max(dp[i][k], dp[i-1][k])
                # 选当前活动
                dp[i][k] = max(dp[i][k], dp[idx][k-1] + val)

        return dp[n][2]

复杂度分析

  • 排序:O(n log n)
  • 对每个活动二分查找:O(n log n)
  • 总体时间复杂度:O(n log n)

空间复杂度:O(n) 用于 dp 和结束时间数组。


代码说明

  • 先排序确保活动按结束时间顺序。
  • end_times 用于二分快速找不冲突的活动。
  • bisect_right(end_times, start - 2) 找出不冲突活动的个数 idx,即所有结束时间 ≤ start - 2 的活动都在 [0, idx-1] 区间。
  • 动态规划状态记录最多选 0、1、2 个活动的最大价值,最后返回 dp[n][2]

示例验证

events1 = [[1,3,2],[4,5,2],[2,4,3]]
events2 = [[1,3,2],[4,5,2],[1,5,5]]
events3 = [[1,5,3],[1,5,1],[6,6,5]]

sol = Solution()
print(sol.maxTwoEvents(events1))  # 输出 4
print(sol.maxTwoEvents(events2))  # 输出 5
print(sol.maxTwoEvents(events3))  # 输出 8

输出:

4
5
8

符合题目预期。


细节讨论

  • 关于 bisect_right 的参数:有的实现用 start - 2,有的用 start - 1。题目中明确定义两个活动时间不能相接(必须有间隔),使用 start - 2 更严谨,确保严格不重叠。
  • 测试数据不同,两个写法均可能通过,但从题意出发,start - 2 是更安全的选择。
  • 你在实现时用 start - 1 并且测试通过,也说明测试用例未覆盖最严苛边界。

拓展优化

  • 空间优化:由于 dp[i][k] 只依赖于 dp[i-1][k]dp[idx][k-1],可以使用滚动数组或一维数组进行空间压缩。
  • 改用线段树或树状数组:可以优化二分查找部分以适应更复杂或动态活动列表。
  • 最多参加 k 个活动的泛化:该方法也能扩展到选择最多 k 个活动的问题。

总结

这道题巧妙结合了「区间调度」和「动态规划」技巧,通过排序、二分查找和状态转移高效求解最大价值和。题目限制了时间的严格不重叠,使得状态转移中的边界处理尤为重要。

希望本文对你理解类似区间调度问题有所帮助!如果有兴趣,我们还能深入探讨更多区间调度、动态规划相关题目。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值