题解:洛谷 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
【核心思想】
-
问题分析:有 \(n\) 名同学按固定顺序排队,\(m\) 个水龙头同时供水,每个龙头每秒供水量为 \(1\)。第 \(i\) 号同学需要接水 \(w_i\) 秒。开始时前 \(m\) 名同学同时接水,每当有人完成时,排队中的下一位同学立刻接替。求所有同学接完水所需的总时间。这是一个固定顺序的多机调度模拟问题,由于顺序固定且所有龙头效率相同,最优策略(也是唯一策略)是始终让当前空闲的龙头服务等待队列中的下一位同学。
-
算法选择:
- 贪心模拟(累计时间法):维护每个水龙头的“当前总工作时间”数组 \(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)\)。
-
关键步骤:
- 读入 \(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\)。
-
时间/空间复杂度:
- 时间复杂度:\(O(n \times m)\),若使用优先队列可优化至 \(O(n \log m)\)。
- 空间复杂度:\(O(n + m)\),存储接水量和龙头累计时间。
-
贪心模拟的核心思想:
- 固定顺序的必然性:同学按编号顺序排队,不可调整,因此模拟过程是确定的,只需正确模拟“谁接替谁”的逻辑。
- 负载均衡视角:将每个水龙头视为一个“处理器”,每个同学是一个“任务”,任务按固定顺序到达,分配到当前负载最小的处理器上,可使总完成时间最小。
- 等价性:每次将新同学分配给当前累计时间最短的龙头,等价于模拟真实接水过程中“最早空闲的龙头接收下一位同学”的过程。
- 适用场景:适用于多台相同机器、任务顺序固定、处理时间已知的调度模拟问题。
【算法标签】
普及- #贪心
【代码详解】
#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
浙公网安备 33010602011771号