小球称重问题

Posted R-Pursue

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了小球称重问题相关的知识,希望对你有一定的参考价值。

  • 问题重述


    给定n枚硬币,其中有一枚假币,它与真币重量不同,但不知道是轻还是重。现有一个无砝码的天平,问要找出假币,并确认假币至少要称几次,并给出可视化编程。
  • 问题分析

    此问题是经典的信息论算法问题,许多大公司都曾以此作为面试、笔试题来考核员工。结合信息论的观点来看,每一次的称量,都会带来三种可能的结果,左边重、右边重、一样重。给我们将带来log_2⁡3,大约是1.58bit。n个球有一个不一样,那么总可能有n×2种结果(1号硬币轻,1号硬币重,2号硬币轻,2号硬币重••••••n号硬币轻,n号硬币重)。可以得知需要的信息量是log_2⁡2n,所以需要的次数大概就是log_2⁡2n⁄log_2⁡3 =log_3⁡n次。
    此分析是在理想状态下的称量状态,在实际中可能由于操作性的局限难以以此称量次数得到结果。下面,我将给出一些特殊情况的分析。

    • 特殊性分析
      考虑一般问题,把硬币分三堆,放两堆等数的球上天平,如果平衡,次品当然在外面这一堆。如果不平衡,比如说左边重,我们只知道外边的肯定是正品,但次品不知道是在天平的哪一边。重的那一边的硬币定义为“疑重硬币”,如果在这边,那次品一定是较重。轻的那一边定义为“疑轻球”,若是在这边则次品较轻。所以天平上的这些球经过一次称量之后已经被消除掉一部分的不确定性,称为半确定的硬币。半确定的球集合中只有两类的硬币,疑重硬币或疑轻硬币,集合中可以混合着这两类硬币,但知道每个硬币属于哪一类。
      我们来分析n=6的情况。

      1. 将硬币等分为3组,分别编号为A1,A2,B1,B2,C1,C2
      2. 将A组和B组放到天平两侧称量(第一次称量),将有两种可能情况
        i. 天平平衡,那么问题硬币必然在C组。则将AB中任取一组与C做称量(第二次称量),可以得知假币是比正常硬币轻还是重。再将C1,C2放入天平两侧称量(第三次称量),根据第二次称量的结果便可找出问题硬币。
        ii. 天平不平衡,则可以确定C组全部为正常硬币。将A,B两组中任取一组与C进行称量(第二次称量),便可确定问题硬币在哪一组,并且可知问题硬币是轻还是重。再将问题硬币组中两枚硬币做称量(第三次称量),则根据前两次称量结果可确定问题硬币是哪枚。
        这仅仅是六枚硬币便有如此复杂的称量方式,当硬币个数上升时,称量过程中出现的情况将指数增长,下面给出此问题的一般性推广分析。
    • 一般性推广
      通过数学的方法我们可以证明在一堆等重的硬币中有一个重量不同的假币,用天平称k次找出来,这堆球最多有(3^n-3)/2个球[附录一]。
      对如此多的球我们难以直接进行计算,因此我们做一下处理来得出结果
      编码
      知道了球数,就能算出需要称量几次;
      以这个次数作为长度,使用0、1、2排列组合进行编码,如001021、212022等等,再去掉全0、全1和全2,可知一共有3^n-3个编码;
      如果在一个编码中,第一处相邻数字不同的情况是01、12或20,则我们称它为正序码,如1120021;
      否则为逆序码,如2221012;
      在长度为n的编码中,正序码和逆序码的数量相等,均为(3^n-3)/2个。
      赋值
      如果把一个正序码中的0换成1,1换成2,2换成0,则它仍然是正序码;
      根据这个原理,我们把所有正序码按3个3个进行分组,如12001、20112、01220这3个就是一组;
      把正序码一组一组地分配给小球,每球一个,直到分完;
      然后把每个正序码的0换成2,2换成0,它就变成了一个逆序码,如12001变成10221;
      这样,每个小球就有了两个编码,一个正序,一个逆序,而且所有球都不重复。
      称重
      第一轮,我们把所有正序码第一位为0的小球放在天平左侧,为2的小球放在右侧,其它的放在旁边;
      如果天平左倾,记为0;右倾,记为2;平衡,记为1;
      然后是第二轮,把第二位为0的小球放在左侧,为2的放在右侧,同样记下称量结果;
      每一轮都按这个顺序进行,一共要称n次,最终结果是个n位的编码;
      如果编码等于某个小球的正序码,则这个小球比其它球重;
      如果编码等于某个小球的逆序码,则这个小球比其它球轻。

  • 问题演示


    这个问题的算法比较,我将把Java语言实现此算法并将代码附在附录,由于在找资料的过程中看到有一位前辈以javascript实现了此程序的可视化演示,自认为自己的演示的精彩程度将不及其千分之一,因此附上他的演示地址http://www.funnyjs.com/ballweight/#demo
  • 参考资料


    1. The Problem of the Pennies, F. J. Dyson, The Mathematical Gazette , Vol. 30, No. 291 (Oct., 1946), pp. 231-234
    2. http://www.funnyjs.com/ballweight/#demo
  • 附录


    1. 称球通解问题的证明
      摘自The Problem of the Pennies, F. J. Dyson, The Mathematical Gazette , Vol. 30, No. 291 (Oct., 1946), pp. 231-234
      • 引理1:在多于2个的一堆球,已知次品在其中,称k次可以并最多在3**k个半确定的球中找出次品,并且知道其轻重。
        用数学归纳法,k=1。3个半确定的球,一定至少有两个属于同一类,比如说疑重球,将这两个上天平,重的那个就是次品,如果平衡,外边的那个就是次品,而且从它的类别知道这次品是较重还是较轻。验证正确。
        假设结论对k-1次正确。将不多于3**k个半确定的球三等分,如果不能够等分,除天平两边要等数外,三方都不多于3**(k-1)个球,且使得两边共有偶数个疑重球,记为2a个。这总是可以做到的。因为我们可以把天平上“不齐整”的球和外面异类的球对调。这样天平左右各有a个疑重球和3**(k-1)-a个疑轻球。这一般有多种可能的a值满足要求,任取一个都行。这时如果左边重,左边的a个疑重球和右边的3**(k-1)-a个疑轻球,共3**(k-1)个半确定球有嫌疑,其他都是正品。如果右边重,同理将嫌疑缩小到3**(k-1)个半确定球。如果平衡,嫌疑在外面的3**(k-1)个半确定球中。如果这嫌疑是1个或2个半确定球,可以用一个正品与其中一个称一次解决,其他情况我们已知用k-1次可以解决不多于3**(k-1)个半确定的球。证毕。
      • 引理2:已知次品在其中,加一个已知的正品球称k次,可以并最多在(3**k+1)/2个球中找出次品,但有且仅有一种情况不知其次品的轻重。
        在k=1情况,有2个球,取一个与正品球上天平,如果平衡,次品在外面,但不知它比正品轻还是重,注意这是归纳证明中仅有的情况。如果只有1个球,它就是次品了,称一次可以知道比正品轻了还是重。
        假设结论对k-1次正确。考虑第一次天平称量,一边取(3**(k-1)-1)/2个加上一个正品球,另一边取(3**(k-1)+1)/2个球。我们知道这次称量以后,如果天平平衡,那么嫌疑在外面。余下k-1次可以解决这里的不超过(3**(k-1)+1)/2个球,有且仅有一种情况不知其次品的轻重。如果天平不平衡,天平的两边都是半确定的球。由引理1知道,余下k-1次可以解决这里的3**(k-1)个球。因为这个数是奇数,所以我们必须在第一次天平称量时再加上一个已知的正品球。因此称k次,可以并最多解决3**(k-1)+(3**(k-1)+1)/2=(3**k+1)/2个球。证毕。
        定理:在一堆等重球中有一个重量不同的次品球,用天平称k次找出来,这堆球最多且可以是(3**k-1)/2个球。
        在第一次称量我们最多可以将3**(k-1)-1个球两等分放在天平上,如果不平衡,由引理1,可以再称k-1次解决。如果平衡,天平这里都是已知球,由引理2,可以再称k-1次解决外面的(3**(k-1)+1)/2个球。所以总共可以解决(3**k-1)/2个球。证毕。
  • 称球问题java实现
import java.util.ArrayList;  
import java.util.List;  
import java.util.Scanner;  
public class Main 
    static int round = 1;  
    static int maxSteps;  

    public static void run(Status root, List<Status> list)  //求解  
        long time = System.currentTimeMillis();  
        List<Status> newlist = new ArrayList<Status>();   
        for (int i=0; i<list.size(); i++)   
            Status status = list.get(i);  
            status.produceBalances();  
            for (int j=0; j<status.bls.size(); j++)   
                Balance bl = status.bls.get(j);  
                bl.weight();  
                if (root.succeed())   
                    System.out.println("第" + round + "轮: 计算至上轮第" + (i+1) + "个节点得解,之前获得节点" + newlist.size() + "个,用时" + (double)(System.currentTimeMillis()-time)/1000 + "秒");                    return;  
                  
                if (bl.out1.isUnknown()) newlist.add(bl.out1);  
                if (bl.out2.isUnknown()) newlist.add(bl.out2);  
                if (bl.out3.isUnknown()) newlist.add(bl.out3);  
              
          
        System.out.println("第" + round + "轮: 获得节点" + newlist.size() + "个,用时" + (double)(System.currentTimeMillis()-time)/1000 + "秒");  
        round++;  
        run(root, newlist);   
      
    public static void print(Status st, int depth)  //输出结果  
        String indent="";  
        for (int i=0; i<depth-1; i++) indent = indent+"t";   
        Balance bl=null;  
        for (int i=0; i<st.bls.size(); i++)   
            if (st.bls.get(i).unresolved==0) bl=st.bls.get(i);  
        if (bl!=null)   
            if (depth>maxSteps) maxSteps=depth;  
            System.out.println(indent + "第" + depth + "步称重: " + bl + "rn");  
            System.out.println(indent + "如果一样重: " + bl.out1 + (bl.out1.getConclusion()==Status.RESOLVED?"  *解决*":(bl.out1.getConclusion()==Status.REDICULOUS?"  ×不可能×":"")) + "rn");  
            print(bl.out1, depth+1);  
            System.out.println(indent + "如果左边重: " + bl.out2 + (bl.out2.getConclusion()==Status.RESOLVED?"  *解决*":(bl.out2.getConclusion()==Status.REDICULOUS?"  ×不可能×":"")) + "rn");  
            print(bl.out2, depth+1);  
            System.out.println(indent + "如果右边重: " + bl.out3 + (bl.out3.getConclusion()==Status.RESOLVED?"  *解决*":(bl.out3.getConclusion()==Status.REDICULOUS?"  ×不可能×":"")) + "rn");  
            print(bl.out3, depth+1);  
          
      
    /**
     * @param args
     */
    public static void main(String[] args) 
        // TODO Auto-generated method stub
        Scanner sc = new Scanner(System.in);  
        System.out.println("请输入小球个数(大于2,超过14请调整JVM内存):");  
        int n = sc.nextInt();  
        Status root = new Status(n);  
        ArrayList<Status> list = new ArrayList<Status>();  
        list.add(root);  
        System.out.println("***** 开始求解......");  
        run(root, list);  
        System.out.println("rn***** 步骤说明:");  
        maxSteps = 0;  
        print(root, 1);  
        System.out.println("rn***** 总计" + maxSteps + "步可解!");  
    



import java.util.ArrayList;
import java.util.List;


public class Status 
     public static int RESOLVED=1, UNKNOWN=2, REDICULOUS=3, RESOLVABLE=4;  
        public int count=0;  
        public int[] data;  
        public List<Balance> parents = new ArrayList<Balance>();  
        public List<Balance> bls = new ArrayList<Balance>();  
        private int conclusion;  

        public Status(int c)   
            count = c;  
            int[] data1 = 0,c,0,0;  
            data = data1;  
            int conc = data[0]<count-1?UNKNOWN:(data[0]==count-1?RESOLVED:REDICULOUS);  
            setConclusion(conc);  
          
        public Status(int[] is)   
            data = is;  
            for (int i=0; i<is.length; i++) count+=is[i];  
            int conc = data[0]<count-1?UNKNOWN:(data[0]==count-1?RESOLVED:REDICULOUS);  
            setConclusion(conc);  
          
        public void addParent(Balance bl)   
            parents.add(bl);  
            if (conclusion==RESOLVED || conclusion==RESOLVABLE || conclusion==REDICULOUS) bl.prop();  
          
        public String toString()   
            return "正常" + data[0] + "、不明" + data[1] + "、或重" + data[2] + "、或轻" + data[3];  
          
        public void setConclusion(int conc)   
            if (conclusion == conc) return;  
            conclusion = conc;  
            if (conclusion==RESOLVED || conclusion==RESOLVABLE || conclusion==REDICULOUS)   
                for (int i=0; i<parents.size(); i++)  
                    parents.get(i).prop();  
          
        public int getConclusion() return conclusion;  
        public boolean succeed() return conclusion==RESOLVED || conclusion==RESOLVABLE;  
        public boolean isUnknown()return conclusion==UNKNOWN;  

        public void produceBalances() //得到当前状况下所有可能的称重方案   
            List<int[]> bldata = getBalanceDataArray(data);  
            bls = new ArrayList<Balance>();  
            for (int i=0; i<bldata.size(); i++)   
                Balance bl = new Balance(bldata.get(i));  
                bl.in = this;  
                bls.add(bl);  
              
          
        private List<int[]> getBalanceDataArray(int[] src)   
            List<int[]> list = new ArrayList<int[]>();  
            list.add(new int[src.length*2]);  
            return getBalanceDataArray(src,0,list);  
          
        private List<int[]> getBalanceDataArray(int[] src, int id, List<int[]> list)   
            int total=0,left,right;  
            if (id>=src.length)   
                for (int i=list.size()-1; i>=0; i--)   
                    int[] is = list.get(i);  
                    left=0;  
                    right=0;  
                    for (int j=0; j<src.length; j++) left+=is[j];  
                    for (int j=src.length; j<src.length*2; j++) right+=is[j];  
                    if (left!=right || left==0 || is[0]>0&&is[is.length/2]>0)  
                        list.remove(i);  
                  
                return list;  
              
            List<int[]> r = new ArrayList<int[]>();  
            for (int i=0; i<src.length; i++) total += src[i];  
            int half = total/2;  
            for (int i=0; i<list.size(); i++)   
                int[] is = list.get(i);  
                left=0;  
                right=0;  
                for (int j=0; j<src.length; j++) left+=is[j];  
                for (int j=src.length; j<src.length*2; j++) right+=is[j];  
                for (int j=0; j<=Math.min(half-left, src[id]); j++)   
                    for (int k=0; k<=Math.min(half-right, src[id]-j); k++)   
                        int[] iis = list.get(i).clone();  
                        iis[id] = j;  
                        iis[id+src.length] = k;  
                        r.add(iis);  
                      
                  
              
            return getBalanceDataArray(src,id+1,r);  
          



public class Balance 
     public int[] data;  
        public Status in,out1,out2,out3;   
        public int unresolved = 3;  

        public Balance(int[] data)   
            this.data = data.clone();  
          
        public void weight() //称重量,推理出三种可能的结果   
            int[] temp;  
            // 一样重   
            temp = in.data.clone();  
            for (int i=1; i<4; i++)  //所有参与称重的球都移入正常球集合   
                temp[0] = temp[0] + data[i] + data[i+4];  
                temp[i] = temp[i] - data[i] - data[i+4];  
              
            out1 = new Status(temp);  
            out1.addParent(this);  

            //左边重   
            temp = in.data.clone();  
            for (int i=1; i<4; i++)   
                temp[0] = temp[0] + temp[i] - data[i] - data[i+4]; //未参与称重的球  -->> 正常球   
              
            temp[0] += data[3] + data[6]; //左边的疑似轻球、右边的疑似重球  -->> 正常球   
            temp[1] = 0;  
            temp[2] = data[1] + data[2]; //左边的不明轻重球移入疑似重球集合   
            temp[3] = data[5] + data[7]; //右边的不明轻重球移入疑似轻球集合   
            out2 = new Status(temp);  
            out2.addParent(this);  

            //右边重   
            temp = in.data.clone();  
            for (int i=1; i<4; i++)   
                temp[0] = temp[0] + temp[i] - data[i] - data[i+4]; //未参与称重的球  -->> 正常球   
              
            temp[0] += data[2] + data[7]; //左边的疑似重球、右边的疑似轻球  -->> 正常球   
            temp[1] = 0;  
            temp[2] = data[5] + data[6]; //右边的不明轻重球移入疑似重球集合   
            temp[3] = data[1] + data[3]; //左边的不明轻重球移入疑似轻球集合   
            out3 = new Status(temp);  
            out3.addParent(this);  
          
        public String toString()  
            return "(" + (data[0]>0?"正常球×"+data[0]+"个 ":"") + (data[1]>0?"不明球×"+data[1]+"个 ":"")   
            +(data[2]>0?"疑似重球×"+data[2]+"个 ":"") + (data[3]>0?"疑似轻球×"+data[3]+"个 ":"")      
            + ") --天平-- ("  
            + (data[4]>0?"正常球×"+data[4]+"个 ":"") + (data[5]>0?"不明球×"+data[5]+"个 ":"")   
            +(data[6]>0?"疑似重球×"+data[6]+"个 ":"") + (data[7]>0?"疑似轻球×"+data[7]+"个 ":"") + ")";     
          
        public void prop()   
            if (unresolved <= 0) return;  
            unresolved--;  
            if (unresolved == 0) in.setConclusion(Status.RESOLVABLE);  
          
      

以上是关于小球称重问题的主要内容,如果未能解决你的问题,请参考以下文章

12分球问题

8个球7个一样重的,有一个偏重,一个天平,如何两次找出偏重的小球

小球称重问题

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

枚举 天平称重

小球称重问题~通过三次称重找出十二个小球质量不一样的小球,并判断小球轻重