
一、问题引入(竞赛背景)
题目溯源:洛谷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 贪心选择正确性证明
数学归纳法:
- 基础情况:n=2时,显然让接水时间短的人先接水最优
- 归纳假设:假设对于k个人,贪心策略最优
- 归纳步骤:对于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,3), (12,2), (33,7), (55,8), (56,1)
- (99,4), (99,9), (234,6), (812,10), (1000,5)
-
输出顺序:3 2 7 8 1 4 9 6 10 5
-
等待时间计算:
- 第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 解题思路模板
- 问题分析:理解等待时间的数学定义
- 贪心策略:证明接水时间短者优先的正确性
- 数据结构:使用结构体保存编号和时间
- 排序实现:自定义比较函数处理相同时间情况
- 时间计算:注意数据范围和精度要求
7.2 考场实战技巧
- 识别贪心特征:最小化平均等待时间→短作业优先
- 注意排序稳定性:时间相同时按编号排序
- 防止整数溢出:使用long long存储累加和
- 检查输出格式:保留两位小数,空格分隔
八、扩展学习
8.1 类似题目推荐
- P2240部分背包:性价比贪心
- P1478陶陶摘苹果:简单的贪心选择
- P1090合并果子:优先队列贪心
- P1803线段覆盖:区间调度贪心
8.2 算法思维拓展
贪心算法的应用场景:
- 调度问题:短作业优先、最早截止时间优先
- 哈夫曼编码:频率高的字符用短编码
- 最小生成树:Prim和Kruskal算法
- 硬币找零:面额大的硬币优先
📚 学习资源推荐
- 推荐练习:洛谷P1223、P1090、P1803
- 理论深化:《算法导论》贪心算法章节
💎 实战建议
- 掌握贪心证明:理解为什么短作业优先最优
- 熟练结构体排序:C++中自定义比较函数
- 注意细节处理:相同时间的特殊处理
✨ 本文提供的贪心算法解法已通过严格测试,能够正确处理所有边界情况并通过洛谷评测!
🔥 关注我,解锁CSP-J/S竞赛全攻略 🔥
(每日更新高频考点 + 精选真题解析,助你轻松备赛!)
👇 点击关注 → 立即提升竞赛战力 👇
[https://blog.csdn.net/stillwatersss]
📚 专栏亮点抢先看
-
高频考点突破
- 每日题解:精选洛谷/LeetCode CSP-J/S经典真题,附详细题解与时间复杂度优化技巧
- 考点拆解:动态规划、图论、字符串算法等核心专题深度剖析,直击竞赛命题规律
- 实战模板:限时领取《C++竞赛模板大全》👉 关注后私信回复“模板”获取
-
备赛效率翻倍技巧
- 从O(n²)到O(n):独家算法优化套路,解决TLE超时问题
- 考场避坑指南:常见失分点分析 + 数据边界处理技巧
- 互动答疑:评论区留言题目编号,优先解析你的个性化难题
-
独家福利🌟
- 粉丝专享:高价值文章设为 “仅粉丝可见”(如《CSP-J/S近5年考点分布与预测》)
- 资料包:关注后私信 “资料” 领取 竞赛真题库+调试代码工具包
💡 为什么值得关注?
✅ 数据驱动:内容基于CSP-J/S真题大数据,命中率超80%
✅ 即学即用:每篇附可运行代码(代码通过洛谷测评)与测试用例
✅ 垂直领域:专注竞赛辅导,拒绝泛技术水文,直击备赛痛点
📢 今日关注福利:前100名新粉丝回复【进阶】赠送《洛谷青铜~黄金段位进阶题库》📘
🔥 行动提示:点击主页 → 专栏 → 开启订阅更新,系统自动推送最新解析!
588

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



