信奥新赛季备考 · 贪心策略高频考点。本文用一道原创「校园开放日摊位排期」题,带你吃透区间调度与最少资源整合两类经典贪心,附 C++/Python 双版解法、复杂度、易错点与进阶。

一、引言:为什么贪心是考场"性价比之王"
在信息学竞赛里,贪心算法以"每步只看当前最优、代码量极少、运行飞快"著称。统计上看,约 30% 的普及组题、15% 的提高组题会单独或结合使用贪心思想,常和排序、优先队列、扫描线搭配出现。
贪心能用的前提只有两个:局部最优能推出全局最优(贪心选择性质) + 子问题最优解可拼接(最优子结构)。一旦证明成立,原本需要 O(2ⁿ) 暴搜或 O(n²) 动态规划的问题,往往能被压缩到 O(n log n)。
今天我们就用区间类问题------贪心最经典的战场------来实战。
二、题目与目标(原创题:校园开放日摊位排期系统)
学校开放日来了 N 个社团申请摊位,每个社团给出自己的活动时间 [开始时刻, 结束时刻)(左闭右开,端点相接不算冲突)。你需要回答两个调度问题:
- 任务 A(区间调度) :同一时刻一个摊位只能办一场活动,求最多能安排多少场不冲突的活动?
- 任务 B(最少资源整合) :若所有活动都必须办成,同一时刻并行活动最多有几场?这等价于最少需要开放几个摊位(资源)才能不冲突地容纳全部活动。
输入格式 :第一行整数 N;接下来 N 行,每行两个整数 s e,表示活动的起止时刻。
样例输入:
6
1 3
2 5
3 6
5 7
6 9
8 10
样例输出:
任务A 最多可安排活动数:3
任务B 最少需要摊位数:2
三、核心考点拆解
- 排序预处理是贪心的命门:几乎所有区间贪心都先排序,排错关键字全盘皆输。
- 任务 A 按"结束时间"升序:每次选"结束最早且不与已选冲突"的活动,能给后面留出最多空档。
- 任务 B 用扫描线/事件法:把每个区间拆成"开始 +1、结束 −1"两个事件,扫描一遍即可求出任意时刻的最大并发数。
- 端点开闭处理 :区间按
[s, e)理解,两个区间如[1,3)与[3,6)首尾相接不算重叠------事件排序时同坐标必须先处理结束 (−1) 再处理开始 (+1)。 - 多关键字排序:同结束时间时顺序任意;同坐标事件必须靠第二关键字区分 ±1。
四、解法拆解
任务 A:最多不重叠活动(区间调度)
思路 :按结束时间升序排列,维护上一个被选活动的结束时刻 lastEnd。从左到右遍历,只要当前活动开始 s ≥ lastEnd(端点相接允许),就选它并更新 lastEnd = e。
正确性直觉:局部选"结束最早"的,留给后续的可选空间最大,可证这是全局最优(反证法:若存在更优解,把其中最早结束的活动替换成我们的选择,不减少数量且结束不更晚)。
C++ 实现:
cpp
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
// 任务A:最多不重叠区间(区间调度)
// intervals: 每个为 [start, end),返回可安排的最大活动数
int maxNonOverlap(vector<pair<int, int>>& intervals) {
// 关键:按结束时间升序排序
sort(intervals.begin(), intervals.end(),
[](const pair<int, int>& a, const pair<int, int>& b) {
return a.second < b.second; // 仅按结束时间排,结束相同则任意
});
int cnt = 0;
int lastEnd = -1; // 已选最后一个活动的结束时间(初值视为 -∞)
for (auto& it : intervals) {
int s = it.first, e = it.second;
if (s >= lastEnd) { // 开始不早于上一个结束(端点相接不冲突)
cnt++;
lastEnd = e;
}
}
return cnt;
}
Python 实现:
python
def max_non_overlap(intervals):
"""intervals: list of (start, end) 左闭右开,返回最多不重叠区间数"""
ivs = sorted(intervals, key=lambda x: x[1]) # 按结束时间升序
cnt = 0
last_end = float('-inf')
for s, e in ivs:
if s >= last_end: # 端点相接不算冲突
cnt += 1
last_end = e
return cnt
样例手推 :排序后顺序为 (1,3)(2,5)(3,6)(5,7)(6,9)(8,10)。依次选 (1,3)→(3,6)→(6,9),共 3 场,其余都因冲突被跳过。
任务 B:最少资源(最大并发区间数)
思路 :把每个区间 [s, e) 拆成事件 (s, +1) 与 (e, −1)。按坐标升序扫描,同坐标时结束事件 (−1) 排在开始事件 (+1) 之前 。用 cur 维护当前并发数,mx 记录峰值------峰值就是"同一时刻最多有几场并行",也就是最少需要的摊位数。
C++ 实现:
cpp
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
// 任务B:最少所需资源数 = 任意时刻的最大重叠区间数
int minResources(vector<pair<int, int>>& intervals) {
vector<pair<int, int>> events; // (坐标, +1 或 -1)
for (auto& it : intervals) {
events.push_back({it.first, 1}); // 区间开始:占用 +1
events.push_back({it.second, -1}); // 区间结束:释放 -1
}
// 同坐标时,结束(-1)必须先于开始(+1)处理,
// 这样"首尾相接"的两个区间不会被误算成重叠
sort(events.begin(), events.end(),
[](const pair<int, int>& a, const pair<int, int>& b) {
if (a.first != b.first) return a.first < b.first;
return a.second < b.second; // -1 < +1,先处理结束
});
int cur = 0, mx = 0;
for (auto& ev : events) {
cur += ev.second;
mx = max(mx, cur);
}
return mx;
}
Python 实现:
python
def min_resources(intervals):
"""最少资源数 = 任意时刻最大重叠区间数"""
events = []
for s, e in intervals:
events.append((s, 1))
events.append((e, -1))
# 同坐标:结束(-1)先于开始(+1)
events.sort(key=lambda x: (x[0], x[1]))
cur = mx = 0
for _, d in events:
cur += d
mx = max(mx, cur)
return mx
样例手推 :事件流为 1:+1(1) → 2:+1(2) → 3:-1(1) → 3:+1(2) → 5:-1(1) → 5:+1(2) → 6:-1(1) → 6:+1(2) → 7:-1(1) → 8:+1(2) → 9:-1(1) → 10:-1(0)。cur 的峰值始终为 2 ,故最少需要 2 个摊位。注意 (3,-1) 在 (3,+1) 之前处理,避免了 [1,3) 与 [3,6) 被误算成重叠。
五、时间与空间复杂度
| 任务 | 时间复杂度 | 空间复杂度 | 说明 |
|---|---|---|---|
| A 区间调度 | O(n log n) | O(1)(原地排序) | 瓶颈在排序,扫描为 O(n) |
| B 最少资源 | O(n log n) | O(n) | 事件数组 2n,排序 + 扫描 |
两者都远优于暴搜(2ⁿ)和普通 DP(O(n²)),在 n 达到 10⁵ 时仍轻松跑过。
六、易错点(考前必看)
- 任务 A 排错关键字 :必须按结束时间升序;若误按开始时间排序,会得到错误答案(贪心选择性质不成立)。
- 端点开闭理解错 :本题约定
[s, e)。若把区间当成闭区间,会导致"首尾相接"被误判冲突或误判重叠。 - 任务 B 同坐标事件顺序 :
(e, −1)必须在(s', +1)(当 e == s')之前处理,否则把相邻区间算成重叠,资源数虚高。 lastEnd初值 :应初始化为负无穷(如 −1 或float('-inf')),否则第一个区间可能因s < 0类初值被漏选(时刻非负时 −1 也行,但负无穷更稳妥)。- 坐标跨度大时别用差分数组 :若时刻范围到 10⁹,开数组会爆内存------务必用事件扫描线法而非
diff[]数组。 - 多组测试别忘重置 :
cnt / cur / mx等计数器每组需重新初始化,全局变量残留是常见 WA 来源。
七、进阶延伸
掌握上面两题后,可顺势拓展以下变体:
(1) 区间合并:把所有重叠/相接区间合并成不相交的大区间。
python
def merge_intervals(intervals):
ivs = sorted(intervals)
res = []
for s, e in ivs:
if res and s <= res[-1][1]: # 与上一区间重叠或相接
res[-1][1] = max(res[-1][1], e)
else:
res.append([s, e])
return res
(2) 区间覆盖 :用最少区间覆盖住整个 [1, T],每次贪心选"能盖住当前起点且延伸最远"的区间;若出现盖不住的空隙则返回 −1。
python
def cover_segment(intervals, T):
ivs = sorted(intervals) # 按左端点升序
pos, used, i, n = 1, 0, 0, len(ivs)
while pos <= T:
best_r = -1
while i < n and ivs[i][0] <= pos:
best_r = max(best_r, ivs[i][1])
i += 1
if best_r < pos:
return -1 # 出现无法覆盖的空隙
used += 1
pos = best_r + 1
return used
(3) 带权活动选择(加权区间调度):每个活动有权重,求最大权重不重叠子集。按结束时间排序后 DP + 二分查找前驱,复杂度 O(n log n)。
python
def weighted_activity(intervals, vals):
n = len(intervals)
items = sorted(zip(intervals, vals), key=lambda x: x[0][1]) # 按结束时间
ends = [it[0][1] for it in items]
dp = [0] * (n + 1)
for k in range(1, n + 1):
s, e = items[k - 1][0]
v = items[k - 1][1]
lo, hi, p = 0, k - 1, -1
while lo < hi: # 找结束时间 <= s 的最后一个
mid = (lo + hi) // 2
if ends[mid] <= s:
p = mid; lo = mid + 1
else:
hi = mid
take = v + (dp[p + 1] if p >= 0 else 0)
dp[k] = max(dp[k - 1], take)
return dp[n]
(4) 堆优化扫描线:任务 B 求最大并发也可改用优先队列实时维护"结束时间最小"的活动,堆的峰值即答案,适合需要动态增删的场景。
(5) 区间选点:求最少需要几个点,使每个区间至少包含一个点------按右端点升序,每次在末尾放点即可。
八、小结与互动
今天我们拿下了两道区间贪心经典题:区间调度(按结束时间贪心) 与 最少资源整合(扫描线求最大并发)。记住三句话------"先排序、再贪心、端点顺序别搞反",区间类题基本就稳了。
你在新赛季刷题时还遇到过哪些"看着像贪心、其实是 DP/二分"的坑?欢迎在评论区聊聊你的踩坑经历,我们一起对拍验证 🚀
如果觉得有用,记得点赞 + 收藏 + 关注,下一篇我们继续拆解信奥高频算法。
📚 免费少儿编程资料(夸克网盘领取)
以下资料来自夸克网盘分享,点击链接可直接保存;若需在 App 内打开,也可复制下方明文链接:
- 全国青少年信息素养大赛复赛集训题目Python&C++.docx
https://pan.quark.cn/s/93995d3cb150 - 2024信息素养-智能算法应用挑战赛-复赛初中组题目7月7日.pdf
https://pan.quark.cn/s/da97b5dbf75d - Python背记手册.pdf
https://pan.quark.cn/s/7568ae9ca92b - Python课程
https://pan.quark.cn/s/a94bf02d00c6 - 2024信息素养大赛图形化复赛集训题答案3-9
https://pan.quark.cn/s/6ccab7ec3cbc - 2025年03月份电子学会考级真题
https://pan.quark.cn/s/4403c4228912 - 2025全国青少年信息素养大赛赛项说明
https://pan.quark.cn/s/d9d0df4a9f29 - 青少儿信息素养大赛编程资料
https://pan.quark.cn/s/4ab6bd83be8a
资料持续更新,关注获取最新分享。