在算法世界里,“区间” 是一个极其常见的概念 —— 从任务调度的时间窗口,到地图上的地理范围,再到数据查询的索引区间,处处都能看到区间的身影。而贪心算法,凭借其 “局部最优推导全局最优” 的核心思想,成为解决区间问题的 “利器”。无论是 “最多参加多少场比赛”“最少安装多少个雷达”,还是 “如何安排牛棚最省空间”,贪心算法都能以简洁高效的方式找到最优解。今天,我们就从区间问题的核心场景出发,拆解贪心策略的设计思路、证明方法和代码实现,带你吃透这类经典问题。
在开始具体问题前,我们先明确区间问题的共性:所有问题都围绕 “区间的位置关系” 展开,常见的位置关系包括 “重叠”“包含”“相邻”“不相交”。而贪心算法的核心,就是针对不同问题目标,设计出 “每次选择哪个区间” 的规则 —— 比如 “选结束最早的”“选覆盖最广的”“选重叠最少的”。
一个有效的贪心策略,必须满足两个条件:
对于区间问题,贪心策略的设计通常围绕以下两个维度:
后续所有例题,都会围绕这两个排序维度展开,带你理解 “为什么这样排序”“为什么这样选择”。
很多同学学贪心时会有疑问:“我怎么知道这个策略一定能得到最优解?” 其实,区间问题的贪心策略,大多可以通过交换论证法证明 —— 假设存在一个最优解,通过交换其中两个区间的顺序,使其与贪心解一致,且不改变最优性,最终证明贪心解就是最优解。
举个简单例子:如果贪心策略是 “选结束最早的区间”,而最优解中第一个区间的结束时间比贪心解的第一个区间晚,那么我们可以将最优解的第一个区间换成贪心解的第一个区间,此时总选择数量不变,且后续可选择的区间更多,最优性不变。通过这种 “交换”,就能将最优解逐步转化为贪心解,证明策略有效。
题目链接:https://www.luogu.com.cn/problem/P1803
有 n 个比赛,每个比赛有开始时间 a_i 和结束时间 b_i(a_i <b_i)。要求选择尽可能多的比赛,使得所选比赛互不重叠(不能同时参加两场比赛)。例如输入 3 场比赛:[0,2]、[2,4]、[1,3],最多能参加 2 场(如 [0,2] 和 [2,4])。
目标是 “最大化选择的区间数量”,核心是 “如何在不重叠的前提下,选最多的区间”。直觉告诉我们:如果选了一个结束时间早的区间,就能给后续留下更多时间选择其他区间 —— 这就是贪心策略的核心。
假设存在一个最优解 S,其选择的区间数量比贪心解 G 多。设 S 中第一个与 G 不同的区间为 S_k,G 中对应的区间为 G_k。由于 G 按结束时间排序,G_k 的结束时间 ≤ S_k 的结束时间。将 S 中的 S_k 换成 G_k,此时 S 的区间数量不变,且后续可选择的区间更多(因为 G_k 结束更早),因此新的解 S' 仍是最优解。通过不断交换,最终 S 会转化为 G,证明 G 是最优解。
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
// 定义区间结构体
struct Interval {
int start; // 开始时间
int end; // 结束时间
};
// 排序规则:按结束时间从小到大排序
bool cmp(const Interval& a, const Interval& b) {
return a.end < b.end;
}
int main() {
int n;
cin >> n;
vector<Interval> intervals(n);
for (int i = 0; i < n; i++) {
cin >> intervals[i].start >> intervals[i].end;
}
// 步骤1:按结束时间排序
sort(intervals.begin(), intervals.end(), cmp);
// 步骤2:选择不重叠的区间
int count = 0; // 选中的区间数量
int last_end = -1; // 上一个选中区间的结束时间(初始为-1,确保第一个区间能被选中)
for (auto& inter : intervals) {
// 如果当前区间的开始时间 >= 上一个区间的结束时间,选择该区间
if (inter.start >= last_end) {
count++;
last_end = inter.end;
}
}
cout << count << endl;
return 0;
}last_end = -1(假设所有比赛的开始时间 ≥ 0,确保第一个区间能被选中);3
0 2
2 4
1 3 输出:2
分析:排序后区间为 [0,2]、[1,3]、[2,4],选中 [0,2] 和 [2,4],共 2 个。
题目链接:https://www.luogu.com.cn/problem/UVA1193
海岸线是 x 轴,大海在 x 轴上方,有 n 个小岛(坐标 (x_i, y_i))。雷达的覆盖范围是半径为 d 的圆,雷达只能安装在 x 轴上(海岸线)。要求找到最少的雷达数量,使得所有小岛都被覆盖。如果无法覆盖(如小岛的 y_i > d),输出 - 1。
首先,我们需要将 “二维问题转化为一维问题”:对于每个小岛

,雷达要覆盖它,必须安装在 x 轴上的某个区间内。根据勾股定理,这个区间的左端点为

,右端点为

(记为
![[L_i, R_i]](https://developer.qcloudimg.com/http-save/yehe-100000/84f1820ede3f8842c67490b8f594e75d.png)
)。

此时问题转化为:在 x 轴上选择最少的点,使得每个区间 [L_i, R_i] 都至少包含一个点—— 这就是经典的 “区间点覆盖” 问题。
目标是 “最小化点的数量”,核心是 “让每个点覆盖尽可能多的区间”。策略如下:
假设存在一个最优解 S,其雷达数量比贪心解 G 少。设 S 中第一个雷达的位置为 s_1,G 中第一个雷达的位置为 g_1(g_1 = R_1)。由于 S 的雷达数量更少,s_1 必须 > g_1(否则 S 的第一个雷达覆盖范围不会比 G 大)。但此时,第一个区间 [L_1, R_1] 的右端点是 g_1,s_1 > g_1 意味着 s_1 无法覆盖第一个区间,与 S 是可行解矛盾。因此,G 的雷达数量不可能比 S 多,即 G 是最优解。
#include <iostream>
#include <algorithm>
#include <vector>
#include <cmath>
using namespace std;
// 定义区间结构体(存储雷达覆盖小岛的x轴区间)
struct Interval {
double L; // 左端点
double R; // 右端点
};
// 排序规则:按区间右端点从小到大排序
bool cmp(const Interval& a, const Interval& b) {
return a.R < b.R;
}
int main() {
int n, d;
int case_num = 0;
while (cin >> n >> d, n || d) { // 输入0 0结束
case_num++;
vector<Interval> intervals(n);
bool impossible = false; // 是否无法覆盖
for (int i = 0; i < n; i++) {
int x, y;
cin >> x >> y;
if (y > d) {
impossible = true; // 小岛的y坐标超过雷达半径,无法覆盖
}
// 计算雷达覆盖区间的左右端点
double len = sqrt(d * d - y * y);
intervals[i].L = x - len;
intervals[i].R = x + len;
}
if (impossible) {
cout << "Case " << case_num << ": -1" << endl;
continue;
}
// 步骤1:按区间右端点排序
sort(intervals.begin(), intervals.end(), cmp);
// 步骤2:选择最少的雷达
int radar_count = 0;
double last_radar = -1e18; // 上一个雷达的位置(初始为极小值)
for (auto& inter : intervals) {
// 如果当前区间的左端点 > 上一个雷达的位置,需要新雷达
if (inter.L > last_radar) {
radar_count++;
last_radar = inter.R; // 在当前区间的右端点放雷达
}
}
cout << "Case " << case_num << ": " << radar_count << endl;
}
return 0;
}3 2
1 2
-3 1
2 1
1 2
0 2
0 0输出:
Case 1: 2
Case 2: 1分析:
题目链接:https://www.luogu.com.cn/problem/P2887
有 C 头奶牛晒太阳,每头奶牛能忍受的阳光强度范围是
![[L_i, R_i]](https://developer.qcloudimg.com/http-save/yehe-100000/84f1820ede3f8842c67490b8f594e75d.png)
(太小没感觉,太大晒伤)。有 L 种防晒霜,每种防晒霜的固定强度为

,数量为

(每头奶牛只能用一瓶)。要求找到最多能享受晒太阳的奶牛数量。
这是一个 “区间与点的匹配” 问题:每个奶牛需要一个强度在其区间
![[L_i, R_i]](https://developer.qcloudimg.com/http-save/yehe-100000/84f1820ede3f8842c67490b8f594e75d.png)
内的防晒霜,每个防晒霜只能用

次。目标是 “最大化匹配数量”。
关键是 “如何分配防晒霜,让更多奶牛用上合适的”。直觉是:给每头奶牛分配 “能满足它的最小 / 最大防晒霜”,避免浪费高适配性的防晒霜。这里我们选择 “按奶牛区间右端点排序,按防晒霜强度排序,优先给奶牛分配最小的合适防晒霜”—— 这样能让大强度的防晒霜留给需要更大强度的奶牛。
具体步骤:
假设存在一个最优解 S,其中某头奶牛 A(区间
![[L_A, R_A]](https://developer.qcloudimg.com/http-save/yehe-100000/1e4f212467b06d35e55f734dec992ac8.png)
)使用了防晒霜

,而另一头奶牛 B(区间
![[L_B, R_B]](https://developer.qcloudimg.com/http-save/yehe-100000/4f1eacbe4b5c1fadd9c9b9d2cfe10753.png)
,

)使用了防晒霜

(

),且

也能满足 A。交换 A 和 B 的防晒霜,此时 A 仍满足(

且

),B 仍满足(

且

,因为

且

),匹配数量不变。通过这种交换,最终会将最优解转化为 “优先给奶牛分配最小合适防晒霜” 的贪心解,证明贪心解最优。
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
// 定义奶牛结构体(忍受的阳光强度范围)
struct Cow {
int L; // 最小强度
int R; // 最大强度
};
// 定义防晒霜结构体(强度和数量)
struct Sunscreen {
int S; // 固定强度
int C; // 数量
};
// 奶牛排序规则:按右端点R从小到大排序
bool cmpCow(const Cow& a, const Cow& b) {
return a.R < b.R;
}
// 防晒霜排序规则:按强度S从小到大排序
bool cmpSunscreen(const Sunscreen& a, const Sunscreen& b) {
return a.S < b.S;
}
int main() {
int C, L;
cin >> C >> L;
vector<Cow> cows(C);
vector<Sunscreen> sunscreens(L);
for (int i = 0; i < C; i++) {
cin >> cows[i].L >> cows[i].R;
}
for (int i = 0; i < L; i++) {
cin >> sunscreens[i].S >> sunscreens[i].C;
}
// 步骤1:排序
sort(cows.begin(), cows.end(), cmpCow);
sort(sunscreens.begin(), sunscreens.end(), cmpSunscreen);
// 步骤2:匹配奶牛和防晒霜
int count = 0; // 能享受晒太阳的奶牛数量
int sun_ptr = 0; // 遍历防晒霜的指针
for (auto& cow : cows) {
// 找到所有强度 <= 奶牛R的防晒霜(因为奶牛按R排序,后续奶牛的R更大,无需回头)
while (sun_ptr < L && sunscreens[sun_ptr].S <= cow.R) {
sun_ptr++;
}
// 从已遍历的防晒霜中,找强度 >= 奶牛L且数量>0的最小强度防晒霜
for (int i = 0; i < sun_ptr; i++) {
if (sunscreens[i].S >= cow.L && sunscreens[i].C > 0) {
count++;
sunscreens[i].C--;
break;
}
}
}
cout << count << endl;
return 0;
}上述代码中,遍历防晒霜的内层循环时间复杂度为 O (L),总复杂度为 O (C×L),对于 C=2500、L=2500 的情况(题目数据范围)完全可行。如果数据范围更大(如 C=1e5),可使用 “优先队列” 或 “二分查找” 优化,将内层循环复杂度降至 O (logL)。
输入:
3 2
3 10
2 5
1 5
6 2
4 1 输出:2
分析:
题目链接:https://www.luogu.com.cn/problem/P2859
有 N 头奶牛,每头奶牛的产奶时间是 [A_i, B_i](包含 A_i 和 B_i)。要求给每头奶牛分配一个牛棚,使得同一牛棚内的奶牛产奶时间不重叠。求最少需要多少个牛棚,以及每头奶牛的牛棚编号。
这是 “区间划分” 问题:将所有区间划分到最少的集合中,每个集合中的区间互不重叠。目标是 “最小化集合数量”(牛棚数量)。
核心是 “让每个牛棚尽可能容纳更多奶牛”,策略如下:
根据 “区间图着色定理”:区间图的色数(最少颜色数,对应最少牛棚数)等于最大 clique 大小(即最多重叠区间的数量)。而该贪心策略能保证:每次都将当前奶牛分配到 “最早可用” 的牛棚,避免新建不必要的牛棚,最终堆的大小就是最大重叠区间数,即最少牛棚数。
#include <iostream>
#include <algorithm>
#include <vector>
#include <queue>
using namespace std;
// 定义奶牛结构体(开始时间、结束时间、原始下标、分配的牛棚号)
struct Cow {
int start;
int end;
int idx; // 原始下标(用于输出牛棚号)
int stall; // 分配的牛棚号
};
// 奶牛排序规则:按开始时间从小到大排序
bool cmpCow(const Cow& a, const Cow& b) {
return a.start < b.start;
}
// 优先队列(小根堆)存储:<牛棚最后结束时间, 牛棚编号>
using PQElement = pair<int, int>;
struct cmpPQ {
bool operator()(const PQElement& a, const PQElement& b) {
// 小根堆:结束时间小的优先
return a.first > b.first;
}
};
int main() {
int N;
cin >> N;
vector<Cow> cows(N);
for (int i = 0; i < N; i++) {
cin >> cows[i].start >> cows[i].end;
cows[i].idx = i; // 记录原始下标(0-based)
}
// 步骤1:按开始时间排序
sort(cows.begin(), cows.end(), cmpCow);
// 步骤2:分配牛棚
priority_queue<PQElement, vector<PQElement>, cmpPQ> pq;
int stall_count = 0; // 牛棚总数
vector<int> result(N); // 存储每头奶牛的牛棚号(按原始下标)
for (auto& cow : cows) {
if (pq.empty()) {
// 没有牛棚,新建一个
stall_count++;
cow.stall = stall_count;
pq.push({cow.end, stall_count});
} else {
auto [last_end, stall_id] = pq.top(); // C++17特性,结构化绑定
if (last_end < cow.start) {
// 该牛棚可容纳当前奶牛,更新牛棚的最后结束时间
pq.pop();
cow.stall = stall_id;
pq.push({cow.end, stall_id});
} else {
// 新建牛棚
stall_count++;
cow.stall = stall_count;
pq.push({cow.end, stall_count});
}
}
// 记录结果(按原始下标)
result[cow.idx] = cow.stall;
}
// 输出结果
cout << stall_count << endl;
for (int s : result) {
cout << s << endl;
}
return 0;
}auto [last_end, stall_id] = pq.top() 是结构化绑定,若编译器不支持,可拆分为 int last_end = pq.top().first; int stall_id = pq.top().second;。输入:
5
1 10
2 4
3 6
5 8
4 7输出:
4
1
2
3
2
4分析:
通过以上例题,我们可以总结出区间问题的四大类型及对应的贪心策略:
问题类型 | 目标 | 排序维度 | 贪心策略 |
|---|---|---|---|
区间选择(最多不重叠) | 最大化选择数量 | 按区间结束时间从小到大 | 选结束最早的,后续选不重叠的 |
区间点覆盖(最少点) | 最小化点的数量 | 按区间结束时间从小到大 | 每个点放在当前区间的右端点,覆盖最多后续区间 |
区间匹配(点与区间) | 最大化匹配数量 | 奶牛按 R 从小到大,点按 S 从小到大 | 给奶牛分配最小的合适点,避免浪费 |
区间划分(最少集合) | 最小化集合数量(无重叠) | 按区间开始时间从小到大 | 用小根堆记录集合最后结束时间,优先分配到最早可用的集合,否则新建集合 |
解决任何区间问题,都可以遵循以下三步:
<还是≤;long long(本题中无此问题,但需注意);贪心算法的魅力在于 “简单却高效”,而区间问题则是这种魅力的最佳体现,它不需要复杂的数据结构,却能很好地锻炼 “局部最优推导全局最优” 的思维。希望你在后续的练习中,能灵活运用本文所学的策略,举一反三,解决更多复杂的区间问题。如果在解题过程中遇到困难,不妨回到本文的例题,对比思路,找到问题所在 —— 算法学习的本质,就是在不断的思考和实践中,积累 “选择” 的智慧。