信息学奥赛接水问题:从暴力模拟到优先队列的三种解法深度剖析
如果你正在备战NOIP或类似的算法竞赛,那么“接水问题”这道经典题目一定不会陌生。它出现在洛谷P1190、信息学奥赛一本通1233等多个平台,是NOIP2010普及组的第二题。表面上看,这只是一个关于水龙头和学生接水的简单模拟题,但深入探究后你会发现,它其实是一个绝佳的算法思维训练案例——同一个问题,可以用完全不同的思路来解决,每种解法背后都对应着不同的时间复杂度和代码风格。
很多初学者拿到题目后,第一反应就是按照题意“一秒一秒”地模拟整个过程,这种思路直观但效率未必最优。实际上,这道题至少有三种主流的实现方式:基于循环查找的暴力模拟、使用结构体自定义优先级的优先队列以及仅存储时间的纯优先队列。每种方法都有其独特的适用场景和思维价值。对于竞赛选手来说,掌握多种解法不仅能在笔试或机试中灵活应对,更能深刻理解算法优化的本质——从O(nm)到O(nlogm)的跨越,往往就体现在数据结构的选择上。
这篇文章,我将从一个竞赛教练和多年选手的角度,带你彻底拆解这三种解法。我不会仅仅给出代码,而是会深入分析每种方法的设计思路、时间复杂度、空间开销、边界处理细节以及在实际比赛中的选用策略。无论你是刚开始接触信息学奥赛的新手,还是希望巩固基础、寻求突破的进阶选手,相信都能从中获得新的启发。
1. 问题重述与核心挑战
在深入解法之前,我们有必要先明确题目到底在问什么。很多同学失分不是因为算法不会,而是因为题意理解有偏差。
题目核心:有 m 个水龙头,n 个学生排队接水。每个水龙头每秒供水量为1。初始时,前 m 个学生各占一个水龙头同时开始接水。一旦某个学生接完(耗时等于他的接水量 w_i),队伍中的下一个学生立即接替该水龙头,中间没有停顿。求所有学生都接完水所需的总时间。
这里有几个关键点需要特别注意:
- “瞬间完成”意味着没有切换损耗:这是简化问题的关键,我们不需要考虑切换时间,只需要关注水龙头的“可用时刻”。
- 总时间由最晚结束的水龙头决定:因为所有水龙头同时开始,最后结束的那个水龙头的时间就是总耗时。
- m 可能小于、等于或大于 n:虽然题目给定 m ≤ n,但思考时应该考虑到更一般的情况。当 m ≥ n 时,所有学生同时开始,总时间就是接水量最大的那个学生的时间。
输入格式示例:
5 3
4 4 1 2 1
输出:4
这个例子中,3个水龙头,5个学生。手动模拟一下就会发现,第4秒结束时所有学生都接完了水。理解了这个过程,我们才能开始设计算法。
2. 解法一:循环查找最小值的暴力模拟
这是最直观、最容易想到的解法,也是很多同学在考场上第一时间会写出来的代码。它的核心思想是:维护一个数组 time[1..m],记录每个水龙头当前的“可用时刻”(即该水龙头接完当前学生后的时刻)。对于每一个学生,我们总是把他安排到当前可用时刻最早(即 time 值最小)的那个水龙头。
2.1 算法步骤与代码实现
让我们一步步拆解这个思路。
步骤分解:
- 初始化:读入 n, m 和每个学生的接水量数组 w[1..n]。创建数组
time[1..m],全部初始化为0(表示初始时所有水龙头都可用)。 - 安排前 m 个学生:实际上,在后续的统一循环中,这一步可以自然地处理。但为了理解,我们可以认为前 m 个学生分别占据了 m 个水龙头,
time[i] = w[i](i从1到m)。 - 安排剩余学生:对于第 i 个学生(i 从 m+1 到 n): a. 遍历
time数组,找到值最小的那个水龙头编号mni。 b. 将第 i 个学生安排到水龙头mni,更新time[mni] += w[i]。 - 计算总时间:所有学生安排完毕后,遍历
time数组,找到最大值,即为答案。
#include <iostream>
#include <algorithm>
using namespace std;
int main() {
int n, m;
int w[10005]; // 学生接水量,根据数据范围 n <= 10000
int time[105] = {0}; // 水龙头可用时刻,m <= 100
int maxTime = 0;
cin >> n >> m;
for (int i = 1; i <= n; ++i) {
cin >> w[i];
}
// 安排每一个学生
for (int i = 1; i <= n; ++i) {
// 1. 找到当前最早可用的水龙头
int minIndex = 1; // 假设第一个水龙头最早可用
for (int j = 2; j <= m; ++j) {
if (time[j] < time[minIndex]) {
minIndex = j;
}
}
// 2. 将学生i安排到这个水龙头
time[minIndex] += w[i];
}
// 找出所有水龙头中最晚结束的时间
for (int i = 1; i <= m; ++i) {
if (time[i] > maxTime) {
maxTime = time[i];
}
}
cout << maxTime << endl;
return 0;
}
2.2 时间复杂度与优劣分析
这种解法的优点非常明显:思路直接,代码简单,不易出错。对于刚接触算法竞赛的同学来说,能在短时间内写出正确代码就是胜利。
但它的缺点也同样突出:
- 时间复杂度高:外层循环遍历 n 个学生,内层循环每次都要遍历 m 个水龙头找最小值。因此总时间复杂度为 O(n × m)。
- 数据范围

460

被折叠的 条评论
为什么被折叠?



