[NOIP 2010 普及组] 接水问题

GitHub跳转原题关系图返回列表

用小根堆维护每个水龙头当前最早空闲的时间,下一位同学总是接到最先空闲的龙头。

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
  • 之后每来一个新同学,都应该接到“最早空闲”的那个水龙头

所以可以用一个小根堆维护所有水龙头当前的完成时间:

  1. 先把前 m 名同学的完成时间入堆
  2. 取出最小值 earliest,表示最早空闲的水龙头
  3. 新同学接上去后,它的新完成时间就是 earliest + w_i
  4. 再把这个新完成时间放回堆中

所有同学处理完后,最大的完成时间就是答案。

代码

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;
}

复杂度

时间复杂度是 O(nlogm)O(n log m),空间复杂度是 O(m)O(m)

总结

这题的核心不是逐秒模拟,而是抓住“谁最早空闲”这个信息。

只维护每个水龙头的完成时间,就能把过程压缩到事件级别。