把奶牛看成 SPF 轴上的区间、防晒霜看成时间点上的资源:按 SPF 升序扫描,用堆维护待选奶牛并按 maxSPF 从小到大优先满足最快过期的奶牛,得到 O((C+L)logC) 的最优解。
OJ: roj
题目 ID: 3018
难度:普及+/提高-
标签:贪心堆排序python
创建: 2026-10-01 10:26
更新: 2026-10-07 13:50
形式化题目
给定
正解
思路
朴素做法一:搜索。 一头一头奶牛地枚举它涂哪一瓶防晒霜,回溯搜索。分支因子是
朴素做法二:二分图最大匹配。 左边
关键观察:在同一个 SPF 上,奶牛之间只有「谁更急」的区别。
把防晒霜按
越小的奶牛,未来可用的 SPF 集合越小。
用样例验证这个直觉。先把三头奶牛按 minSPF 升序记为 # 表示该强度落在区间内):
强度 1 2 3 4 5 6 7 8 9 10
u1(1,5) # # # # # . . . . .
u2(2,5) . # # # # . . . . .
u3(3,10) . . # # # # # # # #
防晒霜 . . . 4 . 6 . . . .观察这张图:SPF=4 有三个候选(SPF=6 只剩一个候选(SPF=4 的那一瓶给了
于是得到贪心:按 SPF 升序扫描每一瓶防晒霜,每瓶都优先给「能接受它且
如何高效实现。 直接按上面这句话写,每一瓶都要在全体奶牛里找一遍候选,是
- 奶牛按
升序排序,防晒霜按 升序排序。 - 用一个下标
next_cow顺序推进:扫到值为的防晒霜时,把 的奶牛 (也就是刚刚「解锁」的那一段)的 依次压入小根堆。因为 单调递增, 每头奶牛只会入堆一次,指针也只会前进一次。 - 堆顶是待选池中
最小的奶牛。先把堆顶所有 的奶牛弹出: 它们连当前这瓶都用不了,之后更大的 更用不了,可以永久放弃。 - 若这瓶防晒霜还有余量且堆非空,就反复「弹出堆顶、答案 +1、余量 −1」—— 堆顶正是「最急」的那头。
第 3、4 步的顺序不能颠倒:必须先把过期奶牛清走,才能保证喂出去的都是合法匹配。
代码
Python 版:
#!/usr/bin/env python3
# Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
# rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
# rainboy的学习导航网站: https://idx.roj.ac.cn
# create_at: 2026-10-01 10:26
# update_at: 2026-10-01 10:26
import sys
from heapq import heappop, heappush
def max_satisfied(cows: list[tuple[int, int]], bottles: list[tuple[int, int]]) -> int:
"""按 SPF 从小到大发放防晒霜,每个强度下优先喂给 maxSPF 最小(最快过期)的奶牛。
左端点不足的奶牛晚点才进入待选池,所以扫描 SPF 时把它们按 minSPF 逐个入堆,
堆按 maxSPF 排序:堆顶一旦连当前 SPF 都够不到,就再也用不上任何防晒霜了。
"""
pending: list[int] = [] # 待选奶牛的 maxSPF,堆顶是其中最急的一头
next_cow = 0 # 下一头等待进入待选池的奶牛下标
ans = 0
for spf, cover in bottles:
while next_cow < len(cows) and cows[next_cow][0] <= spf: # minSPF 已到,可以接受这瓶
heappush(pending, cows[next_cow][1])
next_cow += 1
while pending and pending[0] < spf: # 堆顶这头牛的 maxSPF 已经过期,彻底放弃
heappop(pending)
while cover > 0 and pending: # 同强度的每一瓶都给当前最快过期的奶牛
heappop(pending)
cover -= 1
ans += 1
return ans
def solve() -> None:
data = iter(map(int, sys.stdin.buffer.read().split()))
C, L = next(data), next(data)
cows = sorted((next(data), next(data)) for _ in range(C)) # 按 minSPF 升序,配合扫描依次入堆
bottles = sorted((next(data), next(data)) for _ in range(L)) # 按 SPF 升序依次发放
print(max_satisfied(cows, bottles))
if __name__ == "__main__":
solve()C++ 版(同一算法):
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-06 14:15
* update_at: 2026-10-06 14:15
*/
// main.cpp:把奶牛看成 SPF 轴上的区间、防晒霜看成带重数的点,求最大匹配数。
// 做法:防晒霜按 SPF 升序发放,用小根堆维护待选奶牛,优先满足 maxSPF 最小(最快过期)的奶牛。
#include <algorithm>
#include <iostream>
#include <queue>
#include <utility>
typedef long long ll;
const int MAXN = 2505;
int C, L;
std::pair<ll, ll> cow[MAXN]; // cow[i] = {minSPF, maxSPF},按 minSPF 升序排序
std::pair<ll, ll> bottle[MAXN]; // bottle[j] = {SPF, cover},按 SPF 升序排序
int main() {
std::ios::sync_with_stdio(false);
std::cin.tie(0);
std::cin >> C >> L;
for (int i = 0; i < C; i++)
std::cin >> cow[i].first >> cow[i].second;
for (int j = 0; j < L; j++)
std::cin >> bottle[j].first >> bottle[j].second;
// 奶牛按 minSPF 升序:扫到 SPF=s 时,minSPF<=s 的奶牛刚好全部解锁
std::sort(cow, cow + C);
// 防晒霜按 SPF 升序依次发放
std::sort(bottle, bottle + L);
// 小根堆存待选奶牛的 maxSPF:堆顶是可行集合最小(最急)的那头
std::priority_queue<ll, std::vector<ll>, std::greater<ll> > pending;
int next_cow = 0; // 下一头等待入堆的奶牛下标,只前进不后退
ll ans = 0;
for (int j = 0; j < L; j++) {
ll spf = bottle[j].first;
ll cover = bottle[j].second;
// 把 minSPF <= s 的奶牛压入堆,每头牛只入堆一次
while (next_cow < C && cow[next_cow].first <= spf) {
pending.push(cow[next_cow].second);
next_cow++;
}
// 先清掉已经过期的奶牛:maxSPF < s 连这瓶都接受不了,以后更没机会
while (!pending.empty() && pending.top() < spf)
pending.pop();
// 每瓶优先发给最急(maxSPF 最小)的奶牛
while (cover > 0 && !pending.empty()) {
pending.pop();
cover--;
ans++;
}
}
std::cout << ans << std::endl;
return 0;
}复杂度
| 阶段 | 代价 |
|---|---|
| 排序 |
|
| 每头奶牛入堆一次 | 摊还 |
| 每瓶防晒霜的发放循环 | 每次弹出一头牛,总计不超过 |
| 扫描 |
|
| 合计 |
空间
总结
本题的核心是把「区间与点的匹配」放到时间轴上扫描:
- 模型:奶牛是区间
,防晒霜是 SPF 轴上的点(带重数),求最大匹配数。 - 贪心准则:按 SPF 升序发放,每瓶优先给能接受它的、
最小的奶牛 —— 因为在同一时刻所有候选的 都已满足, 小者的可行集合是别人的子集,最急。 - 实现:
minSPF升序的指针 +maxSPF的小根堆,把「找最急的候选」从降到 。
交换论证保证了贪心的正确性:若最优解把当前最急的奶牛
图示解析
下面这张表把样例
样例的输入是:奶牛区间 SPF=6 有 2 瓶、SPF=4 有 1 瓶。
排序后按 (4, 1)、(6, 2)。
| 步骤 | 当前防晒霜 | 新入堆的奶牛 | 淘汰 | 堆中 |
发放 | 累计答案 |
|---|---|---|---|---|---|---|
| 1 | SPF=4, cover=1 |
无 | {5, 5, 10} |
弹出 |
1 | |
| 2 | SPF=6, cover=2 |
无 | {10} |
弹出 |
2 |
得到答案 2,与样例一致。
观察重点有三个:
- 第 1 步的候选是全部三头牛,但算法选择
,因为它的 最小 —— 最急的先喂。 - 第 2 步开始时
的 被淘汰,这就是「先弹出过期奶牛」这一步的作用: 如果没有它, SPF=6就会错喂给,而 也就再没有机会了。 - 若第 1 步改把
SPF=4给(违反贪心准则),第 2 步的 SPF=6两瓶就无人可用 (的 都是 5),答案从 2 掉到 1 —— 这正是「 小者的可行集合是别人子集、 子集必须先满足」的代价。