
CP Weekly Challenge · USACO Bronze
USACO 2018 December Bronze Mixing Milk Solution: Simulating 100 Pours
Learn the USACO 2018 December Bronze Mixing Milk solution with direct simulation, cyclic updates, invariants, and tested Python and C++ code.
CP Weekly Challenge · USACO Bronze
USACO 2018 December Bronze Mixing Milk 题解:用循环模拟 100 次倒奶
精确模拟 100 次循环倒奶,用状态更新和总奶量不变量检查实现是否可靠。
Three buckets pour milk in a fixed cycle: bucket 1 into bucket 2, bucket 2 into bucket 3, and bucket 3 back into bucket 1. This continues for exactly 100 operations. The problem does not require a closed-form formula or a clever cycle shortcut. It tests whether we can implement one state update precisely and repeat it in the correct order.
Mixing Milk is an excellent introduction to direct simulation. We store the current amount in each bucket, calculate the maximum legal transfer for one pour, and update the source and destination together.
Where This Fits in the USACO / CP DSA Map
- Level: USACO Bronze, Difficulty Band L1
- Pattern: Direct Simulation, Cyclic Updates, State Invariant
- Prerequisite: arrays/lists,
forloops,min, modulo%, assignment and subtraction - What this teaches: Translate a process into an exact repeatable state update and use invariants to verify the simulation.
- Where it appears again: process simulation, circular indexing, state machines, event-by-event updates
What to Study Next
- Related CP Weekly problems: CCC 2026 J4 Snail Path for step-by-step simulation, CCC 2017 S1 Sum Game for running totals, and USACO 2019 January Bronze Shell Game for testing multiple starting states (planned).
- Related Python / C++ foundations: lists/arrays,
forloops,min, modulo-based circular indices, and coordinated state updates. - Related DSA note:
DSA-004Dry Runs: checking simulation order with a state table (planned).
What the Problem Is Asking
Farmer John has three buckets, possibly with different capacities. The input gives each bucket's capacity and current amount of milk.
The pouring order is fixed:
bucket 1 -> bucket 2
bucket 2 -> bucket 3
bucket 3 -> bucket 1
repeat
During every operation, he pours as much as possible until the source bucket is empty or the destination bucket is full. After exactly 100 pours, we output the final amount in each bucket on its own line.
The official bounds are 1 <= milk <= capacity <= 10^9. The values can be large, but we should not move the milk one unit at a time. One arithmetic update simulates an entire pour.
Where Beginners Often Get Stuck
The first mistake is confusing the destination's capacity with its remaining free space. The usable space is:
free_space = capacity[target] - milk[target]
The second mistake is considering only the source amount. A transfer is limited by both the milk available at the source and the space available at the destination.
The third mistake is updating too early. We must calculate the transfer amount before changing either bucket. Otherwise, using the already-reduced source amount can produce an incorrect destination update.
Finally, the simulation must perform 100 operations, not 99. A loop over turns 0 through 99 performs exactly 100 pours.
Core Algorithm Idea
For one pour from source to target:
source_milk = milk[source]
target_space = capacity[target] - milk[target]
transfer = min(source_milk, target_space)
Then update both parts of the state:
milk[source] -= transfer
milk[target] += transfer
If the source contains less milk than the destination can accept, the source empties first. If the destination has less free space, the destination fills first. The smaller quantity is exactly the largest legal transfer.
State and Cyclic Order
We store two arrays of length three:
capacity[i] = fixed capacity of bucket i
milk[i] = current amount in bucket i
For a zero-based turn number:
source = turn % 3
target = (source + 1) % 3
The remainder cycles through 0, 1, 2, and the second modulo wraps the destination from index 2 back to index 0. Turn 99 is the 100th operation; it pours from index 0 to index 1, matching the statement.
Dry Run with the Official Sample
The official sample begins with milk amounts [3, 4, 5]:
| Pours completed | Bucket 1 | Bucket 2 | Bucket 3 | Transfer |
|---|---|---|---|---|
| 0 | 3 | 4 | 5 | Initial state |
| 1 | 0 | 7 | 5 | 1 -> 2, move 3 |
| 2 | 0 | 0 | 12 | 2 -> 3, move 7 |
| 3 | 10 | 0 | 2 | 3 -> 1, move 10 |
| 4 | 0 | 10 | 2 | 1 -> 2, move 10 |
A state table makes it easy to verify the direction, transfer amount, capacity limit, and operation count.
The Conservation Invariant
Every operation subtracts transfer from one bucket and adds the same amount to another. Therefore, the total milk never changes.
The sample starts with 3 + 4 + 5 = 12, so every later state must also sum to 12. Each amount must also remain between zero and its bucket's capacity. These invariants do not compute the answer directly, but they are powerful debugging checks.
Python Solution
capacity = [0] * 3
milk = [0] * 3
for i in range(3):
capacity[i], milk[i] = map(int, input().split())
for turn in range(100):
source = turn % 3
target = (source + 1) % 3
free_space = capacity[target] - milk[target]
transfer = min(milk[source], free_space)
milk[source] -= transfer
milk[target] += transfer
for amount in milk:
print(amount)
This version uses standard input and output, which is convenient for modern judges and local testing. The historical USACO statement uses mixmilk.in and mixmilk.out; only the input/output adapter changes if that file interface is required.
C++ Solution
#include <algorithm>
#include <array>
#include <iostream>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
array<long long, 3> capacity{};
array<long long, 3> milk{};
for (int i = 0; i < 3; i++) {
cin >> capacity[i] >> milk[i];
}
for (int turn = 0; turn < 100; turn++) {
int source = turn % 3;
int target = (source + 1) % 3;
long long freeSpace = capacity[target] - milk[target];
long long transfer = min(milk[source], freeSpace);
milk[source] -= transfer;
milk[target] += transfer;
}
for (long long amount : milk) {
cout << amount << '\n';
}
return 0;
}
The C++ solution uses long long, which also makes a debugging sum of all three buckets safe when the total can be close to 3 x 10^9.
Why the Algorithm Is Correct
During each turn, transfer does not exceed the milk in the source, so the source cannot become negative. It also does not exceed the free space in the destination, so the destination cannot exceed its capacity.
If transfer equals the source amount, the source becomes empty. If it equals the destination's free space, the destination becomes full. Since it is the minimum of these two limits, at least one stopping condition is reached, and the operation transfers the maximum legal amount required by the statement.
The loop performs exactly 100 correct operations in the required cyclic order. Therefore, the three stored amounts after the loop are exactly the required final state.
Complexity Analysis
For a general number of pours P:
Time: O(P)
Space: O(1)
Because this problem fixes P = 100, both the running time and extra space are constant with respect to the input values. Large capacities do not increase the number of operations.
Common Mistakes
- Using the destination capacity instead of
capacity[target] - milk[target]. - Moving milk one unit at a time and making the running time depend on the capacity.
- Changing the source before calculating the complete transfer amount.
- Forgetting to wrap from bucket 3 back to bucket 1.
- Performing 99 or 101 pours instead of exactly 100.
- Trying to exploit a sample cycle without proving it, when 100 direct updates are simpler.
- Printing the three results on one line instead of three lines.
Review Questions
- Why is the transfer the minimum of the source amount and destination free space?
- How does
turn % 3generate the cyclic pouring order? - Why should
transferbe computed before either bucket is modified? - Which quantity remains invariant, and how can it reveal a bug?
- If the number of pours were changed from 100 to
P, what would the time complexity be?
三个奶桶按固定顺序互相倒奶:1 号倒进 2 号,2 号倒进 3 号,3 号再倒回 1 号,如此循环 100 次。题目没有要求我们寻找公式或预测很远的未来,它真正考察的是一项竞赛编程基本功:能否把一次操作写准确,再按正确顺序重复。
Mixing Milk 是很适合学习 direct simulation 的 USACO Bronze 真题。我们只需要维护三个桶当前的奶量,每次计算“最多能倒多少”,同步更新源桶和目标桶。
本题在 USACO / CP DSA 地图里的位置
- Level: USACO Bronze, Difficulty Band L1
- Pattern: Direct Simulation, Cyclic Updates, State Invariant
- Prerequisite: arrays/lists,
forloops,min, modulo%, assignment and subtraction - What this teaches: 把题目描述拆成一个精确、可重复的状态更新,并用不变量检查模拟是否可靠。
- Where it appears again: process simulation, circular indexing, state machines, event-by-event updates
下一步推荐
如果你已经理解 Mixing Milk,可以继续看:
- 相关 CP Weekly 真题:CCC 2026 J4 Snail Path(逐步模拟与状态)、CCC 2017 S1 Sum Game(running totals)、USACO 2019 January Bronze Shell Game(多种初始状态模拟,planned)
- 相关 Python / C++ 基础:list/array、
forloop、min、%循环下标、同时更新相关状态 - 相关 DSA 专题:
DSA-004Dry Run 是什么?用状态表检查模拟顺序(planned)
题目要我们做什么
Farmer John 有三个容量可能不同的奶桶。输入给出每个桶的:
capacity = 桶的最大容量
milk = 桶里当前的奶量
倒奶顺序固定为:
bucket 1 -> bucket 2
bucket 2 -> bucket 3
bucket 3 -> bucket 1
然后重复
每次倒奶都要“尽可能多地倒”,直到发生下面至少一种情况:
- 源桶变空。
- 目标桶变满。
完成恰好 100 次操作后,输出三个桶最终的奶量,每个答案占一行。
官方约束为 1 <= milk <= capacity <= 10^9。数值虽然大,但我们不需要一单位一单位地移动奶;一次倒奶可以用一个公式完成。
初学者容易卡在哪里
第一个误区是把“目标桶容量”当成还能倒进去的量。真正的剩余空间是:
free_space = capacity[target] - milk[target]
第二个误区是只看源桶有多少奶。实际转移量同时受两个条件限制:源桶最多提供自己的全部奶量,目标桶最多接收自己的剩余空间。
第三个误区是更新顺序。必须先把 transfer 算好,再同时减少源桶、增加目标桶。如果先修改源桶,再拿修改后的数值计算目标桶增加量,就会把本次倒出的奶量弄丢。
最后还要小心次数:循环是 100 次,不是 99 次。使用 range(100) 或 turn < 100,会执行编号为 0 到 99 的 100 次操作。
核心算法概念
对于一次从 source 倒向 target 的操作:
source_milk = milk[source]
target_space = capacity[target] - milk[target]
transfer = min(source_milk, target_space)
然后更新:
milk[source] -= transfer
milk[target] += transfer
为什么是 min?
- 如果源桶里的奶更少,就会先把源桶倒空。
- 如果目标桶的空间更少,就会先把目标桶装满。
两者中较小的那个,正是这次合法操作可以转移的最大数量。
状态定义与循环顺序
我们用两个长度为 3 的数组:
capacity[i] = 第 i 个桶的固定容量
milk[i] = 第 i 个桶当前的奶量
容量从不改变,奶量会在每次操作后改变。
若 turn 从 0 开始,则:
source = turn % 3
target = (source + 1) % 3
turn % 3 会产生循环序列 0, 1, 2, 0, 1, 2, ...,恰好对应三个源桶。(source + 1) % 3 让 2 号下标之后回到 0 号下标。
因此第 100 次操作对应 turn = 99:
source = 99 % 3 = 0
target = 1
也就是题目说明的 1 号桶倒进 2 号桶。
用状态表 Dry Run
官方样例从奶量 [3, 4, 5] 开始:
| 已完成操作数 | Bucket 1 | Bucket 2 | Bucket 3 | 本次转移 |
|---|---|---|---|---|
| 0 | 3 | 4 | 5 | 初始状态 |
| 1 | 0 | 7 | 5 | 1 -> 2, 转移 3 |
| 2 | 0 | 0 | 12 | 2 -> 3, 转移 7 |
| 3 | 10 | 0 | 2 | 3 -> 1, 转移 10 |
| 4 | 0 | 10 | 2 | 1 -> 2, 转移 10 |
状态表能帮助我们检查三个问题:倒奶方向是否正确、转移量是否受容量限制、循环次数是否推进。
一个重要不变量
每次操作都从一个桶减去 transfer,同时向另一个桶加上同样的 transfer:
new_total
= milk[source] - transfer
+ milk[target] + transfer
= old_total
因此三个桶的总奶量始终不变。官方样例一开始是:
3 + 4 + 5 = 12
之后每一行状态的总和也必须是 12。如果 dry run 或程序中总量变了,就说明更新逻辑一定有错误。
同时,每个桶都应始终满足:
0 <= milk[i] <= capacity[i]
这些 invariant 不直接产生答案,却是调试模拟题非常可靠的检查工具。
Python 解法
capacity = [0] * 3
milk = [0] * 3
for i in range(3):
capacity[i], milk[i] = map(int, input().split())
for turn in range(100):
source = turn % 3
target = (source + 1) % 3
free_space = capacity[target] - milk[target]
transfer = min(milk[source], free_space)
milk[source] -= transfer
milk[target] += transfer
for amount in milk:
print(amount)
这份代码使用标准输入输出,便于在现代在线评测和本地终端运行。历史 USACO 题面使用 mixmilk.in 和 mixmilk.out;如果评测环境要求文件输入输出,只需替换输入输出适配层,核心模拟不变。
C++ 解法
#include <algorithm>
#include <array>
#include <iostream>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
array<long long, 3> capacity{};
array<long long, 3> milk{};
for (int i = 0; i < 3; i++) {
cin >> capacity[i] >> milk[i];
}
for (int turn = 0; turn < 100; turn++) {
int source = turn % 3;
int target = (source + 1) % 3;
long long freeSpace = capacity[target] - milk[target];
long long transfer = min(milk[source], freeSpace);
milk[source] -= transfer;
milk[target] += transfer;
}
for (long long amount : milk) {
cout << amount << '\n';
}
return 0;
}
C++ 使用 long long,这样即使我们在调试时计算三个桶的总奶量,也能安全容纳最多约 3 x 10^9 的总和。
正确性说明
每一轮中,transfer 不超过源桶当前奶量,因此源桶不会变成负数;它也不超过目标桶剩余空间,因此目标桶不会超过容量。
如果 transfer 等于源桶奶量,源桶被倒空;如果它等于目标桶剩余空间,目标桶被装满。因为 transfer 是这两个限制的较小值,至少有一个停止条件成立,所以这正是题目要求的最大合法转移量。
循环按照 1 -> 2 -> 3 -> 1 的顺序执行恰好 100 次。每轮都正确模拟题目指定的一次操作,因此循环结束后的三个数就是所求最终状态。
复杂度分析
若把操作次数写成一般的 P:
Time: O(P)
Space: O(1)
本题固定 P = 100,因此从输入规模角度看,运行时间和额外空间都是常数级。大容量不会增加循环次数。
常见错误
- 使用
capacity[target]作为可接收量,而不是capacity[target] - milk[target]。 - 一单位一单位地倒奶,导致把容量大小误当成运行时间。
- 在计算完整的
transfer之前先修改源桶。 - 把顺序写成
1 -> 2 -> 3后停止,没有循环回 1 号桶。 - 执行 99 次或 101 次,而不是恰好 100 次。
- 看到样例出现重复状态后直接使用 cycle shortcut,却没有证明周期;本题直接做 100 次更简单可靠。
- 忘记输出三个答案各占一行。
复习问题
- 为什么转移量是源桶奶量与目标桶剩余空间的较小值?
turn % 3如何生成循环的倒奶顺序?- 为什么必须在修改两个桶之前先算出
transfer? - 哪个数量在所有操作中保持不变?它如何帮助调试?
- 如果操作次数从 100 改成
P,算法复杂度是什么?
Questions or feedback?
Have a question about this article or want to suggest an improvement? Send us a private message.
有问题或建议?
如果你对本文有疑问,或希望提出改进建议,请给我们发送私密留言。