贪心算法实战:如何用C++解决洛谷P1223排队接水问题(附完整代码)
贪心算法实战:如何用C++解决洛谷P1223排队接水问题(附完整代码)
在算法竞赛和编程学习中,贪心算法因其直观性和高效性而备受青睐。排队接水问题作为经典的贪心算法应用场景,不仅能帮助我们理解贪心选择性质,还能锻炼实际问题建模能力。本文将深入探讨如何用C++高效解决洛谷P1223排队接水问题,从算法原理到代码实现,再到常见陷阱分析,为算法学习者提供全方位指导。
1. 问题分析与贪心策略构建
排队接水问题的核心是安排n个人的接水顺序,使得所有人的平均等待时间最小。乍看之下,这似乎需要尝试所有排列组合,但贪心算法可以提供更优解。
关键观察点:
- 当接水时间短的人先接水时,后续等待的人数更多,但每人等待的时间更短
- 接水时间长的人后接水时,影响的人数已经减少
通过数学归纳法可以证明:每次选择剩余人员中接水时间最短的人先接水,能够得到全局最优解。这种局部最优选择能导致全局最优的特性,正是贪心算法的精髓所在。
贪心选择性质的简单证明: 假设存在一个最优解,其中接水时间最短的人g不是第一个接水。那么将g与第一个接水的人交换位置,可以证明总等待时间会减少,这与"最优解"假设矛盾。因此,最优解必然包含贪心选择。
2. 数据结构设计与稳定排序
实现贪心策略需要合适的数据结构和排序方法。我们使用结构体存储每个人的编号和接水时间:
struct Person {
double time;
int index;
};
排序稳定性至关重要:题目要求接水时间相同时,保持输入顺序。因此必须使用稳定排序算法。C++中stable_sort正是为此设计:
bool compare(const Person &a, const Person &b) {
return a.time < b.time;
}
// 使用stable_sort保持相同元素的原始顺序
stable_sort(persons.begin(), persons.end(), compare);
为什么不用普通sort? 虽然在某些实现中sort可能保持稳定性,但这不是语言标准保证的行为。竞赛中必须使用stable_sort确保万无一失。
3. 两种等待时间计算方法对比
计算总等待时间有两种等效方法,各有优劣:
方法一:累积等待时间
double total_time = 0, current_wait = 0;
for(int i = 0; i < n; ++i) {
total_time += current_wait;
current_wait += persons[i].time;
}
特点:
- 直观易理解
- 需要额外变量记录当前累积时间
- 时间复杂度O(n)
方法二:贡献度计算
double total_time = 0;
for(int i = 0; i < n; ++i) {
total_time += persons[i].time * (n - i - 1);
}
特点:
- 数学表达更简洁
- 直接计算每个人对总时间的贡献
- 同样时间复杂度O(n)
提示:竞赛中推荐使用方法二,代码更简洁且不易出错。实际项目中方法一可能更易维护。
4. 完整代码实现与解析
下面给出两种风格的完整解决方案,包含详细注释和IO处理:
版本一:面向竞赛的紧凑写法
#include <bits/stdc++.h>
using namespace std;
struct Person { double t; int id; };
int main() {
int n; cin >> n;
vector<Person> v(n);
for(int i = 0; i < n; ++i) {
cin >> v[i].t;
v[i].id = i + 1;
}
stable_sort(v.begin(), v.end(), [](auto &a, auto &b) {
return a.t < b.t;
});
double sum = 0;
for(int i = 0; i < n; ++i) {
cout << v[i].id << " ";
sum += v[i].t * (n - i - 1);
}
cout << "\n" << fixed << setprecision(2) << sum / n;
return 0;
}
版本二:工程风格的清晰实现
#include <iostream>
#include <vector>
#include <algorithm>
#include <iomanip>
using namespace std;
struct Person {
double time;
int original_index;
// 重载小于运算符用于排序
bool operator<(const Person &other) const {
return time < other.time;
}
};
void solve_water_filling(int n) {
vector<Person> people(n);
// 输入处理
for(int i = 0; i < n; ++i) {
cin >> people[i].time;
people[i].original_index = i + 1;
}
// 稳定排序保持原始顺序
stable_sort(people.begin(), people.end());
// 输出排序结果并计算总等待时间
double total_waiting_time = 0;
for(int i = 0; i < n; ++i) {
cout << people[i].original_index << " ";
total_waiting_time += people[i].time * (n - i - 1);
}
// 输出平均等待时间,保留2位小数
cout << "\n" << fixed << setprecision(2)
<< total_waiting_time / n << "\n";
}
int main() {
int n;
cin >> n;
solve_water_filling(n);
return 0;
}
关键改进点:
- 使用vector替代原生数组,更安全
- 重载运算符使代码更易读
- 分离问题求解逻辑到独立函数
- 更详细的变量命名
5. 常见错误与调试技巧
即使理解了算法原理,实现时仍可能遇到各种问题。以下是典型错误案例:
错误1:忽略排序稳定性
// 错误代码:使用普通sort可能导致相同时间时顺序错误
sort(persons.begin(), persons.end(), compare);
错误2:等待时间计算错误
// 错误代码:错误计算了贡献人数
total_time += persons[i].time * (n - i); // 应该是n-i-1
错误3:输出格式不符
// 错误代码:未设置fixed导致精度显示问题
cout << setprecision(2) << average; // 需要添加fixed
调试建议:
- 对于小规模数据(n=3-5),手工计算验证
- 测试边界情况:所有人时间相同、时间递减排列等
- 使用
assert检查中间结果,如:assert(is_sorted(v.begin(), v.end(), [](auto &a, auto &b) { return a.t <= b.t; }));
6. 算法扩展与变种思考
掌握了基础解法后,可以思考问题的各种变种:
变种1:最小化最大等待时间
- 目标函数变化:需要不同的贪心策略
- 可能需要优先让长时间任务先执行
变种2:多水龙头情况
- 问题转化为多机调度
- 需要结合优先队列实现
变种3:带优先级排队
- 增加优先级权重
- 需要自定义更复杂的比较函数
// 多条件比较函数示例
bool compare_advanced(const Person &a, const Person &b) {
if(a.time != b.time) return a.time < b.time;
return a.priority > b.priority;
}
7. 性能分析与优化
虽然O(nlogn)的排序步骤主导了时间复杂度,但对于极端大规模数据仍有优化空间:
优化1:输入输出加速
ios::sync_with_stdio(false);
cin.tie(nullptr);
优化2:避免浮点运算
- 用整数计算最后再转换
- 减少除法运算次数
优化3:特定数据分布优化
- 对于小范围整数时间,可以使用计数排序
- 对于几乎有序的数据,考虑适应性排序算法
实际测试表明,在n=1e5量级时,基础实现能在100ms内完成,完全满足竞赛需求。过度优化往往得不偿失,清晰正确的代码更重要。
更多推荐


所有评论(0)