一、问题描述

  在12个外观完全相同的小球中,有一个与其它球重量不同。如何只用一架天平找到这个球并判断它比其它球轻还是重?最少需要称几次?39个球呢?

二、问题分析

  这是一个很经典的信息论问题,最开始的思路是用分组称重的方法,发现每次测量的次数不固定,而且当球数增多时称重次数会明显增加,所以不是次数最优的方法,于是参考了网上编码的方法并做了一点调整。
  一共有12个小球,每个小球都可能偏轻或偏重,共有24种可能,其信息量为 log224≈4.58bitlog2⁡24≈4.58bitlog_2⁡24≈4.58bit。利用天平进行称重,天平每次显示结果可能为左倾、右倾或平衡三种,假如出现每种情况的概率相同,则每次称重结果的信息量为 log23≈1.58bitlog2⁡3≈1.58bitlog_2⁡3≈1.58bit。4.58bit1.58bit≈34.58bit1.58bit≈3\frac{4.58bit}{1.58bit}≈3,所以理论上3次称重可以找出质量不同的球并判断出它是偏轻还是偏重。
  根据文献The Problem of the Pennies, F. J. Dyson, The Mathematical Gazette , Vol. 30, No. 291 (Oct., 1946), pp. 231-234 的证明可知,N次称重最多可以从3N−323N−32\frac{3^N-3}{2}个小球中找出不同的球,当N=3时可以判断12个小球的质量。

三、算法设计

  要以最少次数称重得到结果,则每次称重应获取尽可能多的信息,也就是每次天平称重结果都应包含左倾、右倾、平衡三种可能的情况。于是称重时小球应该分为三堆,特殊小球随机分布于其中一堆,选择两堆置于天平两侧。对此,可以将每个小球进行编码,每次称重时根据编码选择三分之二的小球,对称重结果进行记录,并根据每次称重结果,找出异常小球,

3.1 编码要求

  由于天平结果有三种,我们选择 ‘0’,‘1’,‘2’ 三种字符对小球进行编码,12个小球三次可以测出,因此每个小球由三位编码构成,编码应满足以下三个条件:

  • 编码互不相同;
    保证编码唯一性,每个编码对应唯一小球。
  • 编码每一位均含有4个‘0’,‘1’,‘2’;
    例如12个小球对应12个编码,这12个编码的第一位中‘0’,‘1’,‘2’各出现四次,第二第三位同理。目的是为了保证每次称重小球均分为三份。
  • 每个小球编码对其中的‘0’,‘1’同时取反后,反编码不存在于已编好的序列中。
    取反指编码中的‘0’变为‘1’,‘1’变为‘0’。结合前两条,该条件可以保证每个小球至少上一次天平,同时保证根据结果能够正确找出异常小球。

下一节将具体介绍这样编码的原因,以及利用这些编码称重的过程。

3.2 称重方法

  每次称重天平结果有左倾、右倾、平衡三种,因此需要三种字符对小球进行编码,这里使用 ‘0’,‘1’,‘2’ ;12个球需要称重三次,则每个编码用三位字符表示。根据小球编码判断每次称重时该小球的位置。第 iii 次称重,选择编码第 i" role="presentation">iii 位为‘0’的小球置于左盘,第 iii 位为‘1’的小球置于右盘,记录天平结果并生成结果序列,为了保证每次称重左右两盘球数相同,编码序列每一位应均包含相同个个数的‘0’,‘1’,‘2’。我们用heavy" role="presentation">heavyheavyheavy表示重球序列,lightlightlight表示轻球序列,序列生成规则如下:

天平结果 可能原因 处理方式
左倾 左盘含有重球 heavyi=0heavyi=0heavy_i = 0
右盘含有轻球 lighti=1lighti=1light_i = 1
右倾 右盘含有重球 heavyi=1heavyi=1heavy_i = 1
左盘含有轻球 lighti=0lighti=0light_i = 0
平衡 非标准球不在盘上 heavyi=2heavyi=2heavy_i = 2
lighti=2lighti=2light_i = 2

如第1次称重结果为天平右倾,则表明天平右盘含有一个重球,或左盘含有一个轻球,而此时天平右盘小球的第一位编码均为‘1’,左盘小球第一位均为‘0’,因此证明:异常球为重球且第一位为‘1’(heavy1=1heavy1=1heavy_1 = 1),或者异常球为轻球且第一位为‘0’(light1=0light1=0light_1 = 0)。根据该规则三次称重并记录完成后, heavyheavyheavy 与 lightlightlight 均为一个三位的序列,且两个序列第 iii 位或‘0’,‘1’相反,或同时为‘2’,根据编码条件三可知,两个编码不可能同时存在,因此存在的编码所对应的小球即是异常球,且若该编码为 heavy" role="presentation">heavyheavyheavy ,则异常小球偏重,否则偏轻。

3.3 一个例子

  以下12个小球中有一个为异常球,下面对小球进行一组可行的编码并通过三次称重将其找出。首先对小球进行编码:


图1. 12个小球及其对应编码

  进行第一次称重,将编码第一位为‘0’的小球(1,4,7,10)置于左盘,为‘1’的小球置于右盘(2,5,8,11),称重结果如下:


图2. 第一次称重结果

由于天平左倾,则表明左盘含有重球,或右盘含有轻球:heavy1=0heavy1=0heavy_1 = 0,light1=1light1=1light_1 = 1;

  第二次称重,将编码第二位为‘0’的小球置于左盘,为‘1’的置于右盘,结果如下:


图3. 第二次称重结果

天平右倾,则表明右盘含有重球,或左盘含有轻球:heavy2=1heavy2=1heavy_2 = 1,light2=0light2=0light_2 = 0;

  第三次称重,将编码第三位为‘0’的小球置于左盘,为‘1’的置于右盘,结果如下:


图4. 第三次称重结果

天平平衡,表示两边都为正常球,异常球第三位编码为‘2’:heavy3=2heavy3=2heavy_3 = 2,light3=2light3=2light_3 = 2

  根据三次称重可得出 heavy=012heavy=012heavy=012,light=102light=102light=102,表明可能存在编码为‘012’的重球,或为‘102’的轻球。通过与图1中编码进行比较,发现‘012’对应10号小球,‘102’未对应小球,则证明10号小球偏重。这与称重结果一致。

3.4 编码方式
  • [原理][1]
    在编程实现过程中,如果依照3.1中所列条件直接生成编码难度较大,因此可以引入下面的方法:
      首次不同的相邻两个字符如果为01, 12, 20,则称这个编码为正序码,如0101010, 11212120, 220202002均为正序码,若首次不同的字符为10, 21, 02,则为逆序码。将一个正序码中0,1取反后将变为一个逆序码,因此对于一系列N位的正序码(或逆序码)集合,会同时满足条件1,3 。对于N位编码,它的全排列集合中除去平序码(全为0,1或2)后,剩余编码或为正序或为逆序,且正序码与逆序码数量相同,各为一半。由于N位编码的全排列数目为 3N3N3^N,因此它的正序码数目为3N−323N−32\frac{3^N-3}{2},当 N=3N=3N=3 时,正序码数目为12,因此所有的三位正序码(或逆序码)恰好符合12个小球的编码要求,即为图1所列。

  • 算法
      对于一个正序码,若将其中的0换为1、1换为2、2换为0,所得到的编码仍为正序,我们可以根据该特点生成编码。当 N=3N=3N=3 时,首先生成两位编码的全排列集合,在每个编码前补一位0构成三位;再找出其中的正序码,判断方法为令每个编码的后一位减去前一位,当首次非零的值为1或-2时,表示该编码正序,否则为逆序。此时会得到所有首位为0的三位正序码,之后依次将每个编码中的字符按上述特点进行变换,并将新编码加入编码集合直到编码重复为止,至此将得到所有三位正序码。

四、算法推广

4.1 称重方法

  以上方法可以完成12个小球的称重,进一步思考可知,3.1中规定的条件3是为了根据编码唯一确定异常小球的编号,条件2是为了保证每次称量时天平两边球数相等,因此可以修改条件2为“编码每一位所含‘0’,‘1’的数量相等”。在调整编码条件后,只要编码同时满足上述三个条件, 仍然可以完成称量。此时该算法可推广至球数为3的倍数的情况。

  当球数不为3的倍数时,小球编码无法满足条件2,这将导致称重过程中左右两盘球数可能不同,无法完成称重。对于这种情况,可以采用补充法或去除法来解决,即当两个盘球数不等时,从球数多的盘中取出多余的标准球或将若干标准球放入球数少的盘。而标准球可以从第一次称重结果中获取,这时只需满足第一次称重左右两盘球数相同即可。
  以补充法为例:当总球数除以3余1时,对于整除部分采用上述3的倍数进行编码,多余一个球的编码在满足条件1,3的同时,使其首位编码为2即可;当总球数除以3余2时,使多余两个球首位编码分别为0,1即可。由此该算法可推广至球数大于3的所有情况。

4.2 编码方法

  3.4的算法是针对12个小球提出的,但稍加改进即可用于球数为3的倍数或者更广的情况。对于N位全排列编码,每一位均累计有 3N33N3\frac{3^N}{3} 个0,1或2,去除其中平序码后,每位均有 3N3−33N3−3\frac{3^N}{3}-3 个0,1,2,而对于所有正序码(或逆序码)而言,他们每一位均有 3N−363N−36\frac{3^N-3}{6} 个0,1,2,显然0,1的数量是相等的,因此符合上述三个条件的编码可以从N位正序码中获得。

  当球数为3的倍数时,首先根据问题分析中的方法计算所需称重次数,即小球编码位数N;之后生成N-1位编码的全排列集合,在每位编码前补0构成N位,删去其中的非正序码;此时得到所有首位为0的N位正序码,取其中一个进行两次轮换,将三个编码(每一位分别为0,1,2)并分别对应前三个小球,再取下一个进行两次轮换,对应之后三个小球,重复该步骤直到每个小球均对应一个编码为止,此时小球编码符合三个条件,可以进行称重。
  当球数不为3的倍数时,同样先计算称重次数N,并生成所有首位为0的N位正序码,之后对3的整数倍的小球利用上述方法进行编码。当余下一个小球时,取下一个编码轮换两次之后首位为2的编码与之对应;余下两个小球时,取下一个首位为0的编码以及它轮换一次后首位为1的编码分别对应,此时可以运用补充法或去除法进行称重。

五、小结

  这是去年信息论的一份作业,方法上借鉴了很多网上大神的想法,但应该是我用Python写的第一个比较复杂的项目,而且展示时候的效果还不错,所以感觉还是蛮有意义的。核心的地方应该还是生成正序编码部分,花的时间最长,主要思路是生成N-1位的全排列编码—>前面补0—>选出正序部分,生成全排列的时候用到了递归,复杂度比较高,而且写得很复杂,后面发现其实也可以用十进制和三进制转化来的,应该会简洁很多。当初前后写了三个版本,分别是12个球的、3的倍数的、任意球数的,这里把任意球数的代码贴在下面,欢迎讨论。

import random
#导入随机数模块,用于随机产生质量不同的小球def InputBallnum():#输入球数,并计算所需称重次数,返回球数,称重次数,除以三的余数n = eval(input("请输入球数 >> "))while not isinstance(n,int):n = eval(input("输入有误,请重新输入 >> "))m = n%3for times in range(n):if (3**times-3)/2 >= n:print("%d个球%d次可以完成称重!"%(n,times))print() return times,n,mdef StarCode(t):#输入称重次数即编码位数t,返回所有t位首位为0的正序编码if t >= 3:star = ['0','1','2']        #初始化列表,元素为三进制的三个字符str1 = Make_Num(t-2,star)   #产生一个t-1位由0,1,2组成的所有编码组合的列表for i in range(len(str1)):str1[i] = '0'+str1[i]   #在t位所有编码前添加一个0for string in str1[:]:#从列表中排除所有非正序的编码for i in range(t-1):if (int(string[i+1]) - int(string[i]) == 1) or (int(string[i+1]) - int(string[i])) == -2:#后一个数字减前一个数字为1或-2,则该编码为正序breakelif (int(string[i+1]) - int(string[i]) == -1) or ((int(string[i+1]) - int(string[i])) == 2) or ((i == t-2) and (int(string[i+1]) - int(string[i])) == 0):#后一个数字减前一个数字为-1或2,或所有数字相同,则该序列非正序str1.remove(string)breakreturn str1else:return star def Make_Num(n,list):#利用递归调用生成n+1位由0,1,2组成的所有编码组合的列表if n:for i in range(len(list)):for j in range(3):list.append(list[i]+str(j))for i in range(int(len(list)/4)):list.remove(list[0])Make_Num(n-1,list)return listdef FinalCode(n,list,m):#输入球数n,首位为0的t位正序码列表,余数,返回n个符合条件的正序码#该步骤为将0替换为1,1替换为2,2替换为0,并将新编码加入列表if m == 0:t = n//3else:t = n//3 + 1templist = []for i in range(t):templist.append(list[i])temp1 = temp2 = ''for j in range(len(list[0])):#第一层替换l = int(list[i][j])+1if l == 3:l = 0temp1 += str(l)for k in range(len(temp1)):#第二层替换ll = int(temp1[k]) + 1if ll == 3:ll = 0temp2 += str(ll)templist.append(temp1)templist.append(temp2)return templistdef SetBall(n,list,m):#生成表示小球的列表并返回,元素分别为编号、质量、编码ball = []for i in range(n-m):fig = '('+str(i+1)+')'  #小球编号表示如"(10)"#按顺序将以上产生的正序编码赋给每个小球,质量设为10ball.append([fig,10,list[i]])if m == 1:#当球数非三的倍数时,需额外生成多余的小球ball.append(['('+str(n-m+1)+')',10,list[-1]])elif m == 2:ball.append(['('+str(n-m+1)+')',10,list[-3]])ball.append(['('+str(n-m+2)+')',10,list[-2]])return balldef ChangeWeight(list):#改变一个小球的质量,手动输入或随机生成n = len(list)select = input("手动选择(Y)或随机生成(N)质量不同的球 >> ")while select not in 'YyNn':select = input("输入有误!请重新输入 >> ")if select == 'Y' or select == 'y':num = int(input("请在1~%d中选择一个球,改变其质量 >> "%n))while num not in range(1,n+1):num = int(input("输入有误!请重新选择 >> "))weit = eval(input("请选择增加质量(1)或减少质量(-1) >> "))while weit != 1 and weit != -1:weit = eval(input("输入有误!请重新输入 >> "))list[num-1][1] += weitelif select == 'N' or select =='n':#随机取一个小球,将其质量+1或-1list[random.randint(0,n-1)][1] += random.choice([1,-1])return listdef Weight(n,list):#对球按编码进行称重,第i次取第i位编码为0和1的小球并分别置于左右两盘,#即每次称重左盘均表示0,右盘为1,根据每次称重天平的结果(轻、重、平#衡)添加对应编码print()print("============================以下为称量结果============================")print()model = []      #生成标准球列表light = heavy = ''print("第1次称重结果为: ")left = right = 0        #每次称重前将左右两盘清空fig_left = fig_right = ''   #编号清空for j in range(len(list)):#遍历每一个小球if (list[j][2][0] == '0'):#判断第j个小球编码的第1位是否为0,若是将质量加于左盘,记录编号left += list[j][1]  fig_left += list[j][0]elif (list[j][2][0] == '1'):#判断第j个小球编码的第1位是否为1,若是将质量加于右盘,记录编号right += list[j][1]fig_right += list[j][0]else:continueif left == right:#若天平平衡,轻编码添加2,重编码添加2,打印称重结果,将盘中所有球#设为标准球light += '2' heavy += '2'print(fig_left,"=",fig_right)for k in range(len(list)):if list[k][2][0] == '0' or list[k][2][0] == '1':model.append(list[k])print()elif left < right:#若天平左倾,轻编码添加0,重编码添加1,打印称重结果,将盘外所有球#设为标准球light += '0'heavy += '1'print(fig_left,"<",fig_right)for k in range(len(list)):if list[k][2][0] == '2':model.append(list[k])print()else: #若天平右倾,轻编码添加1,重编码添加0,打印称重结果,将盘外所有球#设为标准球light += '1'heavy += '0'print(fig_left,">",fig_right)for k in range(len(list)):if list[k][2][0] == '2':model.append(list[k])print()print('轻球编码为:'+ light,'或重球编码为:'+ heavy)a = input()for i in range(1,n):ball_temp = []print("第%d次称重结果为: "%(i+1))left = right = 0    #每次称重前将左右两盘清空count_l = count_r = 0  #用于对盘中小球计数fig_left = fig_right = ''   #清空编号for j in range(len(list)):#遍历每一个小球if (list[j][2][i] == '0'):#判断第j个小球编码的第i位是否为0,若是将质量加于左盘,记录编号#左盘球数加一ball_temp.append(list[j])left += list[j][1]  fig_left += list[j][0]count_l += 1elif (list[j][2][i] == '1'):#判断第j个小球编码的第i位是否为1,若是将质量加于右盘,记录编号#右盘球数加一ball_temp.append(list[j])right += list[j][1]fig_right += list[j][0]count_r += 1elif (list[j][2][i] == '2'):continueif count_l < count_r:#若右盘球数多余左盘,则将一个标准球加于左盘for k in model:#检查标准球是否已在盘中if k not in ball_temp:left += k[1]fig_left += k[0]breakelif count_l > count_r:for k in model:if k not in ball_temp:right += k[1]fig_right += k[0]breakif left < right:#若天平左倾,轻编码添加0,重编码添加1,打印称重结果light += '0'heavy += '1'print(fig_left,"<",fig_right)print()elif left > right:#若天平右倾,轻编码添加1,重编码添加0,打印称重结果light += '1'heavy += '0'print(fig_left,">",fig_right)print()elif left == right:#若天平平衡,轻编码添加2,重编码添加2,打印称重结果light += '2' heavy += '2'print(fig_left,"=",fig_right)print()print('轻球编码为:'+ light,'或重球编码为:'+ heavy)print()a = input()print()return light , heavydef CompareWeight(light,heavy,list):#根据轻重编码查找对应小球n = len(list)for i in range(n):if light in list[i]:print("对比小球编码可知:球 %s 较轻"%(list[i][0]))if heavy in list[i]:print("对比小球编码可知:球 %s 较重"%(list[i][0]))a = input()def PrintCode(list):#打印所有小球编号及其编码,用于对比实验结果print()print("===========================以下为各球编码===========================")print()n = len(list)for i in range(n):print(list[i][0],":",list[i][2])def PrintWeight(list):#打印所有小球编号及其质量,用于对比实验结果print()print("=========================以下为各球实际质量=========================")print()n = len(list)for i in range(n):print(list[i][0],":",list[i][1])def main():#输入球数,并计算所需称重次数,球数除以三所得余数n_times,n_balls,n_remain = InputBallnum()#根据称重次数产生一个编码序列Star_Code = StarCode(n_times)Fina_Code = FinalCode(n_balls,Star_Code,n_remain)#根据以上编码序列生成一个表示小球的列表,元素包含小球编码、质量、标号init_ball = SetBall(n_balls,Fina_Code,n_remain)#改变一个小球的质量,手动输入或随机生成ball = ChangeWeight(init_ball) #打印小球编码PrintCode(ball)a = input()#根据编码逐次称重、返回称重结果的编码light, heavy = Weight(n_times,ball) #将结果编码与小球编码进行对比,找出质量不同的小球,打印其编号CompareWeight(light,heavy,ball)#打印所有小球编号及其质量PrintWeight(ball)a = input()main()

参考资料

[1] https://funnyjs.com/ballweight/
[2] https://www.zhihu.com/question/23505253

十二个小球称重问题及其Python实现相关推荐

  1. python小球方案问题_十二个小球称重问题及其Python实现

    一.问题描述 在12个外观完全相同的小球中,有一个与其它球重量不同.如何只用一架天平找到这个球并判断它比其它球轻还是重?最少需要称几次?39个球呢? 二.问题分析 这是一个很经典的信息论问题,最开始的 ...

  2. c语言中编程称量十二个小球的质量,c语言题:现有十二个小球,其中一个小球的重量与其... 有12个外观相同的小球,其中11个是标准球,质量完......

    导航:网站首页 > c语言题:现有十二个小球,其中一个小球的重量与其... 有12个外观相同的小球,其中11个是标准球,质量完... c语言题:现有十二个小球,其中一个小球的重量与其... 有1 ...

  3. 小球称重的解法整合 N个小球有一个坏球,最少几次能找出坏球

    小球称重的解法整合 N个小球有一个坏球,最少几次能找出坏球     最近在看小球称重的问题,之前只记住了公式,最近回过来看,发现知乎上多了一些优质答案,遂记录一下. 现附上学习地址 先说结论, N 个 ...

  4. matlab最小字典序,12小球称重问题 完美解决方案(转自 skywind.name)

    网上 有好几种 解决方案,skywind.name上的这个最棒, 赞一下! 可惜 偶 苦思冥想 3 个小时,思路换了3.4种,也没找到完美解决方案. 以下 是 转自 skywind.name 12个小 ...

  5. 12球称重C语言算法,12小球称重问题

    题目: 已知有12个小球,一样的形状和外观,其中有一个是次品,现在给你一个无砝码的天平,称三次, 把这个次品找出来,并且求出这个次品相对真品是偏重还是偏轻? 解答: 首先把小球分成三堆,每堆四个. A ...

  6. 十二个小球,一个坏球,3次比较找出坏的那个

    参考链接: https://blog.csdn.net/qq_34192115/article/details/80425120 https://blog.csdn.net/iamthedoctor1 ...

  7. 十二个球称三次C语言编程,十二个球,有一个不知轻重,现有一个天平,称三次,找出此球!...

    平均分成A.B.C三组,每组4个: 第一秤:A.B两组先分别放天平左右: 情况一:平衡.则问题出在C组,A.B组共8个为标准球. 第二秤用3个标准球和C组的3个球对比, 如果第二秤平衡,剩下的一个就是 ...

  8. 入门机器学习(十二)--课后作业解析-偏差与方差(Python 实现)

    在本次作业中,我们要完成的是预测水库水位的变化预测大坝流出的水量.已知特征为水库的水位,要预测的y是大坝流出的水量. 编程作业 5 - 偏差和方差 这次练习我们将会看到如何使用课上的方法改进机器学习算 ...

  9. 机器学习100天(二十二):022 分类模型评价指标-Python实现

    机器学习100天!今天讲的是:分类模型评价指标-Python实现! <机器学习100天>完整目录:目录 打开spyder,首先,导入标准库. import numpy as np impo ...

最新文章

  1. 模拟人生畅玩版android,模拟人生畅玩版手机版
  2. springcloud eureka集群_SpringCloud入门学习之Eureka
  3. Cisco iOS的两种配置文件(思科命令的保存)
  4. python str函数数字转换成字符串,Pandas将数字转换为字符串意外结果
  5. verilog 3段式状态机
  6. Echarts2的使用——绘制中国地图
  7. 计算机电源性能怎么调,笔记本电脑如何将电源模式更改为高性能模式? -
  8. 基于MATLAB的数字图像处理系统设计
  9. C语言递归解决水洼问题
  10. Skype for Business支持离线消息啦
  11. C++Singleton模式
  12. Java异步编程Future应用
  13. JPA之EntityManager踩坑笔记:更改PersistenceContext
  14. 收集了一些Weblogic监控工具
  15. 最大公约数、最小公倍数
  16. 云南旅游之瑞丽,束河,丽江古城
  17. 【KEIL5】是时候给你的Keil换个好看的皮肤了(MDK)
  18. 【Houdini官方入门教程翻译】概述——UV和纹理贴图
  19. 一键获取电脑的系统安装时间、硬盘序列号、MAC地址、补丁安装信息
  20. [夜寂]Spring中Resource接口有关的实现

热门文章

  1. crontab中如何实现每隔多少天执行一次脚本
  2. Python代码量为嘛那么少?
  3. 输入三个数,判断能否以这三个数构成一个三角形,若能则输出所构成三角形的类别(等边、等腰、直角或斜三角形)
  4. 手机密码锁机的朋友不用再去营业厅解锁.自己搞定(未测试)
  5. dnf自动刷图python脚本_python+selenium自动化登录dnf11周年活动界面领取奖励登录部分采坑总结[1]...
  6. Unity Android 前台后台切换判断
  7. 读取MP3文件内的ID3V2专辑图片并显示
  8. Hive-SQL工作中常用函数总结及案例实战
  9. CSS3动画属性 animation详解(看完就会)
  10. 前端水平垂直居中实现