我认真研究天平秤球问题,是在帮一个准备算法面试的朋友复盘题目的时候。题目用一句话就能说完:12个外观完全一样的球,其中1个重量异常(不知道偏轻还是偏重),用一架没有砝码的天平,最多称3次,找出这个异常球,并判断它到底是偏轻还是偏重。说实话,第一眼看到这题,很多人会觉得“无非二分法嘛”,但真正上手做一次就会明白,它比想象中难得多,而且背后的东西也远比一道题值钱:三进制编码、信息论极限、组合设计、甚至程序化搜索方案,全都藏在里面。这篇文章我就把这套东西一次性讲透,从最直观的分组做法,到能直接抄作业的固定编码表,再到13球、N球这类变体,尽量让新手也能看完就能上手。
如果你是准备面试的开发者,可以重点看后面的编码解法和程序验证;如果你只是被这个问题勾起好奇心,前面“三分组自适应”部分就足够让你在朋友面前露一手。两种路径各有价值,我尽量都讲明白。
1. 先把题目和“为什么值得研究”说清楚
1.1 问题描述与解题目标
天平秤球问题的标准描述是:有12个尺寸、颜色、外观完全相同的球,其中只有1个球的重量与其余11个不同,但不知道它是偏重还是偏轻。现在给你一架普通天平(只能比较左右两盘重量谁重谁轻,没有刻度,也不能读数),要求在最多3次称量之内,把这个异常球找出来,并且说清楚它到底是偏重还是偏轻。
这里有两个关键限定。第一,“不知道偏轻还是偏重”让题目难度直接翻倍,因为你面对的不是“在12个里找一个”的12种情况,而是“在12个球里找一个,且找到后还要区分轻重”的24种情况。第二,“最多3次”意味着称量策略是自适应、可分支的,第2次、第3次称什么,取决于前一次的结果。如果忽略这两点,很多看起来像是解法的思路都会在中途翻车。
1.2 为什么这题是面试和思维训练常客
这道题之所以长盛不衰,是因为它几乎没有专业知识门槛,却非常考验一个人的结构化解题能力。它会强制你回答三个层面问题:一是信息量是否足够,二是策略上如何保证最坏情况也能命中,三是具体每一步能否真正“消去”足够多的可能性。这三个问题,本质上就是工程里常说的理论边界、算法设计和异常分支处理。
对面试官来说,这题是很好的干扰项筛选器。喜欢背答案的人往往只能给出“1和2称、3和4称”这种支离破碎的操作,却说不清为什么要这样分组;真正理解的人会从“每次称量有三种结果”出发,自然而然地想到三进制拆分、镜像配对、剩余候选集收缩这些概念。所以与其说这是个逻辑题,不如说它是一面能照出思维习惯的镜子。
1.3 先给出结论:12球称3次确实可以做到
先把结论摆在前面:12个球、不知道轻重、最多称3次,同时找出坏球并判断轻重,这件事是可以做到的。不仅自适应策略可以做到,甚至存在“三次称量完全固定、不需要中途改变策略”的编码方案,这一点比大多数人想象的要强。后面我会把两种方案都给出来,并且用程序验证一次,确保不是碰巧。
如果你之前在网上搜过答案,估计看到过各种真假难辨的“口诀”。我的建议是不要背口诀,因为你只要理解了编码思想,未来遇到13球、未知轻重、已知轻重、甚至N球k次这类变体时,都能现场推出来。这才是这篇文章最想传达的东西。
2. 用信息论算出“理论上最多能称几个球”
2.1 一次称量为什么能带来三种结果
天平这一次动作,结果只有三种:左边重、右边重、平衡。请注意,这三种结果不是简单“是/否”的二元信息,而是三态信息。一次称量携带的最大信息量是 log₂3 比特,约1.585比特。三次称量携带的最大信息量就是 3×log₂3 = log₂27,对应27种不同的“三次结果组合”。
这就是这道题和信息论最直接的交点。如果在某一步你的策略只能产生两种可区分结果,那你就白白浪费了天平“平衡”这个信息通道。这也是为什么单纯二分法永远做不出12球3次:二分法的一次只能排除一半,3次最多区分8种情况,远不够覆盖24种。
2.2 从3^k到2N的不等式
在不考虑其他约束的前提下,k次称量最多能区分的“状态数”是3^k,也就是每种结果序列对应一个最终结论。我们的问题里,总状态数是2N:N个球中每一个都可能是坏球,并且每个球又有偏轻和偏重两种可能。要保证能全部区分,必要条件就是:
3^k ≥ 2N
把k=3代进去,得到3^3=27 ≥ 2N,所以N最大是13。从纯信息量角度看,3次称量理论上最多可以处理13个未知轻重的球。这就是为什么13球问题存在“找出坏球”的解,也解释了为什么12球没有触及信息量天花板,留出了判断轻重的空间。
2.3 为什么“能判断轻重”要更严格
只找出坏球和“找出并判断轻重”是两个难度级别。如果只要求找出坏球,13个球、3次称量是可以做到的,因为27种结果容纳13×2=26种状态绰绰有余。但如果要求同时判断轻重,情况就变了:球i偏轻和球i偏重对应的结果序列,在三次称量的每一位上必须严格相反。比如某球第1次放左盘,偏重时左盘重、第1次结果是“左重”,那它偏轻时第1次就必须是“右重”。
这意味着每个坏球要占用一对“镜像结果”。而三进制三位结果中,非零的镜像对一共有(27−1)/2=13对,但其中还要扣除一对待定冗余,所以能同时判断轻重的上限是(3^k−3)/2。把k=3代进去就是(27−3)/2=12。这个公式直接告诉你:12球3次就是“找出来并判断轻重”的极限,也是为什么题目偏偏选12而不是10、11或13。
3. 经典实用解法:三分组自适应的三步走
3.1 第一次称量:4 vs 4
自适应的经典思路,第一步先把12个球分成三组:A组1、2、3、4,B组5、6、7、8,C组9、10、11、12。第一次称量拿A组对B组,即1、2、3、4 vs 5、6、7、8。
这一步的意义在于:不管是平衡还是不平衡,都能把候选范围压缩到4个球左右。如果平衡,说明A组和B组都是标准球,问题球只能在C组4个里。如果不平衡,比如A组重,那说明两种情况:要么A组4个里有1个偏重,要么B组4个里有1个偏轻。这就是为什么不能简单得出“重的那边有问题”的结论,因为你不知道轻重方向。
3.2 第一次平衡时怎么处理
第一次称量结果是平衡,这是最好处理的分支。此时1到8号全部是标准球,坏球在9、10、11、12四颗里。第二次称量9、10、11 vs 1、2、3,右边放3个标准球。
如果第二次还是平衡,那坏球一定是12号。第三次拿12号 vs 1号标准球,一称便知12号偏轻还是偏重。如果第二次左边重,说明9、10、11中有一个偏重,第三次称9 vs 10,平衡就是11偏重,否则谁重谁就是坏球。如果第二次左边轻,说明9、10、11中有一个偏轻,第三次称9 vs 10,平衡就是11偏轻,否则谁轻谁就是坏球。
这个分支的核心技巧是用“标准球”当参照物。第一次平衡后你已经拥有了8个标准球,它们就像砝码一样可以随意取用,问题从“找坏球”退化成“在4个球里找1个,并且已经知道坏球就在其中”。
3.3 第一次左重时怎么处理
第一次不平衡时会复杂一些,因为存在两种嫌疑方向。以第一次左重为例,候选状态为:1、2、3、4中有一个偏重,或者5、6、7、8中有一个偏轻。注意,9到12号此时已经是标准球。
第二次采用交叉换位策略:称1、2、5 vs 3、6、9,其中9号是标准球。这个策略的精妙之处在于,它把部分“重嫌疑”的球留在左盘,部分换到右盘,还引入了一个标准球,人为制造出更细致的区分。
如果第二次平衡,说明嫌疑落在没有参加第二次称量的4号、7号、8号中。结合第一次左重的信息,只可能是4号偏重、7号偏轻或8号偏轻。第三次称7 vs 8,平衡则4号偏重,谁轻谁就是那个偏轻的坏球。
如果第二次左重,说明嫌疑落在1号、2号、6号中,对应1号偏重、2号偏重或6号偏轻。第三次称1 vs 2,平衡则6号偏轻,否则谁重谁偏重。
如果第二次右重,说明嫌疑落在3号、4号、5号中,对应3号偏重、4号偏重或5号偏轻。第三次称3 vs 4,平衡则5号偏轻,否则谁重谁偏重。
3.4 第一次右重时怎么处理(对称处理)
第一次右重不需要重新想一套方案,它完全是左重分支的镜像。把左重分支里的编号做对称映射:1↔5、2↔6、3↔7、4↔8,并把“重嫌疑”和“轻嫌疑”互换,就得到右重分支的执行方案。
第二次称5、6、1 vs 7、2、9,9号标准球。如果第二次平衡,嫌疑落在3号偏轻、4号偏轻、8号偏重中,第三次称3 vs 4,平衡则8号偏重,谁轻谁是坏球。如果第二次左重,嫌疑是5号偏重、6号偏重、2号偏轻,第三次称5 vs 6,平衡则2号偏轻,谁重谁坏。如果第二次右重,嫌疑是7号偏重、1号偏轻,第三次称7 vs 9号标准球,7重则7号偏重,平衡则1号偏轻。
这里只要掌握了“保持左右盘数量一致”和“每称一次必须让三种结果都有明确归属”两个原则,即使换了编号也不会乱。整个12球自适应方案到此闭环。
4. 进阶玩法:把三次称量写成固定编码表
4.1 编码思路:用三进制记录球的位置
自适应方案虽然直观,但它有一个隐藏缺陷:第二次、第三次称什么取决于前面的结果,不方便程序化,也不方便记忆。其实还存在一种更漂亮的方案,可以让三次称量完全固定下来,中途不做任何调整。
思路是把每个球当成一个“编码对象”。三次称量中,这个球要么放左盘,要么放右盘,要么不上秤。我们用+表示左盘,用−表示右盘,用0表示不上秤,那么每个球就有一个长度为3的编码。核心约束有三条:第一,不能出现两个球编码互为相反数,否则“甲偏重”和“乙偏轻”会产生完全相同的结果;第二,每一列的+和−数量必须相等,否则那一次称量左右盘球数不一致;第三,不能有编码全为0,否则这个球永远不上秤,坏了也判断不了轻重。
4.2 直接照抄:一套可用的固定三称方案
下面给出一套我构造并验证过的固定编码表。每个三元组按“第1次、第2次、第3次”顺序排列,+表示该次放左盘,−表示放右盘,0表示不上秤。
| 球号 | 编码 | 球号 | 编码 |
|---|---|---|---|
| 1 | + + + | 7 | 0 − − |
| 2 | + + − | 8 | 0 − + |
| 3 | + − + | 9 | − + 0 |
| 4 | + + 0 | 10 | − 0 0 |
| 5 | − 0 − | 11 | 0 − 0 |
| 6 | − 0 + | 12 | 0 0 − |
按照这个表,三次称量的具体操作就是:
- 第1次称:1、2、3、4 vs 5、6、9、10
- 第2次称:1、2、4、9 vs 3、7、8、11
- 第3次称:1、3、6、8 vs 2、5、7、12
你可以核对一下,三次称量左右盘的球数都是4对4,没有哪一次出现数量不等的情况。这也是这个方案能成立的最基本保障。
4.3 三次结果反查表
三次称完,你会得到一个形如(a, b, c)的结果向量,每一位只可能是“左重”“右重”“平衡”三种。反查规则很简单:把它与编码表对比。
如果结果向量等于某一行编码,说明这个球偏重。比如结果是(+ + −),它正好等于2号球的编码,结论就是2号球偏重。
如果结果向量等于某一行编码的相反数,也就是把编码里的+和−互换,说明这个球偏轻。比如结果是(− − +),它是2号编码(+ + −)的相反数,结论就是2号球偏轻。
由于编码表在设计时保证了不重复、不互反,所以任何一个结果向量最多只对应一个“球+状态”组合,不会出现“既像甲偏重又像乙偏轻”的二义性。我用程序对所有24种情况做过枚举,结果是完全没有冲突。
4.4 为什么编码表能保证不冲突
这套表的构造逻辑,可以理解为在27种三进制结果里挑出12对“镜像结果”,分别分配给12个球,每对镜像对应同一个球的偏重和偏轻。挑的时候有三条硬性要求:不选全0编码;不选互为相反数的两个编码;保证每一列+和−数量相等。
这三个条件缺一不可。第一列如果不平衡,第1次称量左右球数就不等,结果会预先倾斜;如果两个编码互反,那么A球偏重和B球偏轻的结果会完全相同;如果出现全0编码,一个球三次都没上过秤,即使最终锁定是它,也无法判断轻重。理解了这三条,你甚至能自己用回溯程序搜索出其他风格完全不同的编码表,每个表都能解同一道题。
5. 变体扩展:13球、N球、已知轻重
5.1 13球称3次:能找出但不保证轻重
既然三次结果有27种,而13个球未知轻重一共只有26种状态,理论上13球“找出坏球”是可行的。但前面说过,要同时判断轻重必须满足更严格的镜像配对条件,所以13球场景下会有一个球的状态无法完整判定。
具体表现是:无论怎么设计,结果序列里总会出现一种情况,能告诉你“坏球就是某个球”,却无法同时告诉你它偏轻还是偏重。你可以理解为,缺少的那一个“结果槽位”正好卡在轻重信息的关键位置。因此,13球变体更适合讨论“如何用3次称量锁定坏球”,而不是“如何用3次称量既锁定又判重”。
5.2 已知坏球偏重(或偏轻)的N球问题
如果题目提前告诉你坏球是偏重的,问题会简单很多,每次称量就退化成了标准的三分查找。第一次把球分成三等份,任意两份上天平:如果一边重,坏球就在重的那份里;如果平衡,坏球就在没上秤的那份里。一次称量把范围缩小到原来的1/3。
所以k次称量最多能处理的球数是3^k。比如3次最多可以处理27个已知偏重的球,2次就是9个。这个结果和“未知轻重”时的12球形成了鲜明对比:方向信息值多少钱?在这里,仅仅多知道一个“偏还是轻”,就能把容量从约12个提升到27个。
5.3 通用公式汇总
把几个结论放在一起,方便以后直接查:
| 场景 | k次称量最大球数 | 3次代入 |
|---|---|---|
| 未知轻重,要求找出并判断轻重 | (3^k − 3) / 2 | 12 |
| 未知轻重,只要求找出坏球 | (3^k − 1) / 2 | 13 |
| 已知坏球偏重或偏轻 | 3^k | 27 |
这些公式最大的价值是帮你判断一个变体题到底有没有解。如果出题人考你“16个球,3次,未知轻重,找出并判断轻重”,你可以直接说没解,因为16已经超过上限12。反过来,如果场景是“10个球,3次,未知轻重”,你有充足的裕量,甚至可以采用更简单粗暴的分组方式。
5.4 程序化验证的入口
有了编码表,可以用一小段Python代码验证方案是否真的无冲突。思路是把每个“球+状态”代入编码表,计算它对应的三次结果向量,然后检查是否有两个不同状态产生同一个向量。
codes = { 1: (+1, +1, +1), # 球1:三次都在左盘 2: (+1, +1, -1), 3: (+1, -1, +1), 4: (+1, +1, 0), 5: (-1, 0, -1), 6: (-1, 0, +1), 7: ( 0, -1, -1), 8: ( 0, -1, +1), 9: (-1, +1, 0), 10: (-1, 0, 0), 11: ( 0, -1, 0), 12: ( 0, 0, -1), } results = {} for ball, code in codes.items(): for weight in (+1, -1): # +1 偏重,-1 偏轻 r = tuple(code[i] * weight for i in range(3)) results.setdefault(r, []).append((ball, weight)) conflict = [r for r, cases in results.items() if len(cases) > 1] print("冲突数量:", len(conflict))运行结果会输出“冲突数量: 0”,说明这套固定三称方案在全部24种情况下都能得到唯一结论。如果你想自己设计一套新编码表,也可以把这段代码当作验证器,配合回溯搜索函数使用。
6. 实操踩坑记录与经验心得
6.1 最容易翻车的三个细节
第一个坑是第一次称量就贪多。有人觉得“多称几个球效率更高”,直接拿6个球对6个球上天平。结果一旦不平衡,面对的是6个偏重嫌疑和6个偏轻嫌疑混合在一起的12种情况,后续两次根本拆不完。这也是这道题反直觉的地方:你一次称的量太多,反而把剩余可能性压得不够均匀。
第二个坑是忽略标准球。第一次称量平衡后,你已经拥有了8个标准球,它们是后续解题的关键砝码。很多人在这一步还在“用未知球去称未知球”,其实大可以大胆地把标准球放上天平,这会让判断轻松很多。
第三个坑是编码方案里出现互反编码。我最初尝试固定三称方案时,就踩过这个坑:看似左右盘数量都平衡,编码也不重复,但两组编码互为相反数,导致“甲偏重”和“乙偏轻”的结果一模一样,直到写程序验证才发现。所以只要涉及编码类方案,程序化穷举验证永远是最后一道安全网。
6.2 常见问题速查表
| 问题 | 快速答案 |
|---|---|
| 第一次应该称几个球? | 12个球时称4 vs 4,保证每种分支剩余候选数均衡 |
| 第一次不平衡时,重的那一侧一定有坏球吗? | 不一定,可能是轻的一侧中有球偏轻 |
| 已有一个标准球时该怎么做? | 尽量把标准球放上天平作参照,可以大幅简化判断 |
| 13个球3次能判断轻重吗? | 只能保证找出坏球,不能保证同时判断轻重 |
| 编码表中为什么不能有互为相反数的编码? | 否则“A偏重”和“B偏轻”会产生相同结果序列 |
| 怎么快速验证一套方案? | 把所有“球+状态”仿真一遍,检查结果向量是否唯一 |
6.3 个人经验:这个题练的是什么
我在实际研究这个问题时,最大的体会是:它练的不是“会称球”,而是“会系统性地逼近结论”。从信息量估算极限、到三分支切割候选集、再到编码表设计,每一步都逼着你把“下一步该做什么”想清楚,而不是凭感觉操作。我做程序化验证时,最快定位问题的办法不是反复试称重方案,而是先把所有“球+状态”穷举一遍,看哪个结果冲突了,再倒推是哪一步编码设计不满足约束。
如果你也想把这道题变成自己的“思维肌肉记忆”,我建议你找一张纸和一支笔,先不看任何答案,自己从“每次称量有三种结果”这句话出发,试着推一套12球方案。推不出来也没关系,再对照这篇文章的编码表,一行一行验证“为什么这样可以”。几次之后,你会发现自己面对这种多分支问题时,不再只靠背套路,而是能真的从原理出发推导方案了。