1. 问题背景与需求分析
排队接水问题(P1223)是算法竞赛和编程练习中的经典贪心算法案例。题目场景通常描述为:有n个人在一个水龙头前排队接水,每个人接水所需时间已知,如何安排他们的排队顺序,才能使所有人的平均等待时间最小?
这个问题的现实映射非常广泛:从超市收银台排队策略,到CPU任务调度优化,再到疫情期间的核酸检测顺序安排,本质上都是同一类问题。我在实际刷题过程中发现,这类问题虽然表面简单,但蕴含着深刻的算法设计思想。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 贪心算法核心思想解析
2.1 贪心选择性质证明
正确的贪心策略是将接水时间短的人优先安排。这需要证明两个关键点:
- 最优子结构性质:问题的最优解包含子问题的最优解
- 贪心选择性质:局部最优选择能导致全局最优解
数学证明如下:
设存在两个相邻的人i和j,其中t_i > t_j。交换他们的顺序后:
- 原等待时间总和贡献:S + (n-k)t_i + (n-k-1)t_j
- 交换后等待时间总和贡献:S + (n-k)t_j + (n-k-1)t_j
两者差值为t_i - t_j > 0,说明交换后总等待时间更小。
2.2 算法实现步骤
- 输入n个人的接水时间数组times[]
- 对times[]进行升序排序
- 计算前缀和得到各人等待时间
- 累加所有等待时间求平均值
关键代码实现(C++):
cpp复制sort(times.begin(), times.end());
double total = 0;
for(int i=0; i<n; ++i){
total += times[i] * (n-i-1);
}
double average = total / n;
3. 算法优化与边界处理
3.1 时间复杂度优化
标准排序算法时间复杂度为O(nlogn),这是该问题的时间下限。但在特殊情况下:
- 当时间范围有限时(如0<=t_i<=1000),可以使用计数排序将复杂度降至O(n)
- 使用优先队列(堆)实现部分排序
3.2 数值精度处理
特别注意题目要求的输出精度:
- 使用double类型存储累加和
- 输出时控制小数点位数(如printf("%.2lf", average))
- 避免整数相除导致的精度丢失
4. 同类问题扩展与变种
4.1 多水龙头情况(P1233)
当有k个水龙头并行工作时,问题变为:
- 维护k个水龙头的当前结束时间
- 每次选择最早空闲的水龙头分配任务
- 使用优先队列优化选择过程
4.2 带权重的情况
如果每个人有不同的权重w_i,则优化目标变为最小化Σw_i*T_i。此时:
- 贪心策略应按照t_i/w_i的比例排序
- 需要重新证明贪心选择性质
5. 实战注意事项
-
输入规模判断:
- 小规模数据(n<1e4)可用任意排序
- 大规模数据(n>1e6)需考虑IO优化
-
特殊测试用例:
- 所有人接水时间相同
- 极值情况(如n=1)
- 接水时间为0的情况
-
调试技巧:
- 打印中间排序结果
- 验证小规模案例手工计算
- 使用assert检查前提条件
6. 刷题心得与进阶路线
在实际刷题过程中,建议按照以下路线深化理解:
- 基础:P1223(本问题)
- 进阶:P1233(多水龙头)
- 变种:P1245(带权重)
- 综合:P1289(结合动态规划)
我个人的经验是,贪心类问题最重要的是培养"问题转化"能力。当遇到新问题时,要能识别出它是否属于排队接水问题的变种。例如华为OD机试中的任务调度问题,本质上就是多水龙头情况的扩展。
