news 2026/9/24 21:40:33

P1223排队接水:贪心算法入门与短作业优先实现解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
P1223排队接水:贪心算法入门与短作业优先实现解析

前两天刷题群里有人发了条链接,问P1223 排队接水有没有什么通俗易懂的讲法。我当时回了句:这题你只要抓住一句话——让接水快的人先上,所有排队的人的总等待时间就越少。就是这么个直觉,但真正把它讲清楚、写对,还得拆开揉碎了看。

P1223 排队接水是很多人入门贪心算法的第一道题。题目描述很生活化:n个人在一个水龙头前排队接水,每个人接水耗费的时间已知,要求你给出一个排队顺序,让所有人的平均等待时间最短,并输出这个排队顺序以及对应的平均等待时间。题面看着简单,但里面藏着三个关键点:一是“等待时间”的定义,二是为什么贪心排序有效,三是怎么在排序后还能正确输出每个人的原始编号。这篇文章从题意、证明、代码实现到易错点,完整过一遍,希望能让刚接触贪心算法的朋友少走点弯路。

1. 先搞懂题在问什么:题意拆解与贪心直觉

1.1 “等待时间”到底算到哪一刻

第一次做这题的人,十有八九会栽在这个概念上。题目里的等待时间,指的是每个人从排在队伍开始,到轮到自己接水之前这段干等的时间,不包括自己接水的时间

拿样例说明。n=4,四个人接水时间分别是1、2、3、4。如果按1、2、3、4的顺序排队:

  • 第1个人等待0分钟,接水1分钟;
  • 第2个人等了1分钟,接水2分钟;
  • 第3个人等了1+2=3分钟,接水3分钟;
  • 第4个人等了1+2+3=6分钟,接水4分钟。

所以总等待时间 = 0 + 1 + 3 + 6 = 10,平均等待时间 = 10 / 4 = 2.50。这个2.50就是题目样例输出。

如果你把“等待时间”理解成“从开始排队到接完水的总耗时”,那算出来的平均时间是 (1+3+6+10)/4 = 5.00,那就和题目的2.50对不上了。所以动手写程序之前,先把这个概念钉死:累加等待时间时,只累加轮到自己之前别人已用掉的时间

这样题意就清晰了:平均等待时间最小,等价于总等待时间最小,也就是要安排一种顺序,让“每个位置之前所有人的接水时间之和”的总和尽可能小。

1.2 “短作业优先”的直觉到底怎么来的

这个结论其实大家生活里都有体感。食堂打饭,如果前面一个人磨磨蹭蹭点半天菜,后面所有人都在心里骂;超市结账,收银员一看有人购物车里堆成山,经常会引导只拿一瓶水的顾客去另一个窗口。让耗时短的人先办事,后面的人排队时间自然就短。

但这个直觉要严谨化,得靠经典的交换论证法,也叫邻项交换法。假设当前排队顺序里有两段:前面某些人的接水总时间我们已经确定了,叫S;紧接着是两个人A和B,他们接水时间分别是a和b;再往后还有一批人。

先看A在前、B在后这种顺序:

  • A接水时,B要等a分钟,B后面的所有人也要多等a分钟;
  • B接水时,B后面的人还要多等b分钟。

所以A和B这两个相邻位置,对总等待时间的贡献,主要体现在两个点上:

  • 如果A先接水,B以及B后面的人都会因为A的时间a而等待;A自己前面的人已经确定了,不受影响。
  • 如果B先接水,A以及A后面的人都会因为B的时间b而等待。

关键的差异在于A和B互相之间的等待。先A后B时,B多等a;先B后A时,A多等b。所以当a ≤ b时,也就是A接水更快,先A后B一定不会比先B后A差;反过来也一样。既然任意两个相邻的逆序对都能通过交换让总等待时间不变差,重复交换下去,最终序列一定可以变成耗时从小到大排列。

这就是贪心算法在这道题里的合法性证明。你不需要记住“短作业优先”这个名字,只需要记住这个交换思想,后面很多贪心题都要用它。

2. 核心实现部位:排序、累加与输出顺序

2.1 排序不能丢“编号”,用结构体或 pair 保存原始位置

不少人第二遍写这题还是WA,不是因为算法不对,而是因为输出错了。题目要求的输出有两行:第一行是最优排队顺序对应的原始编号,第二行是平均等待时间。

如果你只开一个数组,sort完就什么都找不回来了。正确做法是给每个人保留两个信息:接水时间、原始编号。

C++里最简单的写法是用一个结构体,或者直接用pair<int, int>pair排序默认先比较第一个字段,再比较第二个字段,这种天然特性在这道题里特别合适:

pair<int, int> a[1005]; // first = 接水时间,second = 原始编号 sort(a + 1, a + n + 1);

因为第一关键字是接水时间,所以排序后a[i].second就是第i个位置上的人原本的编号,直接输出就行。

这里还有个细节:如果两个人接水时间相同,题目没有强制要求谁先谁后,因为相同耗时的两个人无论谁排前面,对总等待时间的贡献是一样的。但如果你希望相同时间的人仍然按原始编号顺序输出,用pair也可以做到——当first相等时,pair会自动按second升序排;如果你用自定义结构体,就在比较函数里加一句“时间相等时按编号升序”。

struct Person { int t; // 接水时间 int id; // 原始编号 } p[1005]; bool cmp(Person x, Person y) { if (x.t != y.t) return x.t < y.t; return x.id < y.id; }

2.2 累加总等待时间:加权写法比两层循环更不容易错

总等待时间有两种写法,我建议新手都掌握。

第一种是“前缀和累加”写法:从第二个位置开始,每个人等待的时间就是他所有前一个人的接水时间之和,把这些等待时间加起来。

long long sum = 0; // 总等待时间 long long prefix = 0; // 当前人之前已用掉的时间 for (int i = 1; i <= n; i++) { sum += prefix; // 当前人干等了 prefix 分钟 prefix += p[i].t; // 这个人开始接水后,队伍累积时间增加 }

第二种是“加权”写法,更简洁也更能体现排序的效果:第1个人的接水时间会被排在后面的n-1个人等待,第2个人的被n-2个人等待,依此类推,最后一个人不会被任何人等待。

long long sum = 0; for (int i = 1; i <= n; i++) { sum += p[i].t * (n - i); }

这两种写法算出的结果完全一样。写第二种时要注意,乘出来是long long级别的量,别拿int硬扛。

2.3 平均等待时间的浮点输出问题

累加完之后,平均等待时间是sum / (double)n,输出保留两位小数。

C++里用printf("%.2f\n", avg);是最省事的。如果你习惯用cout,加上#include <iomanip>后写cout << fixed << setprecision(2) << avg << endl;

这里要特别提醒:sum必须是整数类型或浮点类型,但是除法时一定要把其中一个操作数转成double。如果写成sum / n,整数除以整数会直接砍掉小数部分,结果变成0.00,这种低级错误在评测环境里就是整道题全WA。

3. 完整代码与复杂度剖析

3.1 C++ 完整实现

#include <bits/stdc++.h> using namespace std; struct Person { int t; int id; } p[1005]; bool cmp(Person a, Person b) { if (a.t != b.t) return a.t < b.t; return a.id < b.id; } int main() { int n; cin >> n; for (int i = 1; i <= n; i++) { cin >> p[i].t; p[i].id = i; } sort(p + 1, p + n + 1, cmp); long long sum = 0; long long prefix = 0; for (int i = 1; i <= n; i++) { sum += prefix; prefix += p[i].t; } for (int i = 1; i <= n; i++) { cout << p[i].id << (i == n ? "\n" : " "); } printf("%.2f\n", (double)sum / n); return 0; }

代码本身不复杂,但有三行值得说。

p[i].id = i;这句是把读入时的位置记录下来,这是输出的根本。很多人在输入完时间之后忘了存编号,排序完再想找原编号就来不及了。

排序循环里的sum += prefix;对应的是“当前这个人干等了多久”。由于第1个人等待时间为0,所以第一轮加进去的也是0,不会影响结果;但代码逻辑上是从第1个人开始统一处理的,这是刻意保留这种写法的,因为它和“等待时间不含自己接水”的定义完全对应。

最后一行输出平均时间,(double)sum / n先转类型再除,保证浮点精度。

3.2 Python 完整实现

Python的代码思路一样,但有几个地方写起来更顺手或者更需要注意。

n = int(input()) a = list(map(int, input().split())) arr = [(t, i + 1) for i, t in enumerate(a)] arr.sort() prefix = 0 total = 0 order = [] for t, idx in arr: total += prefix prefix += t order.append(str(idx)) print(" ".join(order)) print(f"{total / n:.2f}")

Python的元组排序天然就是“先按时间,时间相同按下标”,省去了写比较函数的麻烦。这题的n很小,直接用列表就行,不需要优化。

print(" ".join(order))这里我先把编号转成字符串再拼接,如果用print(order)会输出带方括号和逗号的列表,格式不对。输出格式是算法题里非常容易丢分的地方,建议每道题都习惯性按样例比对一遍格式。

3.3 复杂度和数据范围分析

排序是O(n log n),累加是O(n),所以整道题的时间复杂度就是O(n log n)。空间复杂度O(n),因为要保存每个人的时间和编号。

n的范围通常是1000以内,所以无论如何都不会超时。但n小不代表可以随便用O(n^2)的写法,比如每次选出当前最小时再来一个双重循环,虽然也能过,但违背了这道题训练贪心和排序的初衷。做题的意义不是AC就行,而是通过一道题掌握一类解决思路。排序完后一趟累加,这种O(n log n)的做法才是这个题最值得学到的东西。

数据范围这里,虽然n不大,但建议所有累加变量都用long long。原因在第四章详细说,这里是提前打个疫苗。

4. 踩坑记录与排查思路

4.1 时间累加越界:int 不够用

有朋友会问,n最大也就1000,接水时间再大能有多大?但如果每个Ti可以到1e6,最坏情况下总等待时间可以到n^2 * Ti这个量级,1000 * 1000 * 1e6就是1e12级别,远超int的21亿上限。

long long是不会错的选择。就算你现在拿到的数据范围小,养成累加用64位整数的习惯,对后面做更大数据范围的题很有帮助。这个习惯就是算法竞赛里常说的“取值范围先行”。

4.2 把平均等待时间算成0.00

这是最常见WA点之一。如果写的是printf("%.2f\n", sum / n);而sum和n都是整数,结果会先做整数除法,小数部分被截断,再转成浮点数输出。比如10/4会先算成2,再输出2.00,而不是2.50。

排查思路很简单:看到输出全是.00结尾,第一反应就是检查除法两边类型。强制转换其中一个操作数为double,问题立刻解决。

4.3 排序后输出编号顺序搞反

题目要输出的排序顺序是:第一个位置放谁,第二个位置放谁,依次输出编号。不要把它理解成“按编号从小到大输出时间”。有人排完序之后把p[i].id当成时间输出了,这也是典型错误。

调试时可以拿样例跑:输入1 2 3 4,排序后arr是(1,1),(2,2),(3,3),(4,4),输出1 2 3 4,看不出问题;但如果你换个乱序输入,比如3 1 2,正确输出应该是2 3 1,按时间升序对应原始编号是2(时间1)、3(时间2)、1(时间3)。用乱序数据自测一遍,就能看出排序和编号输出是否对应。

4.4 平均等待时间公式的另一种推导

这题还有一个公式可以快速算总等待时间:把排好序后的时间,从第1个到第n-1个,分别乘以(n-1)、(n-2)、...、1,全部加起来。也就是sum += t[i] * (n - i);

我们可以验证一下样例:t = [1,2,3,4],n=4,sum = 13 + 22 + 3*1 = 10,平均2.50。

这个公式在推导上很直观:第1个人接水时,后面3个人都在等,所以他的1分钟贡献了3分钟等待;第2个人接水时,后面2个人在等,贡献22=4分钟;第3个人贡献31=3分钟。第4个人接水时没有人等他,所以不计。

两种公式选哪种看个人习惯,但用加权公式时尤其要注意循环下标别从0开始搞混。如果数组从0开始存,写成sum += t[i] * (n - i - 1)才对。

5. 从一道题到一类问题:贪心还能用在哪

5.1 操作系统里的“短进程优先”调度

P1223 排队接水本质上就是调度问题。操作系统里有一个经典的CPU调度算法叫SJF(Shortest Job First,短作业优先),思路就是让执行时间最短的进程先运行,从而降低所有进程的平均等待时间。

两边的模型几乎一模一样:一个CPU(一个水龙头),一批到达时间相同或者可以排队等待的任务(接水的人),每个任务有已知执行时间(接水耗时)。目标是最小化平均等待时间。所以做完P1223,你其实已经把操作系统的SJF调度思想过了一遍。

现实中的操作系统很少直接使用纯SJF,因为实际环境中无法准确预知每个进程的运行时间,而且短进程一直被插队会导致长进程饿死。但理解SJF是理解更复杂调度算法(比如最短剩余时间优先、多级反馈队列)的基础。算法题和生活经验之间的桥梁,就是这么搭起来的。

5.2 多水龙头版本:排队打水问题的进阶

如果题目把条件改成“有m个水龙头可以同时接水”,那情况就复杂一些了。此时不能简单升序一个个排,而是要尽量把耗时长的任务分散到不同水龙头,避免某一列队伍特别长。

常见的策略是:先把所有人按接水时间排序,然后用一个小根堆维护每个水龙头当前累计的占用时间,每次把下一个任务分配给当前累计时间最少的水龙头。这个过程每步都选择“当前最闲的水龙头”,本身又是一个贪心选择,配合优先队列实现,复杂度O(n log m)。

这种变化版在面试里也很常见,其实就是在P1223的基础上多问一句:“如果资源可以并行呢?”从单机调度到多机并行,贪心策略从简单排序升级为“排序+优先队列”,解题路径的延伸非常自然。

5.3 加权等待时间:当每个人的时间价值不同

现实里不是所有人“每分钟”都等价。医院急诊分诊就是例子:危重病人需要优先处理,而不是只看“处理时间短”。如果给每个人加上一个权重w_i,表示每等待一分钟产生的代价,目标变成最小化加权总等待时间,贪心策略就变了。

这时正确的排序依据是“接水时间/权重”的比值,比值小的排前面。这就是调度理论里的Smith规则。核心思想从P1223的“短作业优先”扩展成“单位权重占用时间越少越优先”。如果你在面试中遇到这种变形,能联想到P1223的证明框架,很快就能推出正确排序规则。

6. 实操心得与扩展建议

最后分享两个我实际刷题过程中总结出来的小技巧,对做P1223和后面的贪心题都有帮助。

第一个技巧是先写暴力再写贪心。如果刚开始对贪心正确性不放心,可以先写一个全排列枚举所有顺序的暴力程序,n很小的时候是能出结果的;再把贪心程序的输出和暴力结果对比,跑几组随机数据就能验证自己的贪心策略。这个习惯在遇到思路不确定的题目时特别有用。暴力代码可能超时,但它的作用是当参照物,不用于提交,只用于对拍。

第二个技巧是格式化输出前先跑一遍样例。我这道题第一次提交就是挂在输出格式上,样例输入是1 2 3 4,顺序本来就升序,看不出编号和时间的对应关系,最后输出好了但多输出了一个空格,被判PE(格式错误)。后来我习惯用乱序数据自测,比如输入3 1 2,如果输出不是2 3 1,就说明排序和编号之间有bug。这个自测习惯帮我避开了很多次不必要的罚时。

P1223排队接水虽然简单,但它是理解贪心算法、排序应用、格式输出的一个综合入门点。做完这道题,你对“为什么排序能优化等待时间”“为什么要保存原始索引”“为什么累加要用long long”这三个问题就有了具体答案。带着这套经验去刷后续的贪心题单,你会发现自己看题的速度明显不一样。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/24 21:39:41

大气层升级22.5.0全指南:版本匹配、签名补丁与故障排查

“我前天刚把大气层整合包换成了支持22.5.0的版本&#xff0c;为什么重启之后反而进不去系统了&#xff1f;”这周已经有三个玩友问过我类似的问题。如果你也正在经历“系统提醒更新→顺手点了→重启后卡LOGO/直接进官方系统/游戏全部装不上”的流程&#xff0c;那这篇东西就是…

作者头像 李华
网站建设 2026/9/24 21:39:02

基于Matlab的齿轮箱传递路径分析与故障诊断贡献量分解

齿轮箱一旦振动超标&#xff0c;工程师最头疼的事情不是“振动大”&#xff0c;而是说不清振动到底从哪个齿轮啮合点出来、经过哪条结构路径传到测点。同一个测点上的信号&#xff0c;包含了电机转速波动、各级齿轮啮合激励、轴承故障冲击、箱体共振等多个源头&#xff0c;再经…

作者头像 李华
网站建设 2026/9/24 21:38:54

基于SSD-VGG的驾驶员疲劳检测毕设实战指南

简介&#xff1a;这是一套面向计算机专业本科生毕业设计与项目实战的驾驶员疲劳检测系统完整实现&#xff0c;基于Python与卷积神经网络&#xff08;CNN&#xff09;构建&#xff0c;融合人脸识别与眼部状态分析技术&#xff0c;解决行车过程中实时疲劳预警的实际问题&#xff…

作者头像 李华
网站建设 2026/9/24 21:38:24

Vue 中 watch 与 computed 的正确用法:何时该删掉 watch?

先说一个我几乎每周都能在 code review 里看到的场景&#xff1a;组件里一个ref&#xff0c;本质上是从另一个 prop 或状态“派生”出来的&#xff0c;但实现却用了watch手动同步。每次看到这种写法&#xff0c;我都会在评审意见里直接写一句&#xff1a;“这个 watch 写法&…

作者头像 李华
网站建设 2026/9/24 21:38:11

Ansys Maxwell静电场电位分布仿真:从建模到后处理全解析

1. 为什么偏偏要用Maxwell做静电场电位分布1.1 静电场分析的核心需求与Maxwell的定位很多朋友第一次接触Ansys Maxwell&#xff0c;是从电机仿真或者电磁阀、电感器这类低频电磁场问题开始的。热搜词里一大半在问“maxwell电机仿真”“ansys maxwell 仿真很慢”“maxwell求解电…

作者头像 李华
网站建设 2026/9/24 21:38:05

萨曼空压机创新能力怎么样,产品/服务质量可靠吗

萨曼机械(上海)有限公司是国内专注于全系列压缩空气系统设备研发、 手机&#xff1a;13764685242 官网地址&#xff1a;http://sac.samanchina.cn/ 制造与全周期服务的专业空压机制造企业&#xff0c;致力于为各行业客户提供节能可靠的压缩空气产品与适配性解决方案。 核心技术…

作者头像 李华