1. 项目背景与问题解析
排队接水问题(Luogu-P1223)是GESP(青少年编程能力等级考试)C++四/五级中的经典贪心算法练习题。题目描述如下:有n个人在一个水龙头前排队接水,每个人接水所需的时间为Ti(i=1,2,...,n)。如何安排这n个人的接水顺序,才能使所有人的平均等待时间最小?
这个问题看似简单,但蕴含着重要的算法思想。在实际生活中,类似的场景比比皆是——银行窗口排队、CPU任务调度、物流配送路线规划等。理解并掌握这个问题的解法,不仅能帮助通过GESP考试,更能培养解决实际问题的计算思维。
2. 贪心算法原理剖析
2.1 贪心算法的基本思想
贪心算法(Greedy Algorithm)是一种在每一步选择中都采取当前状态下最优(即最有利)的选择,从而希望导致结果是全局最优的算法策略。它不像动态规划那样考虑所有可能的子问题,而是做出局部最优选择,期望这些局部最优能叠加成全局最优。
对于排队接水问题,贪心策略表现为:让接水时间短的人先接水。这样后面等待的人需要等待的时间总和会最小化。
2.2 为什么贪心策略有效
我们可以用数学归纳法证明这个策略的正确性:
- 当n=1时,显然成立
- 假设对n=k成立,考虑n=k+1的情况
- 如果存在一个更优的排列,其中某个接水时间较长的人排在接水时间较短的人前面,交换这两个人的位置一定能减少总等待时间
这个证明表明,任何不按照接水时间从短到长排列的顺序,都可以通过交换相邻逆序对来进一步减少总等待时间。
3. 算法实现详解
3.1 数据结构选择
我们需要存储每个人的编号和接水时间,适合使用结构体数组或vector:
cpp复制struct Person {
int id; // 人员编号
int time; // 接水时间
};
3.2 排序算法实现
使用C++标准库的sort函数,需要自定义比较规则:
cpp复制bool compare(const Person &a, const Person &b) {
return a.time < b.time; // 按接水时间升序排列
}
// 使用方式
vector<Person> people;
sort(people.begin(), people.end(), compare);
3.3 完整代码实现
cpp复制#include <iostream>
#include <vector>
#include <algorithm>
#include <iomanip>
using namespace std;
struct Person {
int id;
int time;
};
bool compare(const Person &a, const Person &b) {
return a.time < b.time;
}
int main() {
int n;
cin >> n;
vector<Person> people(n);
for(int i = 0; i < n; ++i) {
people[i].id = i + 1;
cin >> people[i].time;
}
sort(people.begin(), people.end(), compare);
double total_wait = 0;
double current_wait = 0;
for(int i = 0; i < n; ++i) {
if(i > 0) {
cout << " ";
}
cout << people[i].id;
if(i > 0) {
current_wait += people[i-1].time;
total_wait += current_wait;
}
}
cout << endl;
cout << fixed << setprecision(2) << total_wait / n << endl;
return 0;
}
4. 关键点解析与优化
4.1 时间复杂度分析
- 排序阶段:使用快速排序平均时间复杂度为O(nlogn)
- 计算等待时间阶段:O(n)
- 总体时间复杂度:O(nlogn)
这是该问题能达到的最优时间复杂度,因为排序本身的下限就是O(nlogn)。
4.2 空间复杂度分析
- 存储人员信息:O(n)
- 排序使用的栈空间:O(logn)(快速排序的递归深度)
- 总体空间复杂度:O(n)
4.3 输出格式注意事项
题目要求:
- 输出排列顺序,编号间用空格隔开
- 输出平均等待时间,保留两位小数
特别注意:
- 第一个输出的编号前不应有空格
- 使用fixed和setprecision(2)保证小数位数
5. 常见错误与调试技巧
5.1 典型错误案例
- 排序规则错误:将比较函数写成
return a.time > b.time,导致长作业优先 - 等待时间计算错误:忘记累加前人的接水时间
- 输出格式错误:编号间漏掉空格或多了空格,小数位数不正确
5.2 调试建议
- 使用小规模测试数据手工验证
code复制输入: 3 10 20 30 预期输出: 1 2 3 10.00 - 检查边界条件
- n=1的情况
- 所有人接水时间相同的情况
- 使用调试器观察排序后的数组顺序
5.3 测试用例设计
提供几组测试用例帮助验证程序正确性:
| 输入 | 预期输出 | 说明 |
|---|---|---|
| 3 10 20 30 |
1 2 3 10.00 |
基本测试 |
| 5 5 1 8 3 2 |
2 5 4 1 3 4.20 |
乱序测试 |
| 1 50 |
1 0.00 |
边界测试 |
| 4 10 10 10 10 |
1 2 3 4 15.00 |
相同时间测试 |
6. 算法扩展与应用
6.1 多水龙头情况
如果题目变为有m个水龙头,如何安排使平均等待时间最小?这就变成了更复杂的调度问题,可以使用优先队列(堆)来实现:
- 仍然按接水时间从短到长排序
- 维护一个大小为m的最小堆,记录每个水龙头当前的总接水时间
- 每次将当前人员分配给最早空闲的水龙头
6.2 实际应用场景
- CPU调度:短作业优先(SJF)调度算法
- 服务窗口排队:银行、医院等服务系统的排队优化
- 物流配送:合理安排配送顺序减少总等待时间
- 生产线调度:优化工序安排提高生产效率
6.3 相关题目推荐
- Luogu-P1090 合并果子(贪心+优先队列)
- Luogu-P1803 凌乱的yyy(区间调度问题)
- Luogu-P2240 部分背包问题(分数背包)
7. 学习建议与备考技巧
7.1 GESP备考建议
- 掌握基础算法:排序、查找、贪心、简单动态规划
- 熟练使用STL:vector、sort、priority_queue等
- 注重编码规范:变量命名、代码缩进、注释清晰
- 练习调试技巧:学会使用断点调试和输出调试
7.2 贪心算法学习路径
- 理解贪心选择性质和最优子结构
- 从简单题目入手(如找零钱、区间调度)
- 逐步挑战更复杂的问题(如哈夫曼编码、最小生成树)
- 学会证明贪心策略的正确性
7.3 推荐学习资源
- 《算法导论》贪心算法章节
- OI Wiki贪心算法专题
- LeetCode贪心算法标签题目
- GESP官方样题和历年真题
在实际编码中,我发现使用结构体存储人员信息比分开存储编号和时间更不易出错。另外,在计算等待时间时,使用一个累加变量比每次都重新计算前i-1个人的时间和更高效。对于GESP考试,建议先确保基础题的正确率,再挑战更难的题目。
