信息学奥赛必备:接水问题三种解法对比(循环/结构体优先队列/纯优先队列)

信息学奥赛接水问题:从暴力模拟到优先队列的三种解法深度剖析

如果你正在备战NOIP或类似的算法竞赛,那么“接水问题”这道经典题目一定不会陌生。它出现在洛谷P1190、信息学奥赛一本通1233等多个平台,是NOIP2010普及组的第二题。表面上看,这只是一个关于水龙头和学生接水的简单模拟题,但深入探究后你会发现,它其实是一个绝佳的算法思维训练案例——同一个问题,可以用完全不同的思路来解决,每种解法背后都对应着不同的时间复杂度和代码风格。

很多初学者拿到题目后,第一反应就是按照题意“一秒一秒”地模拟整个过程,这种思路直观但效率未必最优。实际上,这道题至少有三种主流的实现方式:基于循环查找的暴力模拟使用结构体自定义优先级的优先队列以及仅存储时间的纯优先队列。每种方法都有其独特的适用场景和思维价值。对于竞赛选手来说,掌握多种解法不仅能在笔试或机试中灵活应对,更能深刻理解算法优化的本质——从O(nm)到O(nlogm)的跨越,往往就体现在数据结构的选择上。

这篇文章,我将从一个竞赛教练和多年选手的角度,带你彻底拆解这三种解法。我不会仅仅给出代码,而是会深入分析每种方法的设计思路、时间复杂度、空间开销、边界处理细节以及在实际比赛中的选用策略。无论你是刚开始接触信息学奥赛的新手,还是希望巩固基础、寻求突破的进阶选手,相信都能从中获得新的启发。

1. 问题重述与核心挑战

在深入解法之前,我们有必要先明确题目到底在问什么。很多同学失分不是因为算法不会,而是因为题意理解有偏差。

题目核心:有 m 个水龙头,n 个学生排队接水。每个水龙头每秒供水量为1。初始时,前 m 个学生各占一个水龙头同时开始接水。一旦某个学生接完(耗时等于他的接水量 w_i),队伍中的下一个学生立即接替该水龙头,中间没有停顿。求所有学生都接完水所需的总时间。

这里有几个关键点需要特别注意:

  1. “瞬间完成”意味着没有切换损耗:这是简化问题的关键,我们不需要考虑切换时间,只需要关注水龙头的“可用时刻”。
  2. 总时间由最晚结束的水龙头决定:因为所有水龙头同时开始,最后结束的那个水龙头的时间就是总耗时。
  3. m 可能小于、等于或大于 n:虽然题目给定 m ≤ n,但思考时应该考虑到更一般的情况。当 m ≥ n 时,所有学生同时开始,总时间就是接水量最大的那个学生的时间。

输入格式示例

5 3
4 4 1 2 1

输出:4

这个例子中,3个水龙头,5个学生。手动模拟一下就会发现,第4秒结束时所有学生都接完了水。理解了这个过程,我们才能开始设计算法。

2. 解法一:循环查找最小值的暴力模拟

这是最直观、最容易想到的解法,也是很多同学在考场上第一时间会写出来的代码。它的核心思想是:维护一个数组 time[1..m],记录每个水龙头当前的“可用时刻”(即该水龙头接完当前学生后的时刻)。对于每一个学生,我们总是把他安排到当前可用时刻最早(即 time 值最小)的那个水龙头

2.1 算法步骤与代码实现

让我们一步步拆解这个思路。

步骤分解

  1. 初始化:读入 n, m 和每个学生的接水量数组 w[1..n]。创建数组 time[1..m],全部初始化为0(表示初始时所有水龙头都可用)。
  2. 安排前 m 个学生:实际上,在后续的统一循环中,这一步可以自然地处理。但为了理解,我们可以认为前 m 个学生分别占据了 m 个水龙头,time[i] = w[i](i从1到m)。
  3. 安排剩余学生:对于第 i 个学生(i 从 m+1 到 n): a. 遍历 time 数组,找到值最小的那个水龙头编号 mni。 b. 将第 i 个学生安排到水龙头 mni,更新 time[mni] += w[i]
  4. 计算总时间:所有学生安排完毕后,遍历 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)
  • 数据范围
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值