活动选择问题

概述

活动选择问题是贪心算法的经典入门问题:给定 个活动及其时间区间,选择最大的互相兼容的活动子集。该问题的贪心策略——选择最早结束的活动——具有直觉上的合理性:尽早释放资源以容纳更多活动。它是区间调度问题的核心,广泛应用于资源分配和任务调度场景。

定义

活动选择问题(Activity Selection)

输入 个活动 ,每个活动 有开始时间 和结束时间 (假设已按结束时间排序:)。

输出 的最大兼容子集 ,其中兼容意味着任意两个活动不重叠(即对于 ,要么 ,要么 )。

贪心策略:每次选择结束时间最早且与已选活动兼容的活动。

核心性质

1. 贪心选择性质

选择结束时间最早的活动 后,剩余子问题是在 中选择最大兼容子集。由 贪心选择性质 中的 Theorem 15.1 可知, 包含在某个最优解中。

2. 递归贪心算法

RECURSIVE-ACTIVITY-SELECTOR(s, f, k, n):
    m = k + 1
    while m <= n and s[m] < f[k]:   // 找到第一个与 a_k 兼容的活动
        m = m + 1
    if m <= n:
        A = {a_m} ∪ RECURSIVE-ACTIVITY-SELECTOR(s, f, m, n)
        return A
    else:
        return ∅

初始调用:RECURSIVE-ACTIVITY-SELECTOR(s, f, 0, n),其中 表示虚拟活动的结束时间。

3. 迭代贪心算法

GREEDY-ACTIVITY-SELECTOR(s, f):
    n = s.length
    A = {a_1}                        // 选择第一个活动(最早结束的)
    k = 1
    for m = 2 to n:
        if s[m] >= f[k]:             // a_m 与 a_k 兼容
            A = A ∪ {a_m}
            k = m
    return A

迭代版本更简洁高效,避免了递归调用的开销。

4. 时间复杂度分析

阶段时间复杂度
按结束时间排序
贪心选择(已排序)
总时间(未排序)或 (已排序)

5. 为什么选择”最早结束”而不是”最早开始”

  • 最早结束:正确。直觉:尽早释放资源,为后续活动留出更多空间
  • 最早开始:错误。反例:一个活动从时间0开始持续100个单位,而另一个从时间1开始持续2个单位。选最早开始会错过大量后续活动
  • 最短活动:错误。反例:短活动可能与多个稍长但不重叠的活动冲突
  • 最少冲突:错误。反例:一个活动虽然冲突少,但可能阻塞关键时间段

应用场景

  • 会议室调度:在有限会议室中安排最多的会议
  • CPU任务调度:在单处理器上调度不重叠的任务
  • 资源分配:在共享资源上安排最多的使用者
  • LeetCode 相关题目
    • LeetCode 435:Non-overlapping Intervals(求最少移除区间数)
    • LeetCode 253:Meeting Rooms II(求最少会议室数,变体问题)

章节扩展

参见