P1204 [USACO1.2] 挤牛奶Milking Cows
题目描述
三个农民每天清晨 555 点起床,然后去牛棚给三头牛挤奶。
第一个农民在 300300300 秒 (从 555 点开始计时) 给他的牛挤奶,一直到 100010001000 秒。第二个农民在 700700700 秒开始,在 120012001200 秒结束。第三个农民在 150015001500 秒开始,210021002100 秒结束。
期间最长的至少有一个农民在挤奶的连续时间为 900900900 秒 (从 300300300 秒到 120012001200 秒),而最长的无人挤奶的连续时间(从挤奶开始一直到挤奶结束)为 300300300 秒 (从 120012001200 秒到 150015001500 秒)。
你的任务是编一个程序,读入一个有 nnn 个农民挤 nnn 头牛的工作时间列表,计算以下两点(均以秒为单位):
最长至少有一人在挤奶的时间段。
最长的无人挤奶的时间段。(从有人挤奶开始算起)
输入格式
第一行一个正整数 nnn
接下来 nnn 行,每行两个非负整数 l,rl,rl,r,表示一个农民的开始时刻与结束时刻。
输出格式
一行,两个整数,即题目所要求的两个答案。
输入输出样例 #1
输入 #1
3
300 1000
700 1200
1500 2100
输出 #1
900 300
说明/提示
【数据范围】 :对于 100%100\%100% 的数据,1≤n≤50001\le n \le 50001≤n≤5000,0≤l≤r≤1060 \le l \le r \le 10^60≤l≤r≤106。
USACO Training Section 1.2
思路分析
✅ 题目本质:本题是一个典型的区间合并问题。
给出若干个时间段 [li,ri][l_i, r_i][li,ri],表示每个农民在这段时间内挤奶。你的任务是从这些区间中:
-
求出最长的“连续有人挤奶”的时间段长度
-
求出最长的“连续没有人挤奶”的时间段长度
🌟 对所有区间按开始时间升序排序
合并区间时,你需要保证——当你扫描到第 iii 个区间时,所有开始时间 ≤≤≤ 当前区间开始时间 的区间都已经被处理过。
只有这样才能正确判断“是否重叠”以及“该怎么更新右端点”。
这个条件恰好由「按开始时间升序」保证,而「按结束时间升序」无法保证。
结束时间从小到大排序的反例:
农民 | 区间 |
---|---|
A | (1, 10) |
B | (2, 5) |
C | (12, 15) |
按结束时间排序 → (B 2‑5
), (A 1‑10
), (C 12‑15
)
处理 B (2‑5)
,当前合并段 = [2, 5]
遇到 A (1‑10)
start=1
小于 当前合并段的 end=5
,程序会认为可以“合并”,并更新 end = max(5, 10) = 10
合并段变成 [2, 10]
,把 1
到 2
这段空闲错过了!最终:得到最长有人挤奶 2‑10
,真正答案应该是 1‑10
排序顺序把起点 111 的区间放到后面来处理,导致漏算。
🌟 线性扫描这些排好序的区间,维护一个当前合并段 [be, ed]
将下一个区间加入当前合并段:
-
如果下一个区间的起点在当前合并段的末尾之后(
start > ed
)-
当前合并段结束了,更新最大有人时间;
-
这两个区间之间是空隙,更新最大无人时间;
-
用新的区间重启合并段
[be, ed] = [start, end]
-
-
否则(存在重叠或相接)
- 将当前合并段的右端点更新为
ed = max(ed, end)
,继续扩展合并段
- 将当前合并段的右端点更新为
🌟 扫描结束后再处理一次最后的合并段
最后一个合并段可能是最长的,不能遗漏
🌟 记录两个值
-
mx1
:所有合并段中最长的挤奶时间 -
mx2
:合并段之间最长的无人时间
🧩 第一步:读入并排序
for(int i = 1; i <= n; i++) {cin >> x >> y;p[i] = make_pair(x, y);
}
sort(p + 1, p + 1 + n);
-
读取每个农民的挤奶时间区间
[x, y]
-
使用
pair<int, int>
存入数组p[i]
-
调用
sort()
按start
升序排序区间
✅ 第二步:初始化第一个区间为当前合并段
int be = p[1].first; // 当前合并段起点
int ed = p[1].second; // 当前合并段终点
int mx1 = ed - be; // 当前最长有人时间段长度
int mx2 = 0; // 当前最长无人时间段长度(初始为0)
- 将排序后的第一个时间区间作为当前的合并区间的起点和终点;
- 初始化最长有人挤奶时间段
mx1
为第一个区间的长度; - 初始化最长无人时间段
mx2
为 000,表示当前还没有无人段。
✅ 第三步:扫描剩余区间并尝试合并
for(int i = 2; i <= n; i++)
{if(p[i].first > ed) {// 区间不重叠,出现无人时间段mx1 = max(mx1, ed - be); // 更新最长有人时间段mx2 = max(mx2, p[i].first - ed); // 更新最长无人时间段be = p[i].first; // 启动新的合并段起点ed = p[i].second; // 启动新的合并段终点} else {// 区间重叠或相接,合并区间ed = max(ed, p[i].second); // 更新合并段终点}
}
- 从第二个区间开始,逐个遍历剩余时间段;
- 判断当前区间是否与已有合并段重叠或相接:
- 不重叠(当前区间的开始时间 > 合并段结束时间):
- 说明出现了无人时间段,先更新最长有人时间段
mx1
; - 更新最长无人时间段
mx2
(当前区间起点减去合并段终点); - 重置合并段为当前区间;
- 说明出现了无人时间段,先更新最长有人时间段
- 重叠或相接:
- 合并当前区间,更新合并段终点为更大的结束时间。
- 不重叠(当前区间的开始时间 > 合并段结束时间):
✅ 第四步:处理最后一个合并段
mx1 = max(mx1, ed - be);
- 遍历完所有区间后,最后一个合并段可能是最长的有人时间段;
- 因此需要用
mx1 = max(mx1, ed - be)
再次更新最长有人时间段。
✅ 第五步:输出结果
cout << mx1 << " " << mx2 << endl;
mx1
:最长连续有人挤奶的时间段长度;mx2
:最长连续无人挤奶的时间段长度。
完整代码:
#include <bits/stdc++.h>
using namespace std;
const int maxn = 5e3 + 9;pair<int, int> p[maxn];int main()
{int n;cin >> n;for (int i = 1; i <= n; i++){int x, y; // 开始时间 结束时间cin >> x >> y;p[i] = make_pair(x, y);}sort(p + 1, p + 1 + n); // 开始时间从小到大排序int mx1 = p[1].second - p[1].first, mx2 = 0, ed = p[1].second, be = p[1].first;for (int i = 2; i <= n; i++){if (p[i].first > ed){mx1 = max(mx1, ed - be);be = p[i].first; // 最新的开始时间mx2 = max(mx2, p[i].first - ed); // 当前的开始时间 减去 上一次的结束时间ed = p[i].second;}else{ed = max(ed, p[i].second);}}mx1 = max(mx1, ed - be); // 一直连续cout << mx1 << " " << mx2 << endl;return 0;
}