十二个小球称重问题及其Python实现

一、问题描述

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

二、问题分析

  这是一个很经典的信息论问题,最开始的思路是用分组称重的方法,发现每次测量的次数不固定,而且当球数增多时称重次数会明显增加,所以不是次数最优的方法,于是参考了网上编码的方法并做了一点调整。
  一共有12个小球,每个小球都可能偏轻或偏重,共有24种可能,其信息量为 log2244.58bit l o g 2 ⁡ 24 ≈ 4.58 b i t 。利用天平进行称重,天平每次显示结果可能为左倾、右倾或平衡三种,假如出现每种情况的概率相同,则每次称重结果的信息量为 log231.58bit l o g 2 ⁡ 3 ≈ 1.58 b i t 4.58bit1.58bit3 4.58 b i t 1.58 b i t ≈ 3 ,所以理论上3次称重可以找出质量不同的球并判断出它是偏轻还是偏重。
  根据文献The Problem of the Pennies, F. J. Dyson, The Mathematical Gazette , Vol. 30, No. 291 (Oct., 1946), pp. 231-234 的证明可知,N次称重最多可以从 3N32 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个球需要称重三次,则每个编码用三位字符表示。根据小球编码判断每次称重时该小球的位置。第 i i 次称重,选择编码第 i 位为‘0’的小球置于左盘,第 i i 位为‘1’的小球置于右盘,记录天平结果并生成结果序列,为了保证每次称重左右两盘球数相同,编码序列每一位应均包含相同个个数的‘0’,‘1’,‘2’。我们用heavy表示重球序列, light l i g h t 表示轻球序列,序列生成规则如下:

天平结果可能原因处理方式
左倾左盘含有重球 heavyi=0 h e a v y i = 0
右盘含有轻球 lighti=1 l i g h t i = 1
右倾右盘含有重球 heavyi=1 h e a v y i = 1
左盘含有轻球 lighti=0 l i g h t i = 0
平衡非标准球不在盘上 heavyi=2 h e a v y i = 2
lighti=2 l i g h t i = 2

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

3.3 一个例子

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


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

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


图2. 第一次称重结果

由于天平左倾,则表明左盘含有重球,或右盘含有轻球: heavy1=0 h e a v y 1 = 0 light1=1 l i g h t 1 = 1

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


图3. 第二次称重结果

天平右倾,则表明右盘含有重球,或左盘含有轻球: heavy2=1 h e a v y 2 = 1 light2=0 l i g h t 2 = 0

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


图4. 第三次称重结果

天平平衡,表示两边都为正常球,异常球第三位编码为‘2’: heavy3=2 h e a v y 3 = 2 light3=2 l i g h t 3 = 2

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

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

  • 算法
      对于一个正序码,若将其中的0换为1、1换为2、2换为0,所得到的编码仍为正序,我们可以根据该特点生成编码。当 N=3 N = 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位全排列编码,每一位均累计有 3N3 3 N 3 个0,1或2,去除其中平序码后,每位均有 3N33 3 N 3 − 3 个0,1,2,而对于所有正序码(或逆序码)而言,他们每一位均有 3N36 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%3
    for times in range(n):
        if (3**times-3)/2 >= n:
            print("%d个球%d次可以完成称重!"%(n,times))
            print() 
            return times,n,m

def 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位所有编码前添加一个0
        for 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,则该编码为正序
                    break
                elif (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)
                    break
        return str1
    else:
        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 list

def FinalCode(n,list,m):
    #输入球数n,首位为0的t位正序码列表,余数,返回n个符合条件的正序码
    #该步骤为将0替换为1,1替换为2,2替换为0,并将新编码加入列表
    if m == 0:
        t = n//3
    else:
        t = n//3 + 1
    templist = []
    for i in range(t):
        templist.append(list[i])
        temp1 = temp2 = ''
        for j in range(len(list[0])):
            #第一层替换
            l = int(list[i][j])+1
            if l == 3:
                l = 0
            temp1 += str(l)
        for k in range(len(temp1)):
            #第二层替换
            ll = int(temp1[k]) + 1
            if ll == 3:
                ll = 0
            temp2 += str(ll)
        templist.append(temp1)
        templist.append(temp2)
    return templist

def SetBall(n,list,m):
    #生成表示小球的列表并返回,元素分别为编号、质量、编码
    ball = []
    for i in range(n-m):
        fig = '('+str(i+1)+')'  #小球编号表示如"(10)"
        #按顺序将以上产生的正序编码赋给每个小球,质量设为10
        ball.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 ball

def 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] += weit
    elif select == 'N' or select =='n':
        #随机取一个小球,将其质量+1或-1
        list[random.randint(0,n-1)][1] += random.choice([1,-1])
    return list

def 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:
            continue

    if 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 += 1

            elif (list[j][2][i] == '1'):
                #判断第j个小球编码的第i位是否为1,若是将质量加于右盘,记录编号
                #右盘球数加一
                ball_temp.append(list[j])
                right += list[j][1]
                fig_right += list[j][0]
                count_r += 1

            elif (list[j][2][i] == '2'):
                continue

        if count_l < count_r:
            #若右盘球数多余左盘,则将一个标准球加于左盘
            for k in model:
                #检查标准球是否已在盘中
                if k not in ball_temp:
                    left += k[1]
                    fig_left += k[0]
                    break
        elif count_l > count_r:
            for k in model:
                if k not in ball_temp:
                    right += k[1]
                    fig_right += k[0]
                    break

        if 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 , heavy

def 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

  • 8
    点赞
  • 56
    收藏
    觉得还不错? 一键收藏
  • 8
    评论
好的,我可以给你提供一个简单的Python程序,它使用RoboDK API来控制机器人在RoboDK中移动5个小球。这个程序将机器人分成5个步骤,每个步骤移动一个小球。你需要按照下面的步骤来操作: 1. 首先,在RoboDK中打开你的机器人模型和场景。 2. 然后打开一个新的Python文件,在文件中导入RoboDK API: ```python from robolink import * from robodk import * ``` 3. 连接到RoboDK: ```python RDK = Robolink() ``` 4. 获取机器人的对象: ```python robot = RDK.Item('机器人的名称') ``` 5. 获取小球的对象: ```python ball1 = RDK.Item('小球1的名称') ball2 = RDK.Item('小球2的名称') ball3 = RDK.Item('小球3的名称') ball4 = RDK.Item('小球4的名称') ball5 = RDK.Item('小球5的名称') ``` 6. 定义一个函数来移动机器人和小球: ```python def move_robot_and_ball(robot, ball, position): target = robot.AddTarget(position) robot.MoveJ(target) ball.setParentStatic(target) ``` 7. 调用函数来移动机器人和小球: ```python move_robot_and_ball(robot, ball1, [100, 0, 0, 0, 0, 0]) move_robot_and_ball(robot, ball2, [200, 0, 0, 0, 0, 0]) move_robot_and_ball(robot, ball3, [300, 0, 0, 0, 0, 0]) move_robot_and_ball(robot, ball4, [400, 0, 0, 0, 0, 0]) move_robot_and_ball(robot, ball5, [500, 0, 0, 0, 0, 0]) ``` 8. 运行程序,机器人就会依次移动5个小球。 请注意,这只是一个简单的程序示例,你需要根据你的具体情况进行修改。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值