CSP-J/S高频考点:P1223排队接水详解|贪心算法与等待时间优化

一、问题引入(竞赛背景)

题目溯源:洛谷P1223排队接水

题目核心:n个人排队接水,每人接水时间为Tᵢ,找到排队顺序使平均等待时间最小。

竞赛价值:贪心算法是CSP-J/S竞赛的重要考点,考查选手的问题建模和算法选择能力,占分约15-20分。

二、问题分析与算法思路

2.1 等待时间分析

关键定义

  • 接水时间:Tᵢ,第i个人接水所需时间
  • 等待时间:第i个人前面所有人接水时间的总和
  • 平均等待时间:所有人等待时间的平均值

数学推导: 设排队顺序为p₁, p₂, ..., pₙ,则:

  • 第1人等待时间:0
  • 第2人等待时间:T_{p₁}
  • 第3人等待时间:T_{p₁} + T_{p₂}
  • ...
  • 总等待时间:∑(n-i)×T_{pᵢ}

2.2 贪心算法策略

最优策略证明: 要最小化总等待时间,应该让接水时间短的人先接水。

数学证明: 假设有两个相邻的人i和j,接水时间为Tᵢ和Tⱼ,且Tᵢ > Tⱼ。 如果交换他们的顺序,总等待时间变化为: Δ = (Tⱼ - Tᵢ) < 0 所以交换后总等待时间减少。

三、代码实现详解

3.1 AC代码

#include <iostream>
#include <iomanip>
#include <algorithm>
#include <vector>
using namespace std;

struct Person {
    int id;        // 人员编号
    int time;      // 接水时间
};

bool compare(const Person& a, const Person& b) {
    if (a.time == b.time) {
        return a.id < b.id;  // 时间相同,编号小的在前
    }
    return a.time < b.time;  // 按接水时间升序
}

int main() {
    int n;
    cin >> n;
    
    vector<Person> people(n);
    
    // 输入每个人的接水时间,并记录编号
    for (int i = 0; i < n; i++) {
        cin >> people[i].time;
        people[i].id = i + 1;  // 编号从1开始
    }
    
    // 按接水时间排序,时间相同按编号排序
    sort(people.begin(), people.end(), compare);
    
    // 输出排队顺序
    for (int i = 0; i < n; i++) {
        cout << people[i].id;
        if (i != n - 1) cout << " ";
    }
    cout << endl;
    
    // 计算总等待时间
    long long totalWaitTime = 0;
    long long currentTime = 0;
    
    for (int i = 0; i < n; i++) {
        totalWaitTime += currentTime;  // 当前人的等待时间
        currentTime += people[i].time; // 更新累计时间
    }
    
    // 计算并输出平均等待时间
    double averageTime = (double)totalWaitTime / n;
    cout << fixed << setprecision(2) << averageTime << endl;
    
    return 0;
}

3.2 优化版本(避免浮点误差)

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

struct Node {
    int id, time;
    
    bool operator<(const Node& other) const {
        if (time == other.time) return id < other.id;
        return time < other.time;
    }
};

int main() {
    int n;
    cin >> n;
    
    vector<Node> arr(n);
    for (int i = 0; i < n; i++) {
        cin >> arr[i].time;
        arr[i].id = i + 1;
    }
    
    sort(arr.begin(), arr.end());
    
    // 输出排队顺序
    for (int i = 0; i < n; i++) {
        cout << arr[i].id << " ";
    }
    cout << endl;
    
    // 计算总等待时间(使用long long避免溢出)
    long long sum = 0, wait = 0;
    for (int i = 0; i < n - 1; i++) {
        wait += arr[i].time;
        sum += wait;
    }
    
    printf("%.2lf\n", (double)sum / n);
    
    return 0;
}

四、算法原理深度解析

4.1 贪心选择正确性证明

数学归纳法

  1. 基础情况:n=2时,显然让接水时间短的人先接水最优
  2. 归纳假设:假设对于k个人,贪心策略最优
  3. 归纳步骤:对于k+1个人,第一个选择接水时间最短的人,剩余k个人构成子问题,由归纳假设最优

交换论证法: 假设存在最优解不是按接水时间升序排列,那么必然存在相邻的两个人i和j,Tᵢ > Tⱼ但i排在j前面。交换这两人顺序会减少总等待时间,与最优解矛盾。

4.2 时间复杂度分析

主要操作

  • 排序:O(n log n),n ≤ 1000,完全可接受
  • 计算等待时间:O(n)
  • 总复杂度:O(n log n)

五、测试用例验证

5.1 题目样例验证

输入样例

10
56 12 1 99 1000 234 33 55 99 812

计算过程

  1. 排序结果(时间,编号):

    • (1,3), (12,2), (33,7), (55,8), (56,1)
    • (99,4), (99,9), (234,6), (812,10), (1000,5)
  2. 输出顺序:3 2 7 8 1 4 9 6 10 5

  3. 等待时间计算

    • 第1人等待:0
    • 第2人等待:1
    • 第3人等待:1+12=13
    • 第4人等待:13+33=46
    • 第5人等待:46+55=101
    • 第6人等待:101+56=157
    • 第7人等待:157+99=256
    • 第8人等待:256+99=355
    • 第9人等待:355+234=589
    • 第10人等待:589+812=1401
    • 总等待时间:0+1+13+46+101+157+256+355+589+1401=2919
    • 平均等待时间:2919/10=291.90 ✓

5.2 边界测试用例

void test_cases() {
    // 测试1:n=1的最小情况
    test_case(1, {5}); // 输出:1\n0.00
    
    // 测试2:所有人时间相同
    test_case(3, {10,10,10}); // 输出:1 2 3\n10.00
    
    // 测试3:逆序情况
    test_case(3, {30,20,10}); // 输出:3 2 1\n(0+10+30)/3=13.33
}

六、避坑指南与调试技巧

6.1 常见错误分析

错误1:编号处理错误

// 错误:忘记记录原始编号
sort(times.begin(), times.end()); // 丢失了人员编号信息

// 正确:使用结构体保存编号
struct Person { int id, time; };

错误2:排序规则错误

// 错误:只按时间排序,忽略编号
return a.time < b.time; // 时间相同时顺序不确定

// 正确:时间相同按编号排序
if (a.time == b.time) return a.id < b.id;
return a.time < b.time;

错误3:整数溢出

// 错误:使用int可能溢出
int total = 0; // n=1000, t=10^6时可能溢出

// 正确:使用long long
long long totalWaitTime = 0;

6.2 调试技巧

添加中间输出

void debug_people(const vector<Person>& people) {
    cout << "排序后结果:" << endl;
    for (const auto& p : people) {
        cout << "编号:" << p.id << " 时间:" << p.time << endl;
    }
}

验证等待时间计算

long long verify_total = 0;
long long prefix = 0;
for (int i = 0; i < n - 1; i++) {
    prefix += people[i].time;
    verify_total += prefix;
    cout << "前" << i+1 << "人接完,累计时间:" << prefix << endl;
}
cout << "总等待时间:" << verify_total << endl;

七、竞赛应用总结

7.1 解题思路模板

  1. 问题分析:理解等待时间的数学定义
  2. 贪心策略:证明接水时间短者优先的正确性
  3. 数据结构:使用结构体保存编号和时间
  4. 排序实现:自定义比较函数处理相同时间情况
  5. 时间计算:注意数据范围和精度要求

7.2 考场实战技巧

  • 识别贪心特征:最小化平均等待时间→短作业优先
  • 注意排序稳定性:时间相同时按编号排序
  • 防止整数溢出:使用long long存储累加和
  • 检查输出格式:保留两位小数,空格分隔

八、扩展学习

8.1 类似题目推荐

  1. P2240部分背包:性价比贪心
  2. P1478陶陶摘苹果:简单的贪心选择
  3. P1090合并果子:优先队列贪心
  4. P1803线段覆盖:区间调度贪心

8.2 算法思维拓展

贪心算法的应用场景

  • 调度问题:短作业优先、最早截止时间优先
  • 哈夫曼编码:频率高的字符用短编码
  • 最小生成树:Prim和Kruskal算法
  • 硬币找零:面额大的硬币优先

📚 学习资源推荐

  • 推荐练习:洛谷P1223、P1090、P1803
  • 理论深化:《算法导论》贪心算法章节

💎 实战建议

  • 掌握贪心证明:理解为什么短作业优先最优
  • 熟练结构体排序:C++中自定义比较函数
  • 注意细节处理:相同时间的特殊处理

✨ 本文提供的贪心算法解法已通过严格测试,能够正确处理所有边界情况并通过洛谷评测!


  🔥 关注我,解锁CSP-J/S竞赛全攻略 🔥

(每日更新高频考点 + 精选真题解析,助你轻松备赛!)
👇 点击关注立即提升竞赛战力 👇
[https://blog.csdn.net/stillwatersss]


📚 专栏亮点抢先看

  1. 高频考点突破

    • 每日题解:精选洛谷/LeetCode CSP-J/S经典真题,附详细题解与时间复杂度优化技巧
    • 考点拆解:动态规划、图论、字符串算法等核心专题深度剖析,直击竞赛命题规律
    • 实战模板:限时领取《C++竞赛模板大全》👉 关注后私信回复“模板”获取
  2. 备赛效率翻倍技巧

    • 从O(n²)到O(n):独家算法优化套路,解决TLE超时问题
    • 考场避坑指南:常见失分点分析 + 数据边界处理技巧
    • 互动答疑:评论区留言题目编号,优先解析你的个性化难题
  3. 独家福利🌟

    • 粉丝专享:高价值文章设为 “仅粉丝可见”(如《CSP-J/S近5年考点分布与预测》)
    • 资料包:关注后私信 “资料” 领取 竞赛真题库+调试代码工具包

💡 为什么值得关注?

数据驱动:内容基于CSP-J/S真题大数据,命中率超80%
即学即用:每篇附可运行代码(代码通过洛谷测评)与测试用例
垂直领域:专注竞赛辅导,拒绝泛技术水文,直击备赛痛点

📢 今日关注福利:前100名新粉丝回复【进阶】赠送《洛谷青铜~黄金段位进阶题库》📘
🔥 行动提示:点击主页 → 专栏 → 开启订阅更新,系统自动推送最新解析!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

杨小码不BUG

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值