用小根堆维护每个水龙头当前最早空闲的时间,下一位同学总是接到最先空闲的龙头。
OJ: luogu
题目 ID: P1190
难度:普及-
标签:堆模拟
日期: 2026-06-19 01:24
题意
有 m 个水龙头,n 名同学按固定顺序接水,第 i 名同学需要 w_i 秒。
开始时前 m 名同学同时接水。以后哪个同学先接完,下一位排队同学就立刻补上这个位置。
问所有同学都接完水一共需要多少秒。
思路
先看一个可以直接验证想法的朴素解:
按秒模拟每个水龙头现在是谁、还剩多少秒。每过一秒,就把所有正在接水的同学剩余时间减一;谁变成 0,就立刻补上下一个同学。
cpp
// brute.cpp:按秒模拟每个水龙头当前还要接多少水。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 10005;
const int MAXM = 105;
int n, m;
int w[MAXN];
int tap[MAXM]; // tap[i] 表示第 i 个水龙头当前同学还要接多少水
int next_id; // 下一位还没安排的同学编号
bool all_done() {
if (next_id <= n) {
return false;
}
for (int i = 1; i <= m; i++) {
if (tap[i] > 0) {
return false;
}
}
return true;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m;
for (int i = 1; i <= n; i++) {
cin >> w[i];
}
next_id = 1;
for (int i = 1; i <= m && next_id <= n; i++) {
tap[i] = w[next_id];
next_id++;
}
int sec = 0;
while (!all_done()) {
sec++;
for (int i = 1; i <= m; i++) {
if (tap[i] > 0) {
tap[i]--;
}
}
for (int i = 1; i <= m; i++) {
if (tap[i] == 0 && next_id <= n) {
tap[i] = w[next_id];
next_id++;
}
}
}
cout << sec << '\n';
return 0;
}这个做法很直观,但如果时间很长,会做大量“逐秒推进”的重复工作。
更好的想法是只关心“每个水龙头什么时候会空出来”。
把每个正在使用的水龙头看成一个完成时间:
- 初始前
m名同学的完成时间分别是w_1, w_2, ..., w_m - 之后每来一个新同学,都应该接到“最早空闲”的那个水龙头
所以可以用一个小根堆维护所有水龙头当前的完成时间:
- 先把前
m名同学的完成时间入堆 - 取出最小值
earliest,表示最早空闲的水龙头 - 新同学接上去后,它的新完成时间就是
earliest + w_i - 再把这个新完成时间放回堆中
所有同学处理完后,最大的完成时间就是答案。
代码
cpp
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 10005;
int n, m;
int w[MAXN];
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m;
for (int i = 1; i <= n; i++) {
cin >> w[i];
}
priority_queue<int, vector<int>, greater<int> > q;
int ans = 0;
for (int i = 1; i <= m; i++) {
q.push(w[i]);
ans = max(ans, w[i]);
}
for (int i = m + 1; i <= n; i++) {
int earliest = q.top();
q.pop();
q.push(earliest + w[i]);
ans = max(ans, earliest + w[i]);
}
cout << ans << '\n';
return 0;
}复杂度
时间复杂度是
总结
这题的核心不是逐秒模拟,而是抓住“谁最早空闲”这个信息。
只维护每个水龙头的完成时间,就能把过程压缩到事件级别。