1 问题:一天最多参加几场比赛
给定若干活动的开始时间和结束时间,参加一个活动必须从头到尾,不能同时参加两个活动。怎样安排,才能参加最多的活动?
把每个活动画成时间轴上的一个区间,就是最大不相交区间数量问题,也叫区间调度或活动选择问题。
这里的「最大」指的是选出的区间个数最多,不是总长度最长。
本文以比赛调度为例,允许一场结束时立即参加下一场。因此
若题目要求闭区间连端点也不能相交,则 l >= last_end 改成 l > last_end。
2 例题:洛谷 P1803
题目:luogu P1803: 凌乱的yyy / 线段覆盖。
有
输入格式:第一行是
输出格式:一个整数,表示最多能参加的比赛数。
数据范围:
输入:
3
0 2
2 4
1 3
输出:
2
选择
3 先想暴力:每场选或不选
每场比赛都有「参加」与「不参加」两种选择,枚举全部子集,再判断其中的比赛是否冲突,就能找到正确答案。
判断一个子集时,可以按开始时间排列所选比赛,检查相邻两场是否满足「前一场结束不晚于后一场开始」。
这样的做法需要枚举
我们需要找到一种选择:获得同样的一场比赛,却尽量少限制后面的安排。
4 两个看似合理、实际错误的策略
4.1 谁开始得早,就先选谁?
考虑三场比赛:
; ; 。
最早开始的
开始得早,不代表结束得早。 我们关心的是选完以后,还能给后面的比赛留多少机会。
4.2 谁持续时间短,就先选谁?
考虑三场比赛:
; ; 。
最短的
所以「占用时间短」也不是答案。一个区间占据时间轴的什么位置,同样重要。
5 正确选择:能参加的里面,谁结束最早就选谁
假设已经参加完一些比赛,上一个已选比赛的结束时间是 last_end。
下一场必须满足
否则它已经在你空闲之前开始了,无法完整参加。
在所有满足条件的比赛中,优先选择结束时间最早的那一场。原因是:选哪一场都只让答案增加一,但结束得越早,后面可安排的比赛就越多。
把所有比赛按结束时间升序排序。
从前向后扫描:若当前比赛开始时间不早于上一个已选比赛的结束时间,就选择它,并更新结束时间;否则跳过它。
这不是「只要有空就随便选一场」:排序保证我们每次碰到的第一场可选比赛,正是结束最早的可选比赛。
6 为什么正确:替换最优方案的第一场
固定已经选择的比赛,设最后结束时刻为
若还有比赛可选,设贪心选择为
因为
把这个最优方案的第一场
开始时间不早于 ,因此不会与已经固定的比赛冲突。 - 原方案第二场的开始时间不早于
,也就不早于 ,所以仍能接在 后面。更后面的安排不变。 - 替换前后场数相同,仍然是最优方案。
因此,至少存在一个最优方案以贪心选择开头。固定这一场,对剩下的比赛重复相同论证,就能得到整体最优解。若没有可选比赛,显然也无法再增加场数。
注意我们并没有证明「所有最优方案都必须选择
7 排序后只扫一遍
维护两个量:
ans:已经选择的比赛数;last_end:最后一场已选比赛的结束时间。
扫描时分两种情况:
seg[i].l >= last_end:可以完整参加,选择它,更新last_end = seg[i].r。seg[i].l < last_end:与已选比赛冲突,跳过。
跳过的比赛以后也不必回头考虑:后续 last_end 只会变大,它的开始时间不会再满足条件。
结束时间相同的比赛,先扫描谁都不影响最优数量,因为选中后留下的时间边界相同。代码额外按开始时间升序打破平局,只是为了让排序结果确定。
7.1 样例推演
按结束时间排序后,顺序为
| 当前比赛 | 扫描前 last_end |
是否选择 | 扫描后 last_end |
ans |
|---|---|---|---|---|
| 0 | 是, |
2 | 1 | |
| 2 | 否, |
2 | 1 | |
| 2 | 是, |
4 | 2 |
7.2 完整 C++17 实现
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1000005;
struct Segment {
int l;
int r;
};
int n;
Segment seg[MAXN]; // 每个比赛的开始时间和结束时间
bool cmp_segment(const Segment &a, const Segment &b) {
if (a.r != b.r) {
return a.r < b.r;
}
return a.l < b.l;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> seg[i].l >> seg[i].r;
}
sort(seg + 1, seg + n + 1, cmp_segment);
int ans = 0;
int last_end = 0; // 上一个已选比赛的结束时间
for (int i = 1; i <= n; i++) {
// 当前比赛开始时间不早于 last_end,就可以接在已选比赛后面。
if (seg[i].l >= last_end) {
ans++;
last_end = seg[i].r;
}
}
cout << ans << '\n';
return 0;
}
P1803 的开始时间都非负,因此 last_end = 0 可以作为初始值。若迁移到允许负时间的题目,应使用「尚未选择」标记,或按数据范围设置足够小的初始值,不能直接照搬零。
7.3 复杂度
排序需要
8 四种区间问题,不要混用策略
题目标题不一定准确描述模型。P1803 虽然叫「线段覆盖」,实际求的是最多能选多少个互不冲突的区间,不是用最少区间覆盖目标。
| 模型 | 选择或安排什么 | 优化目标 | 典型策略 |
|---|---|---|---|
| 最大不相交区间数量(本文) | 选区间 | 数量最多 | 右端点升序,能接上就选 |
| 区间选点 | 选点,让每个区间都有点 | 点数最少 | 右端点升序,未覆盖时选右端点 |
| 区间分组 | 把所有区间分组,组内互不冲突 | 组数最少 | 左端点升序,小根堆维护每组末尾 |
| 区间覆盖 | 选区间,铺满目标范围 | 区间数最少 | 从第一个缺口出发,选择能接上的最远右端点 |
区间选点见 雷达设备,区间分组见 畜栏预定,区间覆盖见 区间覆盖:每次把边界推得最远。
对于闭区间且端点相交也算冲突的约定,最大不相交区间数与最少覆盖点数之间有经典的相等关系。
但不能把这个数值关系直接套到 P1803 的端点约定上。例如
9 自查与练习
- 只有一场比赛:答案是
。 - 多场比赛完全相同:只能选一场。
- 所有比赛首尾相接:可以全部选择,注意使用
>=。 - 一个长区间包含许多短区间:不要被「最早开始」误导。
- 若每场比赛有不同收益,目标改成总收益最大:最早结束优先不再保证最优。例如
收益为 , 收益为 ,只选前者显然不优,通常需要考虑带权区间调度的动态规划。
完成 luogu P1803: 凌乱的yyy / 线段覆盖 后,可以继续阅读 对拍练习:线段覆盖问题,用暴力、错误贪心和随机数据练习寻找反例。
每场比赛给答案的贡献都是一,因此在当前能参加的比赛里,结束越早,留给后面的空间越大。
按右端点排序 → 能接上就选 → 交换论证保证最优。
这套贪心解决的是数量最大化;端点能否相接、目标是否带权,都要另外核对。
10 相关题解
| 题目 | OJ | 难度 |
|---|---|---|
| 凌乱的yyy / 线段覆盖 | luogu | 普及- |