【牛客刷题】活动安排

文章目录

  • 一、题目介绍
  • 二、解题思路
    • [2.1 核心问题](#2.1 核心问题)
    • [2.2 贪心策略](#2.2 贪心策略)
    • [2.3 正确性证明](#2.3 正确性证明)
  • 三、算法分析
    • [3.1 为什么按结束时间排序?](#3.1 为什么按结束时间排序?)
    • [3.2 复杂度分析](#3.2 复杂度分析)
    • [3.3 算法流程图解](#3.3 算法流程图解)
      • [3.3.1 流程图说明](#3.3.1 流程图说明)
      • [3.3.2 关键步骤说明](#3.3.2 关键步骤说明)
  • 四、模拟演练
  • 五、完整代码

一、题目介绍

题目描述

给定 n n n 个活动,每个活动的时间区间为 [ a i , b i ) [a_i, b_i) [ai,bi)(左闭右开)。要求选择尽可能多的活动,使得这些活动的时间区间互不重叠。

输入描述

  • 第一行:整数 n n n( 1 ≤ n ≤ 2 × 1 0 5 1 \leq n \leq 2 \times 10^5 1≤n≤2×105),表示活动数量
  • 后续 n n n 行:每行两个整数 a i , b i a_i, b_i ai,bi( 0 ≤ a i < b i ≤ 1 0 9 0 \leq a_i < b_i \leq 10^9 0≤ai<bi≤109)

输出描述

  • 一个整数,表示最多可选择的活动数

示例

  • 输入:

    复制代码
    3
    1 4
    1 3
    3 5
  • 输出:2

  • 说明:可选择活动 [ 1 , 3 ) [1,3) [1,3) 和 [ 3 , 5 ) [3,5) [3,5)

二、解题思路

2.1 核心问题

在多个时间区间中选出最大互斥子集------经典的区间调度问题

2.2 贪心策略

  1. 排序策略

    • 将所有活动按结束时间升序排序
    • 结束时间相同时,开始时间不影响结果(可任意排序)
  2. 选择策略

    • 初始化选择第一个活动(最早结束)
    • 遍历后续活动:
      • 若当前活动的开始时间 ≥ \geq ≥ 上一个选中活动的结束时间
      • 则选择该活动,并更新记录点

2.3 正确性证明

  • 贪心选择性:最早结束的活动一定在某个最优解中
  • 最优子结构:选择最早结束活动后,剩余问题仍是相同结构的子问题
  • 反证法:若存在更优解,其第一个活动结束时间一定不早于贪心选择的活动

三、算法分析

3.1 为什么按结束时间排序?

排序方式 反例 问题原因
按开始时间排序 [1,5] [2,3] [4,6] [1,5] 后无法选其他
按区间长度排序 [1,4] [2,3] [3,5] 选最短 [2,3] 后只能再选一个
按结束时间排序 无反例 保证最大化剩余时间

3.2 复杂度分析

  • 时间复杂度 : O ( n log ⁡ n ) O(n \log n) O(nlogn)
    • 排序: O ( n log ⁡ n ) O(n \log n) O(nlogn)(占主导)
    • 遍历: O ( n ) O(n) O(n)
  • 空间复杂度 : O ( n ) O(n) O(n)
    • 存储 n n n 个活动对象

3.3 算法流程图解

flowchart TD A[开始] --> B[读取活动数量n] B --> C[创建空活动列表] C --> D[循环读取n个活动] D --> E[存储活动到列表] E --> F{是否读完n个活动?} F -- 否 --> D F -- 是 --> G[按结束时间升序排序] G --> H[初始化:count=0, lastEnd=-1] H --> I[遍历排序后活动列表] I --> J{当前活动开始时间 ≥ lastEnd?} J -- 是 --> K[count++,更新lastEnd=当前结束时间] J -- 否 --> L[跳过该活动] K --> M{是否还有活动?} L --> M M -- 是 --> I M -- 否 --> N[输出count] N --> O[结束]

3.3.1 流程图说明

  1. 数据读取阶段

    • 读取活动数量 n
    • 循环读取 n 个活动的时间区间
    • 存储在 ArrayList
  2. 排序阶段

    • 使用自定义比较器 ActivityComparator
    • 按结束时间升序排序(最早结束的在前)
  3. 贪心选择阶段

    flowchart LR P[lastEnd初始值-1] --> Q{遍历活动} Q --> R[活动A: start≥lastEnd?] R -- 是 --> S[选择A, count+1, lastEnd=A.end] R -- 否 --> T[跳过A] S --> U{继续遍历} T --> U
  1. 选择逻辑示例 (输入 [[1,4], [1,3], [3,5]]):

    flowchart TB subgraph 排序后 A1[活动2: 1-3] --> A2[活动1: 1-4] --> A3[活动3: 3-5] end A1 --> B1{1 ≥ -1?} -- 是 --> C1[选择, count=1, lastEnd=3] C1 --> A2 A2 --> B2{1 ≥ 3?} -- 否 --> C2[跳过] C2 --> A3 A3 --> B3{3 ≥ 3?} -- 是 --> C3[选择, count=2, lastEnd=5]

3.3.2 关键步骤说明

  1. 排序意义

    • 结束时间最早的活动优先被选择
    • 为后续活动留下最大时间窗口
  2. lastEnd初始值-1的作用

    • 确保第一个活动总是被选择
    • 数学上满足:任意开始时间 ≥ -1
  3. 选择条件 start ≥ lastEnd

    • 严格保证活动时间不重叠
    • 充分利用左闭右开区间特性([1,3)[3,5) 可衔接)

此流程图清晰展示了贪心算法的核心思想:通过结束时间排序最大化剩余时间窗口,通过顺序遍历实现高效选择

四、模拟演练

输入数据

复制代码
3
1 4
1 3
3 5

执行流程

  1. 排序阶段(按结束时间升序):

    原始顺序 开始时间 结束时间
    活动1 1 4
    活动2 1 3
    活动3 3 5

    ↓ 排序后 ↓

    新顺序 开始时间 结束时间
    活动2 1 3
    活动1 1 4
    活动3 3 5
  2. 选择阶段

    当前活动 开始时间 结束时间 上一活动结束时间 是否选择 已选活动数 更新结束时间
    活动2 1 3 -1 (初始) 1 3
    活动1 1 4 3 ❌(1 < 3) 1 3
    活动3 3 5 3 ✅(3 ≥ 3) 2 5
  3. 输出结果:2

边界测试

  • 全重叠活动

    输入:[1,2), [1,2), [1,2)

    输出:1(只能选一个)

  • 大范围数据

    输入: 2 × 1 0 5 2 \times 10^5 2×105 个 [ i , i + 1 ) [i, i+1) [i,i+1) 区间

    输出: 2 × 1 0 5 2 \times 10^5 2×105(所有活动互不重叠)

五、完整代码

java 复制代码
import java.util.*;

/**
 * 活动类:表示一个活动的时间区间 [startTime, endTime)
 */
class Activity {
    int startTime;  // 活动开始时间
    int endTime;    // 活动结束时间(不包含)

    // 构造函数
    Activity(int startTime, int endTime) {
        this.startTime = startTime;
        this.endTime = endTime;
    }
}

/**
 * 活动比较器:按结束时间升序排序
 * 为什么按结束时间排序?因为结束时间决定了活动占用时间段的长度
 */
class ActivityComparator implements Comparator<Activity> {
    @Override
    public int compare(Activity a, Activity b) {
        // 按结束时间从小到大排序:最早结束的排前面
        return a.endTime - b.endTime;
    }
}

public class Main {
    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        // 1. 读取活动数量
        int n = in.nextInt();

        // 2. 创建活动列表并存储所有活动
        List<Activity> activities = new ArrayList<>();
        for (int i = 0; i < n; i++) {
            int startTime = in.nextInt();
            int endTime = in.nextInt();
            activities.add(new Activity(startTime, endTime));
        }

        // 3. 关键步骤:按结束时间升序排序(贪心算法的核心)
        Collections.sort(activities, new ActivityComparator());
        
        // 4. 贪心选择过程
        int count = 0;          // 记录可安排的活动数量
        int lastEndTime = -1;   // 上一个被选中活动的结束时间(初始化为-1,表示尚未选择任何活动)
        
        for (Activity activity : activities) {
            // 如果当前活动开始时间 ≥ 上一个活动的结束时间(说明时间不重叠)
            if (activity.startTime >= lastEndTime) {
                count++;  // 选择不重叠的活动
                lastEndTime = activity.endTime;  // 更新最后一个活动的结束时间
            }
        }
        
        // 5. 输出结果
        System.out.println(count);
    }
}

关键优化点

  1. 结束时间排序

    java 复制代码
    Collections.sort(activities, (a, b) -> a.endTime - b.endTime);
  2. 贪心选择

    java 复制代码
    if (act.startTime >= lastEnd) {
        count++;
        lastEnd = act.endTime;
    }

为什么不用优先队列?

  • 排序后只需一次线性遍历,复杂度 O ( n ) O(n) O(n)
  • 优先队列 O ( n log ⁡ n ) O(n \log n) O(nlogn) 的插入/删除反而增加开销

通过结束时间排序+贪心遍历,高效解决大规模区间调度问题

相关推荐
Honyee7 分钟前
java使用UCanAccess操作Access
java·后端
秋千码途7 分钟前
小架构step系列10:日志热更新
java·linux·微服务
她说人狗殊途11 分钟前
浅克隆 深克隆
java
緈福的街口11 分钟前
【leetcode】2236. 判断根节点是否等于子节点之和
算法·leetcode·职场和发展
timing99412 分钟前
SQLite3 中列(变量)的特殊属性
java·jvm·sqlite
SimonKing18 分钟前
你的Redis分布式锁还在裸奔?看门狗机制让锁更安全!
java·后端·程序员
祁思妙想27 分钟前
【LeetCode100】--- 1.两数之和【复习回滚】
数据结构·算法·leetcode
薰衣草233328 分钟前
一天两道力扣(2)
算法·leetcode
小鲈鱼-32 分钟前
【LeetCode4.寻找两个正序数组的中位数】二分O(log(m+n))
c++·算法
橘颂TA34 分钟前
【C++】红黑树的底层思想 and 大厂面试常问
数据结构·c++·算法·红黑树