
Python Learning Notes
Why Does Greedy Sorting Always Work? The Exchange Argument with Time on Task
Use CCC 2013 Time on Task to understand shortest-first greedy selection and prove it with an exchange argument.
Python 学习笔记
贪心排序为什么一定对?从 Time on Task 讲清交换论证
用 CCC 2013 Time on Task 讲清最短任务优先的贪心策略,并用交换论证证明它一定正确。
You have 6 minutes and three chores that take 3, 6, and 3 minutes — how many can you finish? The instinct says "do the quick ones first": sort the list, then start from the front. That instinct is greediness. But contests do not grade on instinct: why does "shortest first" always deliver the maximum count? If it fails even once, you lose the whole problem. This note uses CCC 2013's Time on Task to walk through the standard proof behind this kind of greedy — the exchange argument. By the end you will recognize it as the exact same pattern that justified sort-and-pair in the same-week Tandem Bicycle walkthrough.
The Common Mistake
total_time = int(input())
count = int(input())
chores = [int(input()) for _ in range(count)]
done = 0
for chore in chores: # in input order
if chore <= total_time:
total_time -= chore
done += 1
print(done)
The flaw is letting the input decide the order. Run it on official sample 2 (T=6, chores 5, 4, 3, 2, 1 minutes): it does the 5-minute chore first, wastes the remaining minute, and finishes only 1 chore — while the answer is 3 (1+2+3). The same data gives different answers under different orders, so the order is itself part of the decision. Leaving it to the input format means abandoning half the problem.
The Core Idea
State the task precisely: choose a subset of chores whose total duration ≤ T, with the largest possible count. Note we are maximizing the count, not minimizing the total time — those are different classic problems, and only the count is asked here.
Now the key lemma: among all selections of exactly k chores, the k shortest have the minimum total duration. Proof by one replacement: take any selection S of k chores that is not "the k shortest". Then some shorter chore x is unselected while a longer chore y is selected. Swap y for x: the count is still k, and the total does not grow (x ≤ y). Repeating finitely many times turns any selection into "the k shortest", never increasing the total. Lemma proved.
By the lemma, the problem becomes a scan: sort ascending, accumulate prefix sums. Let S(k) be the sum of the k shortest; S(k) is non-decreasing, and the maximum finishable count is exactly "the last k with S(k) ≤ T". And by the other side of the lemma: if the k shortest already exceed T, no k chores can fit (any k chores sum to at least S(k)). Both directions close — the greedy is not just "an answer", it equals the optimum.
Walk sample 2 by hand: sorted [1,2,3,4,5], prefix sums 1, 3, 6, 10, 15. With T=6 the first three fit exactly, the fourth breaks the budget — answer 3, matching the official output. Sample 1 (T=6, [3,6,3]) sorts to [3,3,6], prefix sums 3, 6, 12, answer 2.
The Recommended Solution
total_time = int(input())
count = int(input())
chores = [int(input()) for _ in range(count)]
chores.sort()
used = 0
done = 0
for chore in chores:
if used + chore <= total_time:
used += chore
done += 1
else:
break
print(done)
6
3
3
6
3
2
Three details worth keeping. First, break instead of continue: after sorting, everything later is longer — if the current chore does not fit, nothing after it will either. Second, accumulate into a separate used rather than rewriting total_time — the remaining budget stays inspectable while debugging. Third, C ≤ 100, each chore ≤ 100000, T ≤ 100000, so the sums fit easily in an int; but if the limits grow, big-integer accumulation is cheap insurance.
The Same Proof: From Pairing to Counting
Zoom out: this problem and the same-week Tandem Bicycle share one mold. Tandem Bicycle pairs N tandem riders to minimize or maximize the total speed; its proof swaps crossed pairs back into same order and shows the total never worsens. Time on Task swaps any k chores for the k shortest and shows the budget never worsens. Both say "there exists a tidy solution that is no worse": turn any solution into the tidy one step by step, then identify the best among tidy ones. Whenever you see sorting plus greedy, ask "does one replacement step ever make it worse?" — if you can answer, write the code. That question is the line between a slogan and a tool.
Side by Side
| Approach | Result on sample 2 | Why |
|---|---|---|
| Input order | 1 chore | Random order; a long chore first blocks the budget |
| Sorted, shortest first | 3 (optimal) | The k shortest minimize total duration, maximizing the count |
| Sorted, longest first | 1 chore | Reversed direction — the replacement lemma no longer holds |
| Each step: longest that still fits | 1 chore | A different greedy rule with no proof behind it fails immediately |
Practice
- Change T to 7 in sample 1: predict the answer with prefix sums, then verify by running. (Can you finish the two 3-minute chores? Does the 6-minute one fit after that? Work it out.)
- With chores [4, 4, 4, 4] and T=9, how many fit? Use the lemma to explain why shortest-first is still correct here.
- Challenge: if every chore requires an extra 1-minute rest (real cost = duration + 1), does "sort and scan from the front" still hold? Which single line would you change? (Hint: +1 per chore is like the budget paying an extra "done" minutes in advance…)
One Line to Remember
Prove "swapping cannot make it worse" first, then write "sort and pick from the front" — greedy is a provable replacement, not a gamble.
只有 6 分钟,家务清单上却排着 3 分钟、6 分钟、3 分钟三件事——最多能做完几件?直觉说"先挑快的",把清单按耗时排个序,从头开始做。这个直觉就是贪心。但在竞赛里不能靠直觉交卷:为什么"先挑最短的"给出的件数一定最多?如果某次它失败了,你会损失整道题的分数。这篇用 CCC 2013 的 Time on Task,把支撑这类贪心的一道标准证明——交换论证——完整走一遍。读完你会认出:上周 Tandem Bicycle 里"排序后配对"的正确性,和它是同一套手法。
常见错误
total_time = int(input())
count = int(input())
chores = [int(input()) for _ in range(count)]
done = 0
for chore in chores: # 按输入顺序做
if chore <= total_time:
total_time -= chore
done += 1
print(done)
错在把"顺序"交给了输入。输入顺序是任意的:用官方样例 2(T=6,五件家务 5、4、3、2、1 分钟)跑这段代码,它会先做 5 分钟的,剩 1 分钟什么也装不下,只完成 1 件;而答案是 3 件(1+2+3)。同样的数据,顺序不同、答案不同,说明顺序本身就是决策的一部分,把它留给输入格式等于放弃了一半的题目。
核心概念
先把问题说准确:从 C 件家务中选一个子集,使总时长不超过 T,并且件数最多。注意我们优化的不是"用时最少",而是"件数最多"——这两件事容易混:前者是另一道经典题(调度),后者的答案才是本题。
现在给出关键引理:在所有恰好含 k 件家务的选择里,耗时最短的 k 件,总时长最小。 证明用"一次替换":任取一个含 k 件的选择 S,如果它不是"最短的 k 件",那必有一件较短的 chores[x] 没被选、而一件较长的 chores[y] 被选。把 y 换成 x:件数仍是 k,总时长不会增加(x ≤ y)。重复这个替换有限次,任何选择都能变成"最短的 k 件",且总时长只减不增。引理成立。
由引理,题目立刻变成一道"扫描"题:把家务按耗时升序排好,累加前缀和。设最短的 k 件之和为 S(k),则 S(k) 单调递增;能完成的最大件数就是"最后一个满足 S(k) ≤ T 的 k"。而且由引理的另一面:如果最短的 k 件都超过 T,任何 k 件都不可能塞进 T(任意 k 件之和 ≥ S(k))。两个方向合拢,贪心不仅"给出了一个答案",而是恰好等于最优解。
把样例 2 手算一遍:排序后 [1,2,3,4,5],前缀和 1、3、6、10、15。T=6 时,前三个之和 6 恰好放下,第四个就超了——答案 3,与官方输出一致。样例 1(T=6,[3,6,3])排序后 [3,3,6],前缀和 3、6、12,答案 2。
推荐写法
total_time = int(input())
count = int(input())
chores = [int(input()) for _ in range(count)]
chores.sort()
used = 0
done = 0
for chore in chores:
if used + chore <= total_time:
used += chore
done += 1
else:
break
print(done)
6
3
3
6
3
2
三个细节值得记住。第一,break 而不是 continue:排序后比当前件更长的都在后面,当前都装不下,后面更不可能,直接停。第二,用独立的 used 累计已耗时间,不要回写 total_time——预算还剩多少随时可查,调试时少一层心算。第三,C ≤ 100、每件 ≤ 100000、T ≤ 100000,总和远在 int 范围内;但如果把规模改大,把累加变量养成大整数习惯是廉价的保险。
同一套证明:从配对到计数
把镜头拉远,这道题和同周的 Tandem Bicycle 是同一个模子。Tandem Bicycle 要配 N 对双人车手使总速度最小或最大,证明是:对 a ≤ b、c ≤ d,把交叉配对换回同序,总数不会变差;Time on Task 要选的"件数最多",证明是:任意 k 件换成最短的 k 件,预算不会变差。两句都是"存在一种不更差的整齐方案":先把任意解一步步换成整齐解,再证明整齐解里最好的是什么。看到排序 + 贪心时,先在脑子里问一句"替换一步会不会更差",答得出来再写代码——这是把贪心从口诀变成工具的分水岭。
对比一下
| 写法 | 样例 2 结果 | 为什么 |
|---|---|---|
| 按输入顺序做 | 1 件 | 顺序随机,先做长的会堵死预算 |
| 排序后从小到大做 | 3 件(最优) | 最短的 k 件总时长最小,能装的件数最多 |
| 排序后从大到小做 | 1 件 | 方向反了,"替换不变差"的引理不成立 |
| 每次选"当前能放下的最长一件" | 1 件 | 换个贪心规则没有证明支撑,立刻失效 |
小练习
- 把样例 1 的 T 改成 7:先按前缀和手算预测答案,再运行验证。(答案:仍能做完 3 和 3 两件,第三件 6 放不下?手算一遍你就知道了。)
- 家务时长 [4, 4, 4, 4]、T=9,最多几件?用引理解释为什么"最短优先"在这里依然是对的。
- 挑战:如果每做完一件必须休息 1 分钟(每件实际消耗 = 时长 + 1),"排序后从头选"还成立吗?需要改哪一行代码?(提示:每件 +1 相当于预算先扣掉 done 件……)
一句话记住
先证明"换一换不会更差",再写"排序后从头选"——贪心 = 可证明的替换,不是碰运气。
Questions or feedback?
Have a question about this article or want to suggest an improvement? Send us a private message.
有问题或建议?
如果你对本文有疑问,或希望提出改进建议,请给我们发送私密留言。