文章目录
- 一、题目介绍
- 二、解题思路
-
- [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 贪心策略
-
排序策略
- 将所有活动按结束时间升序排序
- 结束时间相同时,开始时间不影响结果(可任意排序)
-
选择策略
- 初始化选择第一个活动(最早结束)
- 遍历后续活动:
- 若当前活动的开始时间 ≥ \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 流程图说明
-
数据读取阶段:
- 读取活动数量
n
- 循环读取
n
个活动的时间区间 - 存储在
ArrayList
中
- 读取活动数量
-
排序阶段:
- 使用自定义比较器
ActivityComparator
- 按结束时间升序排序(最早结束的在前)
- 使用自定义比较器
-
贪心选择阶段:
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,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 关键步骤说明
-
排序意义:
- 结束时间最早的活动优先被选择
- 为后续活动留下最大时间窗口
-
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 × 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);
}
}
关键优化点
-
结束时间排序
javaCollections.sort(activities, (a, b) -> a.endTime - b.endTime);
-
贪心选择
javaif (act.startTime >= lastEnd) { count++; lastEnd = act.endTime; }
为什么不用优先队列?
- 排序后只需一次线性遍历,复杂度 O ( n ) O(n) O(n)
- 优先队列 O ( n log n ) O(n \log n) O(nlogn) 的插入/删除反而增加开销
通过结束时间排序+贪心遍历,高效解决大规模区间调度问题