题解:洛谷 P1190 接水问题

【题目来源】

洛谷:P1190 [NOIP 2010 普及组] 接水问题 - 洛谷

【题目描述】

学校里有一个水房,水房里一共装有 \(m\) 个龙头可供同学们打开水,每个龙头每秒钟的供水量相等,均为 \(1\)

现在有 \(n\) 名同学准备接水,他们的初始接水顺序已经确定。将这些同学按接水顺序从 \(1\)\(n\) 编号,\(i\) 号同学的接水量为 \(w_i\)。接水开始时,\(1\)\(m\) 号同学各占一个水龙头,并同时打开水龙头接水。当其中某名同学 \(j\) 完成其接水量要求 \(w_j\) 后,下一名排队等候接水的同学 \(k\) 马上接替 \(j\) 同学的位置开始接水。这个换人的过程是瞬间完成的,且没有任何水的浪费。即 \(j\) 同学第 \(x\) 秒结束时完成接水,则 \(k\) 同学第 \(x+1\) 秒立刻开始接水。若当前接水人数 \(n'\) 不足 \(m\),则只有 \(n'\) 个龙头供水,其它 \(m−n'\) 个龙头关闭。

现在给出 \(n\) 名同学的接水量,按照上述接水规则,问所有同学都接完水需要多少秒。

【输入】

第一行两个整数 \(n\)\(m\),用一个空格隔开,分别表示接水人数和龙头个数。

第二行 \(n\) 个整数 \(w_1,w_2,…,w_n\),每两个整数之间用一个空格隔开,\(w_i\) 表示 \(i\) 号同学的接水量。

【输出】

一个整数,表示接水所需的总时间。

【输入样例】

5 3
4 4 1 2 1

【输出样例】

4

【核心思想】

  1. 问题分析:有 \(n\) 名同学按固定顺序排队,\(m\) 个水龙头同时供水,每个龙头每秒供水量为 \(1\)。第 \(i\) 号同学需要接水 \(w_i\) 秒。开始时前 \(m\) 名同学同时接水,每当有人完成时,排队中的下一位同学立刻接替。求所有同学接完水所需的总时间。这是一个固定顺序的多机调度模拟问题,由于顺序固定且所有龙头效率相同,最优策略(也是唯一策略)是始终让当前空闲的龙头服务等待队列中的下一位同学。

  2. 算法选择

    • 贪心模拟(累计时间法):维护每个水龙头的“当前总工作时间”数组 \(s[1..m]\),初始为 \(0\)。按顺序遍历 \(n\) 名同学,将第 \(i\) 名同学分配给当前总时间最小的水龙头,即 \(s[t] += w_i\),其中 \(t = \arg\min s[j]\)。最终所有同学接完水的时间就是 \(\max_{j=1..m} s[j]\)
    • 正确性依据:由于接水顺序固定,且每个同学必须被安排到某个水龙头,最优安排是让最早空闲的水龙头接替下一位同学,这等价于将每个新任务分配给当前累计时间最短的龙头(负载均衡)。
    • 复杂度:每次分配需遍历 \(m\) 个龙头找最小值,总时间复杂度 \(O(n \times m)\)\(n, m \le 10000\),可接受。若 \(m\) 较大可用优先队列优化至 \(O(n \log m)\)
  3. 关键步骤

    • 读入 \(n, m\) 以及每个同学的接水量 \(w[1..n]\)
    • 初始化数组 \(s[1..m] = 0\),表示每个水龙头的累计工作时间。
    • 遍历 \(i\)\(1\)\(n\)
      • \(s[1..m]\) 中找到最小值对应的下标 \(t\)
      • 将第 \(i\) 名同学分配给水龙头 \(t\)\(s[t] += w[i]\)
    • 遍历结束后,答案 \(ans = \max(s[1..m])\),即为所有同学接完水的总时间。
    • 输出 \(ans\)
  4. 时间/空间复杂度

    • 时间复杂度:\(O(n \times m)\),若使用优先队列可优化至 \(O(n \log m)\)
    • 空间复杂度:\(O(n + m)\),存储接水量和龙头累计时间。
  5. 贪心模拟的核心思想

    • 固定顺序的必然性:同学按编号顺序排队,不可调整,因此模拟过程是确定的,只需正确模拟“谁接替谁”的逻辑。
    • 负载均衡视角:将每个水龙头视为一个“处理器”,每个同学是一个“任务”,任务按固定顺序到达,分配到当前负载最小的处理器上,可使总完成时间最小。
    • 等价性:每次将新同学分配给当前累计时间最短的龙头,等价于模拟真实接水过程中“最早空闲的龙头接收下一位同学”的过程。
    • 适用场景:适用于多台相同机器、任务顺序固定、处理时间已知的调度模拟问题。

【算法标签】

普及- #贪心

【代码详解】

#include <bits/stdc++.h>
using namespace std;

int main()
{
    // 输入n和m,n表示任务数量,m表示水龙头的数量
    int n, m;
    cin >> n >> m;

    // w数组存储每个任务的工作量(需要的时间单位数)
    int w[n+5];
    for (int i=1; i<=n; i++) cin >> w[i];

    // lt数组表示每个水龙头当前正在处理的任务编号
    // 初始时,水龙头j处理任务j(1<=j<=m)
    int lt[m+5];
    for (int j=1; j<=m; j++) lt[j] = j;

    // time记录总时间,wait表示下一个待分配的任务编号
    long long time = 0;
    int wait = m+1;

    // 主循环,模拟时间流逝
    while (true) {
        time++;  // 时间增加1个单位

        // 遍历所有水龙头
        for (int j=1; j<=m; j++) {
            // 如果水龙头j没有任务(lt[j]<0),跳过
            if (lt[j]<0) continue;

            // 水龙头j处理当前任务1个单位时间
            w[lt[j]]--;

            // 如果当前任务完成
            if (w[lt[j]]==0) {
                // 给水龙头j分配下一个任务
                lt[j] = wait;
                wait++;

                // 如果所有任务都已分配,标记水龙头j为空闲
                if (wait>n+1) lt[j]=-1;
            }
        }

        // 如果还有未分配的任务,继续下一轮处理
        if (wait<=n) continue;

        // 检查所有任务是否都已完成
        bool finish=true;
        for (int i=1; i<=n; i++) {
            if (w[i]>0) {
                finish = false;
                break;
            }
        }

        // 如果所有任务都已完成,退出循环
        if (finish) break;
    }

    // 输出总时间
    cout << time;
    return 0;
}
#include <bits/stdc++.h>
using namespace std;
int n, m;
int s[105];  // 每个水龙头的出水量(当前总时间)
int w[10005];  // 每人的接水量

int main()
{
    cin >> n >> m;  // 输入人数和水龙头数
    for (int i = 1; i <= n; i++)  // 输入每个人的接水量
    {
        cin >> w[i];
    }
    for (int i = 1; i <= n; i++)  // 为每个人分配水龙头
    {
        int t = 1;  // 选择当前总时间最小的水龙头
        for (int j = 2; j <= m; j++)
        {
            if (s[t] > s[j])  // 找到更小的
            {
                t = j;
            }
        }
        s[t] += w[i];  // 将当前人分配到水龙头t
    }
    int mx = 0;
    for (int i = 1; i <= m; i++)  // 找出最晚结束的水龙头
    {
        mx = max(mx, s[i]);
    }
    cout << mx;  // 输出总接水时间
    return 0;
}

【运行结果】

5 3
4 4 1 2 1
4
posted @ 2026-09-02 22:51  团爸讲算法  阅读(19)  评论(0)    收藏  举报