12球的一个算法

12球的一个算法

         12 球问题表述如下: 12个球,有一个球的重量与其它球的重量不一致。用一台没有刻度的天平称3 次把这个球找出。当然12球问题可以推广至N个球。
    首先考虑这道题目的模式,它属于输入反馈的问题。每一次输入(即称一次)便会有一个反馈信息,分析反馈信息再次输入,直到把问题解决。通过一次次输入反馈、这类问题解法思路较简单,由于是有穷状态,状态数目不大,可以用穷举法解答。比较所有的输入,找出最佳输入(某个标准意义下)。问题是如何衡量输入的好坏。直观上,输入越好,反馈的信息就越大。问题变成了对信息的量化。山农给出了信息量的定义是解决问题的关键。信息量是不确定性的大小。如果某个事件的概率为 P ,则它含有的信息量为 log(1/p) 。具体理论请参考关于信息论方面的书籍。(这个问题和猜数字很像,大家参考此文《猜数字的一种解法》)。

     具体到这个 12 球问题。 12 个球从 0-11 编号。 0 号球为较轻的球可能性为 1/24 。这个事件的信息量 log(24) 。同理 0 号球为重球这个事件的信息量为 log(24) 。这样的事件有 24 个,每个事件出现的概率 1/24 ,所以整个系统的信息量 24*(1/24*log(24)) ,即 log(24)
下面我们考虑一次具体的输入:
第一次输入,天平左端放 0 号球,右端放 1 号球。反馈信息可能为平衡,左倾,右倾。
平衡: 这时 0 1 号球是标准球。根据上面的计算过程,同样可以计算出此时系统含有的信息量 20*(1/20*log(20)) ,即 log(20) 。这次输入获得的信息 log(24)-log(20)
左倾 :可能的情况, 1 号球为轻球, 2 号球为重球。此时系统的信息量 log(2) 获得的信息 log(24)-log(2)
右倾: 同左倾,此时系统的信息量 log(2) 获得的信息 log(24)-log(2)
这次输入, 出现平衡的概率 20/24 ,出现左倾的概率 2/24 ,出现右倾的概率 2/24 。这可以计算这次输入能够获得的平均信息量为 20/24*(log(24)-log(20))+2/24*(log(24)-log(2))+2/24*(log(24)-log(2)) ,即 20/24*log(24/20)+2/24*log(24/2)+2/24*log(24/2)

   下面就要枚举所有可能的输入,找出
平均信息量 最大的输入。枚举需要些技巧,需要把球分类,例如第一次输入时每个球可以看成相同,左右天平放的球数为1,2,3,4,5,6,一共六情况。如果把每个球区别看待枚举的次数将会很多。下面介绍分类方法。
根据球的状态,分成4类,一类:标准球即排出不标准的可能性,二类:只可能为轻球,三类:只可能为重球,四类:可能为轻球也可能为重球。
//  把球分类
//  输出参数:type 表示对应球的类型,0为标准球,1可能为轻球,2可能为重球,3都有可能
void CGuessBall::SortBalls(VectorInt
&  type)
{
    
for  ( int  i = 0 ; i < m_iNumOfBalls; i ++ )
    {
        
if  (m_vPsbAns[i * 2 ==   0 )
            type[i]
++ ;
        
if  (m_vPsbAns[i * 2 + 1 ==   0 )
            type[i] 
+=   2 ;
    }
}

//  返回四个数组,分别表示四类球
void CGuessBall::SortBalls(VectorInt
&  type0, VectorInt &  type1, VectorInt &  type2, VectorInt &  type3)
{
    
for  ( int  i = 0 ; i < m_iNumOfBalls; i ++ )
    {
        
if  (m_vPsbAns[i * 2 ==   0   &&  m_vPsbAns[i * 2 + 1 ==   0 )
            type3.push_back(i);
        
else   if  (m_vPsbAns[i * 2 + 1 ==   0 )
            type2.push_back(i);
        
else   if  (m_vPsbAns[i * 2 ==   0 )    
            type1.push_back(i);
        
else
            type0.push_back(i);
    }
}
下面函数说明如何枚举,首先找出2*i个球,然后从中找出i个球。计算这次输入的信息量,需要我以前写的一篇文章《从集合中枚举子集 》。
//  使用分类法得到最佳输入
//  输出参数: left ,right左右天平球的标号
void CGuessBall::GetBestInputSort(VectorInt
&   left , VectorInt &   right  )
{
    
double  max_entropy  =   0.0 ;
    
    
//  把球分类
    VectorInt types(m_iNumOfBalls, 
0 );
    SortBalls(types);
    VectorInt t[
4 ];
    SortBalls(t[
0 ], t[ 1 ], t[ 2 ], t[ 3 ]);
    
    
//  枚举所有可能的输入组合
    
//  首先找出i个球,左右天平各方i / 2个球。
    CSetIterAgent
< int >  iter(types);
    
for  ( int  i = 2 ; i <= m_iNumOfBalls; i += 2 )
    {
        iter.Init(i, CSetIter::MULT_DISORDER_IN);
        VectorInt subset(i, 
0 );
        
while  (iter.GetNextSubset(subset, types))
        {            
            
//  再从i个球中找出i / 2个球
            CSetIterAgent
< int >  iter_i(subset);
            iter_i.Init(i
/ 2 , CSetIter::MULT_DISORDER_IN);
            VectorInt subset_l(i
/ 2 0 );
            VectorInt subset_r(i
/ 2 0 );
            
while  (iter_i.GetNextSubset(subset_l, subset))
            {
                
//  subset  -  subset_l得到subset_r
                
int  idx_r = 0 , idx_l = 0 , idx = 0 ;
                
while  (idx  <  subset.size())
                {
                    
if  (idx_l  >=  subset_l.size() ||
                        subset_l[idx_l] !
=  subset[idx])
                    {
                        subset_r[idx_r
++ =  subset[idx ++ ];
                    }
                    
else
                    {
                        idx_l
++ , idx ++ ;
                    }
                }
                
                
//  把球的类型转化成具体的球号
                
int  idxs[ 4 =  { 0 0 0 0 };
                
left .assign(i / 2 - 1 );
                
for  ( int  j = 0 ; j < subset_l.size(); j ++ )
                {
                    
left [j]  =  t[subset_l[j]][idxs[subset_l[j]] ++ ];
                }
                
right .assign(i / 2 - 1 );
                
for  ( int  j = 0 ; j < subset_r.size(); j ++ )
                {
                    
right [j]  =  t[subset_r[j]][idxs[subset_r[j]] ++ ];
                }
                
                
//  得到所有可能的反馈
                VectorInt outputs(
3 0 );
                
for  ( int  j = 0 ; j < m_vPsbAns.size(); j ++ )
                {
                    
if  (m_vPsbAns[j]  ==   1 )
                        continue;
                    outputs[CheckFeedback(
right left , j) + 1 ] ++ ;        
                }
                
                
//  检查是否是理论最优,提前返回
                 
if  (IsTheoryBestInput(outputs))
                {
                    m_vLeft 
=   left ;
                    m_vRight 
=   right ;
                    return;
                }
                
                
//  比较
                
double  entropy  =  CalcEntropy(outputs);
                
//  求最大的信息量
                
if  (max_entropy  <  entropy)
                {
                    max_entropy 
=  entropy;
                    m_vLeft 
=   left ;
                    m_vRight 
=   right ;
                }
            }
        }
    }
    
left   =  m_vLeft;
    
right   =  m_vRight;
}


    要使得到的信息量最大,就要使天平左倾,右倾,平衡的出现的概率尽可能相等。四类球,只有三类影响平衡,如果它们的个数能被3整除,左右天平各放三分之一份球。当然也有不能被3整除的情况。三类球,每一类均可能有余数0,1,2。组合起来一共有27种情况。考虑这27种情况配合标准球如何分配。得到以下的算法,先将三类球三等分,然后再考虑27种余数情况,得到第二种算法。
//  直接法得到最佳输入
void CGuessBall::GetBestInputDirect(VectorInt
&   left , VectorInt &   right )
{
    
//  第一次输入,没有标准球。
    
if  (m_iTimes  ==   0 )
    {
        
int  num  =  (m_iNumOfBalls + 1 /   3 ;
        
left .assign(num,  - 1 );
        
right .assign(num,  - 1 );
        
for  ( int  i = 0 ; i < num; i ++ )
        {
            
left [i]  =  i;
            
right [i]  =  i  +  num;
        }
    }
    
else
    { 
//  有标准球
        VectorInt t0, t1, t2, t3;
        SortBalls(t0, t1, t2, t3);
        
int  tmp1  =  t1.size()  /   3 ;
        
for  ( int  i = 0 ; i < tmp1; i ++ )
        {
            
left .push_back(t1[i]);
            
right .push_back(t1[i + tmp1]);
        }
        
int  tmp2  =  t2.size()  /   3 ;
        
for  ( int  i = 0 ; i < tmp2; i ++ )
        {
            
left .push_back(t2[i]);
            
right .push_back(t2[i + tmp2]);
        }
        
int  tmp3  =  t3.size()  /   3 ;
        
for  ( int  i = 0 ; i < tmp3; i ++ )
        {
            
left .push_back(t3[i]);
            
right .push_back(t3[i + tmp3]);
        }
        
        switch ((t3.size()%
3 ) * 9 + (t2.size()% 3 ) * 3 + (t1.size()% 3 ))
        {
        
case   0 * 3 * 3   +   0 * 3   +   0 :
        
case   0 * 3 * 3   +   0 * 3   +   1 :
        
case   0 * 3 * 3   +   1 * 3   +   0 :
            break;
        
case   0 * 3 * 3   +   0 * 3   +   2 :
        
case   0 * 3 * 3   +   1 * 3   +   2 :
        
case   0 * 3 * 3   +   2 * 3   +   2 :
//          case   1 * 3 * 3   +   0 * 3   +   2 :
            
left .push_back(t1[tmp1 * 2 ]);
            
right .push_back(t1[tmp1 * 2 + 1 ]);
            break;
        
case   0 * 3 * 3   +   1 * 3   +   1 :
            
left .push_back(t1[tmp1 * 2 ]);
            
right .push_back(t0[ 0 ]);
            break;
        
case   0 * 3 * 3   +   2 * 3   +   0 :
        
case   0 * 3 * 3   +   2 * 3   +   1 :
//          case   1 * 3 * 3   +   2 * 3   +   0 :
            
left .push_back(t2[tmp2 * 2 ]);
            
right .push_back(t2[tmp2 * 2 + 1 ]);
            break;
        
case   1 * 3 * 3   +   0 * 3   +   0 :
//          case   1 * 3 * 3   +   0 * 3   +   1 :
//          case   1 * 3 * 3   +   1 * 3   +   0 :
        
case   2 * 3 * 3   +   0 * 3   +   0 :
            
left .push_back(t3[tmp3 * 2 ]);
            
right .push_back(t0[ 0 ]);
            break;
//          case   1 * 3 * 3   +   1 * 3   +   1 :
//          case   1 * 3 * 3   +   1 * 3   +   2 :
//          case   1 * 3 * 3   +   2 * 3   +   1 :
//              left .push_back(t1[tmp1 * 2 ]);
//              right .push_back(t3[tmp3 * 2 ]);
//             break;
//          case   1 * 3 * 3   +   2 * 3   +   2 :
//              left .push_back(t1[tmp1 * 2 ]);
//              right .push_back(t1[tmp1 * 2 + 1 ]);
//              left .push_back(t2[tmp2 * 2 ]);
//              right .push_back(t2[tmp2 * 2 + 1 ]);
//             break;
//          case   2 * 3 * 3   +   0 * 3   +   1 :
//          case   2 * 3 * 3   +   0 * 3   +   2 :
//          case   2 * 3 * 3   +   1 * 3   +   0 :
//          case   2 * 3 * 3   +   1 * 3   +   1 :
//          case   2 * 3 * 3   +   2 * 3   +   0 :
//          case   2 * 3 * 3   +   1 * 3   +   2 :
//          case   2 * 3 * 3   +   2 * 3   +   1 :
//              left .push_back(t3[tmp3 * 2 ]);
//              right .push_back(t3[tmp3 * 2 + 1 ]);
//             break;
//          case   2 * 3 * 3   +   2 * 3   +   2 :
//              left .push_back(t1[tmp1 * 2 ]);
//              right .push_back(t1[tmp1 * 2 + 1 ]);
//              left .push_back(t3[tmp3 * 2 ]);
//              right .push_back(t3[tmp3 * 2 + 1 ]);
//             break;
        default:;
        }
    }
    m_vLeft 
=   left ;
    m_vRight 
=   right ;
}

这里要注意,如果有标准球则能达到最佳的输入。所以第一次没有标准球要区别对待。此外其实没有27种情况,因为第二,三类不可能和第四类同时出现。

这是一个测试程序 ,解答12球的问题,它输出左右天平放的球的编号,等待用户判断左倾,右倾还是平衡,直到输出结果。

#ifdef _TEST_
int  main()
{
    
int  size  =   12 ;
    CGuessBall guess(size);
    
while  ( 1 )
    {
        VectorInt 
left right ;
        
int  fd;
        guess.GetBestInput(
left right );
        
        
//  输出左右天平的编号
        
for  ( int  i = 0 ; i < left .size(); i ++ )
            printf(
" %d,  " left [i]);
        printf(
" \n " );
        
for  ( int  i = 0 ; i < right .size(); i ++ )
            printf(
" %d,  " right [i]);
        printf(
" \n " );
        
        
//  等待反馈
        printf(
" 左倾,平衡,右倾(-1,0,1): " );
        scanf(
" %d " & fd);
        
if  (fd  <=   - 1 ) fd  =   - 1 ;
        
else   if  (fd  >=   1 ) fd  =   1 ;
        
else  fd  =   0 ;
        
        
int  rz  =  guess.RemoveImpsAns(fd);
        
if  (rz  ==   1 )
        {
            
int  ans  =  guess.GetTheFirstImpsAns();
            printf(
" 猜出结果:%d球为%s球 " , ans / 2 , ans% 2 ? " " : " " );
            break;
        }
        
else   if  (rz  <   1 )
        {
            printf(
" 输入错误 " );
            break;
        }
        
else
            continue;
    }
}
#endif

    从程序运行情况看,很多球也很少几次就可以找出不规则球。下面讨论N个球要多少次才能确定不规则球。最佳输入能够使天平的三种情况出现的次数均匀。如果是最佳输入,系统的信息量只剩下原来的三分之一。由于对N个球,如果N不能被3整除,第一次就找不到最佳输入。对N个球可以在[log3(2*N)]+1次内找出不规则球。

第二个程序是测试上面两种算法的性能
#ifdef _TEST_TIME_
#include 
< windows.h >
int  main()
{
    
const   int  inc  =   20 ;
    DWORD 
time ;
    CGuessBall guess;
    
for  ( int  i = 0 ; i < 2 ; i ++ )
    {
        
for  ( int  j = 12 ; j <   12 + inc; j ++ )
        {
            
time   =   0 ;
            
for  ( int  t = 0 ; t < j * 2 ; t ++ )
            {
                guess.Init(j, i);
                DWORD start 
=  GetTickCount();
                
while  ( 1 )
                {
                    VectorInt 
left right ;
                    guess.GetBestInput(
left right );
                    
int  fd  =  guess.CheckFeedback( left right , t);
                    
int  rz  =  guess.RemoveImpsAns(fd);
                    
if  (rz  ==   1 )
                        break;
                }
                
time   +=  GetTickCount() - start;
            }
            
time   /=  j * 2 ;
            printf(
" 方法:%d\t球数:%d\t平均时间:%d\n " , i, j,  time );
        }
    }
    
    return 
0 ;
}
#endif
这是测试结果:
方法:0 球数:12        平均时间:0
方法:0 球数:13        平均时间:0
方法:0 球数:14        平均时间:0
方法:0 球数:15        平均时间:0
方法:0 球数:16        平均时间:0
方法:0 球数:17        平均时间:1
方法:0 球数:18        平均时间:1
方法:0 球数:19        平均时间:1
方法:0 球数:20        平均时间:5
方法:0 球数:21        平均时间:5
方法:0 球数:22        平均时间:5
方法:0 球数:23        平均时间:6
方法:0 球数:24        平均时间:6
方法:0 球数:25        平均时间:6
方法:0 球数:26        平均时间:27
方法:0 球数:27        平均时间:26
方法:0 球数:28        平均时间:25
方法:0 球数:29        平均时间:167
方法:0 球数:30        平均时间:162
方法:0 球数:31        平均时间:157
方法:0 球数:32        平均时间:171
方法:0 球数:33        平均时间:165
方法:0 球数:34        平均时间:163
方法:1 球数:12        平均时间:0
方法:1 球数:13        平均时间:0
方法:1 球数:14        平均时间:0
方法:1 球数:15        平均时间:0
方法:1 球数:16        平均时间:0
方法:1 球数:17        平均时间:0
方法:1 球数:18        平均时间:0
方法:1 球数:19        平均时间:0
方法:1 球数:20        平均时间:0
方法:1 球数:21        平均时间:0
方法:1 球数:22        平均时间:0
方法:1 球数:23        平均时间:0
方法:1 球数:24        平均时间:0
方法:1 球数:25        平均时间:0
方法:1 球数:26        平均时间:0
方法:1 球数:27        平均时间:0
方法:1 球数:28        平均时间:0
方法:1 球数:29        平均时间:0
方法:1 球数:30        平均时间:0
方法:1 球数:31        平均时间:0
方法:1 球数:32        平均时间:0
方法:1 球数:33        平均时间:0
方法:1 球数:34        平均时间:0


这里是 全部代码

你可能感兴趣的:(12球的一个算法)