Python解决12小球问题

三进制

12小球问题就是说有12个一模一样的小球,外表看不出区别,但是其中有且仅有一个小球是坏球,它比其他球可能重一点,也可能轻一点。给你一架天平,请只称三次就找到这个小球。上一篇博客已经说明了解这个问题的手工解,这次我们试图编写一个计算机算法来解决这个问题,难度显然要大多了。

解决这个问题之前,我们先来谈谈数的进制。为什么人类习惯使用十进制呢?因为人的两只手恰好有十根手指头,十进制计数方便。如果外星人有八个手指头,那他们极有可能常用的是八进制而不是我们认为理所当然的十进制。计算机中主要使用的是二进制,为什么选择二进制?因为绝大多数电子元器件的状态有两种:接通或关闭、电流高或低、磁性有或无。如果外星人的元器件的状态有三种的话我相信他们的计算机可能会使用三进制。

所以二进制也好,十进制也好,八进制也好,甚至三进制也好,本身并无孰优孰劣。计数和计算的能力也是相同的(如果你愿意甚至用一进制也行)。差别仅在于用起来是否方便。比如中国的算盘就是五进制的。在计算机发明以前,算盘是世界上最快的计算工具之一。直到进入21世纪,在中国的一些企事业单位的财务部门里仍能看到算盘的身影。你能说五进制的计算能力就差了吗?

作者前面写过一篇讲猜姓氏问题的博客,一个姓氏在或者不在纸上构成了姓氏的一个二进制编码。而12个小球问题中,天平有三个可能的状态:左边低右边高、左边高右边低或者平衡。这就提示了我们是否可以用三进制帮助我们解决这个困难的12小球问题呢?

 小球的三进制编码

答案是肯定的。我们可以用若干位三进制编码给所有小球编码。比如,我们不妨把一号球编码为“001”。这个编码是什么意思呢?我们可以这样解释:3位编码表示我们一共称了3次天平。每一位的编码可以取值0、1或者2,其含义是:

1:表示左边低右边高。

2:表示左边高右边低。

0:表示天平平衡。

则一号球的编码“001”的含义是:如果一号球是坏球的话,三次称天平的结果分别是:

平衡(0)、平衡(0)、左边低(1)

这说明,作为坏球,一号球没有参与第一次和第二次称天平,但参与了第三次称天平。这很好理解吧?如果一个小球的编码是“122”,这说明,如果该球是坏球的话,它参与了三次称天平,并且天平分别呈现:左低(1)、右低(2)和右低(2)。

小球应放在天平的哪一端?

编码“001”说明了当前小球仅参与了第三次称天平,并且天平左边低。但是该编码并没有说明在第三次称天平时一号球放在了天平的哪一端。假设数字“1”意味着小球是放在天平左边的。现在天平左边低,这就说明,一号球是个重球。

作为一个坏球,如果一号球是个轻球,这意味着第三次称天平的结果是右边低。所以,一号球实际有两个编码,“001”是它作为重球的编码,“002”是它作为轻球的编码。

同一个小球的轻重两个编码是有联系的。比如,假设一个小球作为轻球的编码是“102”,则它作为重球的编码必然是“201”。即同一个小球轻重两个编码在同一位上的值之间具有互补关系。如果一个球的轻球编码第一位是“1”的话,则它的重球编码的第一位必然是“2”。反之亦然。只有当小球的轻球编码在某个位置上的值是“0”时,它的重球编码在同一个位置处的值必然也是“0”

这是因为小球只要作为坏球参与了某次称天平,并且固定放在某一边,则作为轻球导致天平呈现的状态必然与作为重球导致天平所呈现的状态互补。

小球的初步编码

综上所述,我们为12个小球进行初步编码。步骤是这样的:按三进制从001、002、010、011、012、020......的次序依次给每个小球轻重两种情况进行编码。比如:001给1号重球,则1号轻球的编码必然是002。接着010给2号重球,则2号轻球的编码必然是020。接下来011给3号重球,3号轻球编码必然是022。以此类推,给出所有小球的初步编码如下:

小球

1

001

002

2

010

020

3

011

022

4

012

021

5

100

200

6

101

202

7

102

201

8

110

220

9

111

222

10

112

221

11

120

210

12

121

212

重码

在上述编码中,由于同一个小球的轻重两个编码是互补的,所以我们只用看一种编码即可。下面我们仅关注所有小球的重球编码(简称重码)。纵向地看各小球的重码,每一列代表本次称天平要用到哪些小球。其中“0”表示对应的小球没有出现,“1”表示该小球出现在天平的左边,“2”表示它出现在右边。

所以,从左往右,纵向地看每一个小球的编码,我们得出结论:第一次称天平时,左边是5号、6号、...、12号共8个小球,右边是0个小球!显然这是不合理的。每次称天平,两边应该有相同数量的小球,否则无论天平出现什么状态都不能说明任何问题。

优化三进制编码

解决上述小球数量不匹配问题的办法就是交换部分小球的轻重编码,使得每次称天平时两边放上相同数量的小球。同一个小球轻重编码交换会不会影响最终的判断?不会。假设某小球的重码是102(轻码必然是201),这意味着第一次称天平时该小球应该出现在天平的左边,这样才能保证第一次称天平出现左边低的状态(即状态“1”)。现在交换轻重码,重码变成了201,其中第一位的编码“2”意味着小球应该放在右边。重球在右边,当然会导致天平出现状态“2”(即右边低),天平的状态与小球的编码一致。所以交换小球的轻重码完全没有问题。

下面统计一下三次称天平左右两边的小球数量:

编码

第一次称

第二次称

第三次称

左边

“1”

8

6

5

右边

“2”

0

2

3

根据这个统计表,在第一次称天平时左右两边的小球数量分别是8和0 。这意味着我们应该交换四个以“1”打头的编码。假设这四个小球分别是W、X、Y、Z。

这种交换必然会影响第二次和第三次称时天平左右两边小球数量。上面统计表告诉我们第二次称球时的左右小球数量分别是6和2,也就是说需要交换两个编码。所以W、X、Y、Z中必然有两个小球的重码的第二位是“0”,另外两个小球的重码的第二位是“1”。假设W和X的第二位是“0”,Y、Z的第二位是“1”。也就是说,

W和X的重球编码形如10*

Y和Z的重球编码形如11*

其中*表示0、1或者2。

第三次称天平时左右两边小球的数量是5和3。我们可以令W和X的第三位互补,Y和Z的重码的第三位分别是“0”和“1”。反之亦然。

综上所述,W、X、Y、Z的重码分别是101、102、110和111。我们只需交换这四个小球的轻重编码即可。最终,我们得到如下所示的优化三进制编码:

12小球的优化三进制编码

小球

1

001

002

2

010

020

3

011

022

4

012

021

5

100

200

6

202

101

7

201

102

8

220

110

9

222

111

10

112

221

11

120

210

12

121

212

其中黑体字部分就是交换过的编码。当然你也可以交换100、101、111和112的轻重码,最终效果是一样的。

称天平

根据上述优化三进制编码我们就知道每次称天平时左右两边应该放什么球了:

左边

右边

第一次

5、10、11、12

6、7、8、9

第二次

2、3、4、10

8、9、11、12

第三次

1、3、7、12

4、6、9、10

填写好这个表格后再编写程序就很简单了:

# 代码:解决12小球问题

# 给出每个小球的轻重编码:
CODES=[
['001',	'002'],
['010',	'020'],
['011',	'022'],
['012',	'021'],
['100',	'200'],
['202',	'101'],
['201',	'102'],
['220',	'110'],
['222',	'111'],
['112',	'221'],
['120',	'210'],
['121',	'212']]


def get_balls(i):
    # 根据第i列每个小球的重码把小球左右分开
    left = []
    right = []
    for index, (code, _) in enumerate(CODES):
        if code[i] == '1':
            left.append(index+1)  # 小球的编号从1开始
        elif code[i] == '2':
            right.append(index+1)
    return left, right

def find_ball(code):
    for i, (heavy, light) in enumerate(CODES):
        if(heavy == code):
            return i+1, 'heavy'
        elif(light == code):
            return i+1, 'light'
    return None, None

if __name__ == '__main__':
    print('请指定1~12中一个小球是坏球,并且指定它比好球重一点还是轻一点')
    answers = []
    for i in range(3):
        left, right = get_balls(i)  # 从编码获取天平两边的小球
        print('第%d次称天平,请告知天平的状态' % (i+1))
        print('天平左边的小球:', left, '\t右边的小球:', right)
        answer = input('请输入(0-平衡, 1-左边低, 2-右边低)')
        answers.append(answer)
    answers = ''.join(answers)
    ball, heavy = find_ball(answers)
    if ball is None:
        print('错误的输入')
    else:
        print('%d号球是坏球,并且是%s球' % (ball, '重' if heavy else '轻'))

写好这个程序后,我请我太太默选一个小球当作坏球,不要告诉我它是轻的还是重的,只需告诉我天平每次的状态。她说:第一次左边低,第二次平,第三次左边低。于是我就得到编码“101”,然后查上表得知这个坏球是6号球,并且是个轻球。于是我太太夸我真聪明并给了我一个香吻。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

方林博士

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值