问题描述
假设有 n 个活动,每个活动都有开始时间和结束时间。我们的目标是选择尽可能多的活动,使得它们在时间上不冲突(即任意两个活动的时间区间不重叠)。
这是一个经典的贪心算法应用场景,也被称为"区间调度问题"。
解题思路
核心思想
在每一步都做出当前看起来最好的选择,最终得到全局最优解。
贪心策略选择
关键问题:按照什么标准来贪心选择?
可能的策略:
- 选择持续时间最短的活动
- 选择开始时间最早的活动
- 选择结束时间最早的活动 ✅
为什么选择结束时间最早的活动是最优的?
-
直观理解:越早结束,留给后续活动的时间就越多
-
形式化证明:假设存在一个最优解不包含最早结束的活动,我们可以用最早结束的活动替换其中某个活动,得到同样优或更优的解
时间轴示例:
a₁: |-----| (结束时间最早)
aᵢ₁: |-------| (原最优解的第一个活动)
aᵢ₂: |-------| (原最优解的第二个活动)关键点:
- aᵢ₁ 与 aᵢ₂ 不冲突 → aᵢ₂.start ≥ aᵢ₁.finish
- a₁.finish ≤ aᵢ₁.finish → aᵢ₂.start ≥ a₁.finish
- 所以 a₁ 与 aᵢ₂ 也不冲突!
算法步骤
- 将所有活动按照结束时间升序排序
- 选择第一个活动(结束时间最早的)
- 从剩余活动中,选择开始时间不早于上一个已选活动结束时间的活动
- 重复步骤3,直到没有可选活动
时间复杂度
- 排序:O(n log n)
- 选择过程:O(n)
- 总体:O(n log n)
C++ 实现
cpp
#include <iostream>
#include <vector>
#include <algorithm>
// 活动结构体
struct Activity {
int start; // 开始时间
int finish; // 结束时间
int id; // 活动编号(用于输出)
// 构造函数
Activity(int s, int f, int i) : start(s), finish(f), id(i) {}
};
// 比较函数:按结束时间升序排序
bool compareActivities(const Activity& a, const Activity& b) {
return a.finish < b.finish;
}
// 贪心活动选择算法
std::vector<Activity> activitySelection(std::vector<Activity>& activities) {
// 1. 按结束时间排序
std::sort(activities.begin(), activities.end(), compareActivities);
std::vector<Activity> selected;
// 2. 选择第一个活动(结束时间最早的)
selected.push_back(activities[0]);
int lastSelectedIndex = 0;
// 3. 贪心选择后续活动
for (int i = 1; i < activities.size(); i++) {
// 如果当前活动的开始时间 >= 上一个选中活动的结束时间
if (activities[i].start >= activities[lastSelectedIndex].finish) {
selected.push_back(activities[i]);
lastSelectedIndex = i;
}
}
return selected;
}
// 打印活动信息
void printActivities(const std::vector<Activity>& activities) {
std::cout << "选中的活动:\n";
std::cout << "活动ID\t开始时间\t结束时间\n";
for (const auto& activity : activities) {
std::cout << activity.id << "\t" << activity.start << "\t\t" << activity.finish << "\n";
}
}
int main() {
// 测试数据:每个活动的 {开始时间, 结束时间, ID}
std::vector<Activity> activities = {
Activity(1, 4, 1),
Activity(3, 5, 2),
Activity(0, 6, 3),
Activity(5, 7, 4),
Activity(3, 9, 5),
Activity(5, 9, 6),
Activity(6, 10, 7),
Activity(8, 11, 8),
Activity(8, 12, 9),
Activity(2, 14, 10),
Activity(12, 16, 11)
};
std::cout << "原始活动列表:\n";
std::cout << "活动ID\t开始时间\t结束时间\n";
for (const auto& act : activities) {
std::cout << act.id << "\t" << act.start << "\t\t" << act.finish << "\n";
}
std::cout << "\n" << std::string(40, '-') << "\n";
// 执行活动选择算法
std::vector<Activity> selected = activitySelection(activities);
// 输出结果
printActivities(selected);
std::cout << "\n总共选择了 " << selected.size() << " 个活动\n";
return 0;
}
算法正确性证明(简要)
贪心选择性质:存在一个最优解包含最早结束的活动。
证明思路:
- 设 S 是一个最优解,其中第一个活动是 a_k(不是最早结束的 a_1)
- 由于 a_1 结束时间 ≤ a_k 结束时间,所以用 a_1 替换 a_k 后,不会与 S 中其他活动冲突
- 因此 S' = (S - {a_k}) ∪ {a_1} 也是一个最优解,且包含贪心选择
最优子结构:在做出贪心选择后,剩余子问题的最优解与贪心选择组合构成原问题的最优解。
运行结果示例

原始活动列表:
活动ID 开始时间 结束时间
1 1 4
2 3 5
3 0 6
4 5 7
5 3 9
6 5 9
7 6 10
8 8 11
9 8 12
10 2 14
11 12 16
----------------------------------------
选中的活动:
活动ID 开始时间 结束时间
1 1 4
4 5 7
8 8 11
11 12 16
总共选择了 4 个活动
扩展思考
-
变种问题:如果活动有不同权重,目标是最大化总权重而不是活动数量,这时贪心算法不再适用,需要使用动态规划。
-
实际应用:
- 会议室调度
- 任务调度
- 广告插播安排
- 课程安排
-
为什么贪心有效 :这个问题具有贪心选择性质 和最优子结构,这是贪心算法适用的关键条件。