【牛客刷题】活动安排
文章目录
- 一、题目介绍
- 二、解题思路
- 2.1 核心问题
- 2.2 贪心策略
- 2.3 正确性证明
- 三、算法分析
- 3.1 为什么按结束时间排序?
- 3.2 复杂度分析
- 3.3 算法流程图解
- 3.3.1 流程图说明
- 3.3.2 关键步骤说明
- 四、模拟演练
- 五、完整代码
一、题目介绍
- 活动安排
题目描述
给定 nnn 个活动,每个活动的时间区间为 [ai,bi)[a_i, b_i)[ai,bi)(左闭右开)。要求选择尽可能多的活动,使得这些活动的时间区间互不重叠。
输入描述
- 第一行:整数 nnn(1≤n≤2×1051 \leq n \leq 2 \times 10^51≤n≤2×105),表示活动数量
- 后续 nnn 行:每行两个整数 ai,bia_i, b_iai,bi(0≤ai<bi≤1090 \leq a_i < b_i \leq 10^90≤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 贪心策略
-
排序策略
- 将所有活动按结束时间升序排序
- 结束时间相同时,开始时间不影响结果(可任意排序)
-
选择策略
- 初始化选择第一个活动(最早结束)
- 遍历后续活动:
- 若当前活动的开始时间 ≥\geq≥ 上一个选中活动的结束时间
- 则选择该活动,并更新记录点
2.3 正确性证明
- 贪心选择性:最早结束的活动一定在某个最优解中
- 最优子结构:选择最早结束活动后,剩余问题仍是相同结构的子问题
- 反证法:若存在更优解,其第一个活动结束时间一定不早于贪心选择的活动
三、算法分析
3.1 为什么按结束时间排序?
排序方式 | 反例 | 问题原因 |
---|---|---|
按开始时间排序 | [1,5] [2,3] [4,6] | 选 [1,5] 后无法选其他 |
按区间长度排序 | [1,4] [2,3] [3,5] | 选最短 [2,3] 后只能再选一个 |
按结束时间排序 | 无反例 | 保证最大化剩余时间 |
3.2 复杂度分析
- 时间复杂度:O(nlogn)O(n \log n)O(nlogn)
- 排序:O(nlogn)O(n \log n)O(nlogn)(占主导)
- 遍历:O(n)O(n)O(n)
- 空间复杂度:O(n)O(n)O(n)
- 存储 nnn 个活动对象
3.3 算法流程图解
flowchart TDA[开始] --> B[读取活动数量n]B --> C[创建空活动列表]C --> D[循环读取n个活动]D --> E[存储活动到列表]E --> F{是否读完n个活动?}F -- 否 --> DF -- 是 --> G[按结束时间升序排序]G --> H[初始化:count=0, lastEnd=-1]H --> I[遍历排序后活动列表]I --> J{当前活动开始时间 ≥ lastEnd?}J -- 是 --> K[count++,更新lastEnd=当前结束时间]J -- 否 --> L[跳过该活动]K --> M{是否还有活动?}L --> MM -- 是 --> IM -- 否 --> N[输出count]N --> O[结束]
3.3.1 流程图说明
-
数据读取阶段:
- 读取活动数量
n
- 循环读取
n
个活动的时间区间 - 存储在
ArrayList
中
- 读取活动数量
-
排序阶段:
- 使用自定义比较器
ActivityComparator
- 按结束时间升序排序(最早结束的在前)
- 使用自定义比较器
-
贪心选择阶段:
flowchart LRP[lastEnd初始值-1] --> Q{遍历活动}Q --> R[活动A: start≥lastEnd?]R -- 是 --> S[选择A, count+1, lastEnd=A.end]R -- 否 --> T[跳过A]S --> U{继续遍历}T --> U
- 选择逻辑示例(输入
[[1,4], [1,3], [3,5]]
):flowchart TBsubgraph 排序后A1[活动2: 1-3] --> A2[活动1: 1-4] --> A3[活动3: 3-5]endA1 --> B1{1 ≥ -1?} -- 是 --> C1[选择, count=1, lastEnd=3]C1 --> A2A2 --> B2{1 ≥ 3?} -- 否 --> C2[跳过]C2 --> A3A3 --> B3{3 ≥ 3?} -- 是 --> C3[选择, count=2, lastEnd=5]
3.3.2 关键步骤说明
-
排序意义:
- 结束时间最早的活动优先被选择
- 为后续活动留下最大时间窗口
-
lastEnd初始值-1的作用:
- 确保第一个活动总是被选择
- 数学上满足:任意开始时间 ≥ -1
-
选择条件
start ≥ lastEnd
:- 严格保证活动时间不重叠
- 充分利用左闭右开区间特性(
[1,3)
和[3,5)
可衔接)
此流程图清晰展示了贪心算法的核心思想:通过结束时间排序最大化剩余时间窗口,通过顺序遍历实现高效选择。
四、模拟演练
输入数据
3
1 4
1 3
3 5
执行流程
-
排序阶段(按结束时间升序):
原始顺序 开始时间 结束时间 活动1 1 4 活动2 1 3 活动3 3 5 ↓ 排序后 ↓
新顺序 开始时间 结束时间 活动2 1 3 活动1 1 4 活动3 3 5 -
选择阶段:
当前活动 开始时间 结束时间 上一活动结束时间 是否选择 已选活动数 更新结束时间 活动2 1 3 -1 (初始) ✅ 1 3 活动1 1 4 3 ❌(1 < 3) 1 3 活动3 3 5 3 ✅(3 ≥ 3) 2 5 -
输出结果:2
边界测试
-
全重叠活动:
输入:[1,2), [1,2), [1,2)
输出:1(只能选一个) -
大范围数据:
输入:2×1052 \times 10^52×105 个 [i,i+1)[i, i+1)[i,i+1) 区间
输出:2×1052 \times 10^52×105(所有活动互不重叠)
五、完整代码
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> {@Overridepublic 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);}
}
关键优化点
- 结束时间排序
Collections.sort(activities, (a, b) -> a.endTime - b.endTime);
- 贪心选择
if (act.startTime >= lastEnd) {count++;lastEnd = act.endTime; }
为什么不用优先队列?
- 排序后只需一次线性遍历,复杂度 O(n)O(n)O(n)
- 优先队列 O(nlogn)O(n \log n)O(nlogn) 的插入/删除反而增加开销
通过结束时间排序+贪心遍历,高效解决大规模区间调度问题