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类,一类:标准球即排出不标准的可能性,二类:只可能为轻球,三类:只可能为重球,四类:可能为轻球也可能为重球。
要使得到的信息量最大,就要使天平左倾,右倾,平衡的出现的概率尽可能相等。四类球,只有三类影响平衡,如果它们的个数能被3整除,左右天平各放三分之一份球。当然也有不能被3整除的情况。三类球,每一类均可能有余数0,1,2。组合起来一共有27种情况。考虑这27种情况配合标准球如何分配。得到以下的算法,先将三类球三等分,然后再考虑27种余数情况,得到第二种算法。
从程序运行情况看,很多球也很少几次就可以找出不规则球。下面讨论N个球要多少次才能确定不规则球。最佳输入能够使天平的三种情况出现的次数均匀。如果是最佳输入,系统的信息量只剩下原来的三分之一。由于对N个球,如果N不能被3整除,第一次就找不到最佳输入。对N个球可以在[log3(2*N)]+1次内找出不规则球。
第二个程序是测试上面两种算法的性能
方法: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
这里是 全部代码
首先考虑这道题目的模式,它属于输入反馈的问题。每一次输入(即称一次)便会有一个反馈信息,分析反馈信息再次输入,直到把问题解决。通过一次次输入反馈、这类问题解法思路较简单,由于是有穷状态,状态数目不大,可以用穷举法解答。比较所有的输入,找出最佳输入(某个标准意义下)。问题是如何衡量输入的好坏。直观上,输入越好,反馈的信息就越大。问题变成了对信息的量化。山农给出了信息量的定义是解决问题的关键。信息量是不确定性的大小。如果某个事件的概率为 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个球。计算这次输入的信息量,需要我以前写的一篇文章《从集合中枚举子集 》。// 输出参数: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);
}
}
//
使用分类法得到最佳输入
// 输出参数: 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;
}
// 输出参数: 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 ;
}
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
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
这是测试结果:
#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
这里是 全部代码