☰
从5L和3L桶量出4L水:状态空间搜索与通用解法
2026/10/13 4:08:36 网站建设 项目流程

1. 从两个空桶到四升水:这道题到底在考什么

第一次被问到“5L桶和3L桶怎么装出4L水”的时候,我正蹲在实验室走廊等离心机跑完。提问的是隔壁组一个刚进组的师弟,他手里转着一支记号笔,表情特别认真。我当时随口回了一句“倒来倒去呗”,结果他真把两个塑料量杯塞到我手里,说“那你倒给我看”。我折腾了快五分钟才凑出4L,中间还洒了一台面。后来我把这个过程拆开复盘,才发现这道题远不止“倒水”这么简单——它本质上是一道状态空间搜索问题,考的是你在资源受限、操作可逆性差的情况下,能不能用最少的步骤逼近目标。

这道题在面试、智力测试、甚至一些算法入门课里反复出现,不是没有道理的。它不需要任何专业背景,两个桶、一池子水就能开搞,但不同的人给出的解法差异极大:有人三步搞定,有人绕了七八步还在原地打转。更关键的是,它背后藏着一套通用的“倒水问题”求解框架,理解了这套框架,你就能把同类问题一网打尽——比如“7L和4L怎么量出2L”“11L和6L怎么量出8L”,甚至扩展到“三个桶怎么配合”。

这篇文章我会从最直观的手动操作讲起,把每一步的意图、为什么这样倒、有没有更优路径全部拆开。然后我会把这道题抽象成数学模型,告诉你为什么有些组合天生无解,有些组合必然有解。接着我会给出一个可以复用的通用解法模板,让你遇到任何“两个桶量固定水量”的题都能直接套。最后我会分享几个我在实际教学和面试中总结的坑——比如“水无限但桶没有刻度”这个前提到底意味着什么,以及为什么很多人明明倒对了却说不清楚步骤。

适合谁看?如果你正在准备技术面试、想锻炼逻辑思维、或者单纯想搞明白这道经典题到底怎么破,这篇内容都能给你一套完整的、可复现的方法论。不需要数学基础,跟着倒一遍就能懂。

2. 手动倒水全流程拆解:每一步都在干什么

2.1 先把两个桶的状态定义清楚

在动手之前,必须把初始条件和操作规则钉死,否则后面全是糊涂账。这道题的标准设定是:

  • 有一个5L桶和一个3L桶,两个桶都没有刻度线,只能知道“满了”或者“空了”。
  • 有一池子无限量的水,可以随时把桶装满,也可以随时把桶倒空。
  • 目标是在某个桶里精确得到4L水。
  • 每次操作只能做三件事之一:把某个桶装满、把某个桶倒空、把一个桶的水倒进另一个桶直到倒出桶空或接收桶满。

注意:很多人会忽略“桶没有刻度”这个前提,以为可以目测一半。一旦允许目测,这题就失去意义了。所以所有操作必须是“全有或全无”的。

我见过有人一上来就说“5L桶装一半不就是2.5L,再……”——这属于直接违规。没有刻度就意味着你只能通过“满”和“空”这两个极端状态来间接测量。这也是这道题真正的难点所在:你只能通过桶之间的转移来“制造”出中间量。

2.2 最直观的解法:五步倒出4L

先给一套最容易被记住的步骤,我把它叫做“5L桶主导法”:

  1. 把5L桶装满。
  2. 从5L桶往3L桶倒水,直到3L桶满。此时5L桶里剩下2L。
  3. 把3L桶倒空。
  4. 把5L桶里剩下的2L水倒入3L桶。此时3L桶里有2L,5L桶空。
  5. 再次把5L桶装满。
  6. 从5L桶往3L桶倒水,直到3L桶满。因为3L桶里已经有2L,所以只需要再倒1L。此时5L桶里剩下4L。

目标达成。总共六步操作,其中“装满”和“倒空”各算一步。如果你把“从5L往3L倒”和“3L桶满”合并成一步,也可以说五步。

这套解法的核心逻辑是:利用3L桶作为“减法器”,从5L桶里反复减去3L,制造出2L的余量,再用这个2L作为“底数”,让下一次减法只减掉1L,从而在5L桶里留下4L。

我实测过很多次,这套步骤最不容易出错,因为每次操作后两个桶的状态都很清晰:要么一个满一个空,要么一个满一个不满。你可以在纸上画一个两列的状态表,每步记录(5L桶水量, 3L桶水量),就能看到状态是怎么迁移的。

2.3 另一条路径:三升桶主导法

除了上面那条路,还有一条对称的路径,我把它叫做“3L桶主导法”:

  1. 把3L桶装满。
  2. 把3L桶的水全部倒入5L桶。此时5L桶里有3L。
  3. 再次把3L桶装满。
  4. 从3L桶往5L桶倒水,直到5L桶满。因为5L桶里已有3L,所以只能再倒2L。此时3L桶里剩下1L。
  5. 把5L桶倒空。
  6. 把3L桶里剩下的1L倒入5L桶。
  7. 把3L桶装满。
  8. 把3L桶的水全部倒入5L桶。此时5L桶里有1+3=4L。

这条路径需要八步,比上一条多两步。但它有一个好处:如果你把“倒空5L桶”和“倒入1L”之间的顺序记混了,这条路径的中间状态更容易恢复。我在教别人的时候,通常先教第一条,因为步骤少;但如果对方总是记不住“5L桶里剩2L”那一步,我就换第二条,因为“3L桶里剩1L”这个状态更直观。

2.4 为什么不能更少步数

有人会问:能不能四步搞定?答案是:在标准规则下不能。原因很简单:初始状态是(0,0),目标状态是某个桶里有4L。每次操作最多改变一个桶的状态,而且改变量要么是“装满到上限”,要么是“倒空到0”,要么是“转移直到一方满或空”。从(0,0)出发,第一步只有两种有效选择:装满5L得到(5,0),或者装满3L得到(0,3)。从(5,0)出发,第二步只能倒给3L桶得到(2,3)或者倒空得到(0,0)。从(2,3)出发,第三步只能倒空3L得到(2,0)或者把2L倒入3L得到(0,2)。从(0,2)出发,第四步只能装满5L得到(5,2)或者倒空3L得到(0,0)。从(5,2)出发,第五步往3L桶倒,只能倒1L就满了,得到(4,3)。所以最少需要五步(如果把“倒1L”算一步)。这个推导过程其实就是状态空间搜索的雏形,后面我会展开讲。

3. 把倒水问题抽象成状态空间:为什么有些组合必然有解

3.1 状态的定义与转移规则

把两个桶的水量记作一个有序对(a, b),其中a是5L桶的水量,b是3L桶的水量。因为桶没有刻度,所以a只能取0到5之间的整数,b只能取0到3之间的整数。所有可能的状态一共有6×4=24种,但实际可达的状态远少于这个数。

每次操作可以看作一次状态转移。合法的转移有六种:

  • 装满5L桶:(a, b) → (5, b)
  • 倒空5L桶:(a, b) → (0, b)
  • 装满3L桶:(a, b) → (a, 3)
  • 倒空3L桶:(a, b) → (a, 0)
  • 5L倒入3L:(a, b) → (max(0, a-(3-b)), min(3, a+b))
  • 3L倒入5L:(a, b) → (min(5, a+b), max(0, b-(5-a)))

从(0,0)出发,用广度优先搜索遍历所有可达状态,你会发现一共只有16个状态是可达的。目标状态是a=4或b=4。在这个可达图里,a=4的状态是(4,0)、(4,3)、(4,1)、(4,2)等,其中(4,3)就是上面五步法到达的状态。

3.2 有解的条件:最大公约数决定一切

这道题有一个非常漂亮的数学结论:用容量为m和n的两个桶,能量出任意k升水(0≤k≤max(m,n))当且仅当k是m和n的最大公约数的倍数。

对于5和3,gcd(5,3)=1,所以1到5之间的任何整数升水都能量出来。4是1的倍数,所以必然有解。反过来,如果两个桶是6L和4L,gcd(6,4)=2,那么只能量出2L、4L、6L这些偶数升,量不出3L或5L。这就是为什么有些组合你倒到天荒地老也倒不出目标水量——不是方法不对,是数学上就不允许。

这个结论的证明思路是:每次操作改变的水量一定是m和n的线性组合,所以所有可达状态的水量都是gcd(m,n)的倍数。反过来,通过扩展欧几里得算法可以构造出达到任意倍数的操作序列。我在面试里如果遇到候选人卡壳,就会提示他往这个方向想,通常对方立刻就能反应过来。

3.3 用表格看清所有可达状态

下面这张表列出了从(0,0)出发,经过若干步后能到达的所有状态。我用步数作为行,状态作为列,方便你对照。

步数可达状态(5L桶, 3L桶)
0(0,0)
1(5,0), (0,3)
2(2,3), (5,3), (3,0), (0,0)
3(2,0), (0,2), (3,3), (5,1)
4(0,2), (2,2), (3,1), (5,0)
5(4,3), (2,3), (3,0), (0,1)
6(4,0), (0,3), (3,3), (1,0)

可以看到,目标状态(4,3)在第5步出现,(4,0)在第6步出现。如果你只要求“某个桶里有4L”,那么第5步就完成了。这张表也解释了为什么五步是最少的:第4步的所有状态里都没有4L。

4. 通用解法模板:遇到任何“两个桶量水”都能套

4.1 模板的核心思想:反复做减法

不管桶的容量是多少,只要gcd(m,n)=1,就一定能量出1L,进而量出任何整数升。通用的操作模板是这样的:

  1. 选定一个“主桶”(通常选容量大的那个)和一个“辅桶”。
  2. 反复执行“装满主桶→倒入辅桶→辅桶满了就倒空→继续倒”这个循环。
  3. 每次辅桶倒空后,主桶里剩下的水量会形成一个递减序列。当这个序列第一次小于辅桶容量时,继续倒就会在辅桶里留下一个“余数”。
  4. 记录主桶里出现的所有水量,直到目标水量出现。

以5L和3L为例,主桶选5L,辅桶选3L:

  • 装满5L,倒入3L,5L桶剩2L。
  • 倒空3L,把2L倒入3L桶。
  • 装满5L,倒入3L桶(已有2L,只能再倒1L),5L桶剩4L。目标出现。

这个过程中,5L桶里依次出现了5、2、4。如果你继续下去,还会出现1、3、0等。实际上,这个序列就是5的倍数对3取模的结果:5 mod 3 = 2,10 mod 3 = 1,15 mod 3 = 0。每次主桶装满再倒掉若干个辅桶容量,剩下的就是余数。

4.2 反向操作:从目标倒推步骤

如果你觉得正向推导容易乱,可以试试从目标状态倒推。比如目标是4L,那么最后一步之前的状态可能是:

  • 5L桶里有4L,3L桶里有3L(满的),上一步是从5L桶往3L桶倒了1L。
  • 再往前,5L桶是满的5L,3L桶里有2L,所以倒1L就满了。
  • 3L桶里的2L是怎么来的?是5L桶剩2L时倒进去的。
  • 5L桶剩2L是怎么来的?是装满5L后往空的3L桶倒满3L剩下的。

倒推法特别适合在纸上画树状图,每一步只关注“上一个状态可能是什么”,然后逐层展开。我面试的时候经常用这招,因为面试官能看到你的思考过程,而不是只等一个答案。

4.3 扩展到三个桶或更多桶

如果题目变成“5L、3L、2L三个桶怎么量出4L”,解法会更多样。但核心思路不变:把问题分解成多个两桶子问题。比如先用5L和3L量出2L,再用2L桶作为辅助量出4L。三个桶的状态空间更大,但只要有解,广度优先搜索一定能找到最短路径。我在实际编程中写过一个小脚本,用BFS自动求解任意桶组合的最短倒水序列,代码不到50行。核心就是一个队列加一个visited集合,每次从队列取出一个状态,尝试六种操作,把新状态入队。跑出来的结果和手动推导完全一致。

5. 面试和教学中的高频坑:为什么你倒对了却说不清

5.1 把“倒满”和“倒空”混为一谈

这是最常见的错误。很多人描述步骤时说“把5L桶倒进3L桶,然后倒掉”,但没有说清楚倒掉的是哪个桶。如果倒掉的是5L桶,那前面白倒了;如果倒掉的是3L桶,那5L桶里还剩2L。一字之差,结果天壤之别。我在教别人的时候,要求每一步必须明确说“倒空X桶”或“把X桶倒进Y桶直到Y桶满”。这个习惯一旦养成,步骤就不会乱。

5.2 忽略“桶没有刻度”的前提

有些人会不自觉地用“一半”“三分之一”这种描述。比如“把5L桶装一半,得到2.5L,再……”——这在标准规则下是不允许的。如果你在面试中这么说,面试官可能会直接打断你。正确的做法是:只使用“满”“空”“倒到另一个桶满为止”这三种操作。所有中间量都是通过桶之间的容量差间接得到的。

5.3 步骤顺序颠倒导致状态丢失

五步法里,第三步是“倒空3L桶”,第四步是“把5L桶里的2L倒入3L桶”。如果你先倒2L再倒空3L,那就把2L也倒掉了。这种顺序错误在紧张的时候特别容易犯。我的建议是:每做完一步,在纸上写下两个桶的当前水量,然后再做下一步。不要凭记忆连续操作。我见过太多人在面试时嘴上说得头头是道,一动手就乱套,就是因为没有记录中间状态。

5.4 误以为所有组合都有解

前面说过,gcd(m,n)决定了有解性。但很多人不知道这个结论,遇到6L和4L量3L的题,硬倒了十分钟还在试。如果你在面试中遇到这种题,可以先快速算一下gcd,如果目标不是gcd的倍数,直接告诉面试官“这个组合无解”,并解释原因。这比闷头乱倒更能体现你的数学素养。我有个朋友在面试某公司时就是这么做的,面试官当场就给了通过。

6. 从倒水题到算法思维:这道题真正教会我的事

6.1 状态空间搜索的朴素实现

倒水问题本质上是一个隐式图搜索问题。每个状态是一个节点,每次操作是一条边,目标是从起点找到一条到终点的路径。因为状态数很少(最多24个),用广度优先搜索(BFS)可以保证找到最短路径。下面是一个Python实现的简化版:

from collections import deque def solve(cap_a, cap_b, target): start = (0, 0) visited = {start} queue = deque([(start, [])]) while queue: (a, b), path = queue.popleft() if a == target or b == target: return path # 六种操作 next_states = [ ((cap_a, b), f"装满{cap_a}L桶"), ((0, b), f"倒空{cap_a}L桶"), ((a, cap_b), f"装满{cap_b}L桶"), ((a, 0), f"倒空{cap_b}L桶"), ((max(0, a-(cap_b-b)), min(cap_b, a+b)), f"{cap_a}L倒入{cap_b}L"), ((min(cap_a, a+b), max(0, b-(cap_a-a))), f"{cap_b}L倒入{cap_a}L"), ] for state, action in next_states: if state not in visited: visited.add(state) queue.append((state, path + [action])) return None

跑solve(5, 3, 4)会返回六步操作序列,和手动推导完全一致。这个脚本我用了很多年,每次遇到新的桶组合,改一下参数就能直接出结果。

6.2 为什么这道题适合作为算法入门

倒水问题是一个完美的算法教学案例,因为它同时包含了几个核心概念:状态表示、状态转移、搜索空间、最优解。而且它的状态空间小到可以手算,又大到需要系统方法。我在带新人的时候,经常用这道题作为BFS的第一课。学员先手动倒一遍,感受一下“盲目搜索”的低效,然后引入队列和visited集合,立刻就能看到算法带来的效率提升。这种从直觉到形式化的过程,比直接讲抽象概念要有效得多。

6.3 实际工作中的类比

倒水问题的思维模式在实际工作中到处都能用上。比如资源调度:你有两种规格的容器,怎么组合出目标容量?比如版本管理:你有两个分支,怎么通过合并和回滚得到特定的代码状态?甚至日常生活中的“用两个杯子量出半杯水”也是同一个模型。我后来做数据管道的时候,经常需要把不同批次的数据合并成特定大小的块,用的就是类似的“反复做减法”思路。这道题的价值不在于答案本身,而在于它训练你把一个看似随意的操作序列抽象成可复用的模式。

7. 几个我踩过的坑和最后的小技巧

第一次教别人这道题的时候,我自以为讲得很清楚,结果对方倒到第三步就卡住了。后来我发现问题出在我跳过了“为什么要倒空3L桶”这一步。对已经理解的人来说,倒空3L桶是为了腾出空间接收5L桶里剩下的2L;但对新手来说,这个意图不说明白,他就只是机械地执行命令,一旦顺序被打乱就完全不知道怎么办。所以后来我教这道题,一定会先问对方:“你现在想让5L桶里留下2L,但3L桶是满的,你怎么把2L转移过去?”让对方自己说出“先把3L桶倒空”,这样他才能真正记住。

另一个坑是:很多人以为“装满5L桶”和“倒空5L桶”是对称操作,可以互换。但在某些步骤里,倒空5L桶会把关键的水量丢掉。比如五步法的第四步之后,5L桶是空的,3L桶里有2L。这时候如果你习惯性地去“倒空5L桶”,什么也不会发生,但如果你去“倒空3L桶”,就把2L丢了。所以每一步都要看清楚当前状态,不要凭肌肉记忆操作。

最后分享一个小技巧:如果你在面试中一时想不出步骤,可以先在纸上画一个6×4的网格,横轴是5L桶的水量0到5,纵轴是3L桶的水量0到3。从(0,0)开始,每次操作画一条箭头,看看能不能走到(4,任意)。这个网格图能帮你快速定位可行路径,比凭空想要可靠得多。我试过很多次,即使很紧张,只要画出网格,通常一分钟内就能找到解。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询