前言
猜数字(又称 Bulls and Cows )是一种古老的的密码破译类益智类小游戏,起源于20世纪中期,一般由两个人或多人玩,也可以由一个人和电脑玩。查了网上很多关于这个游戏的帖子,大多都是计算机出题,人来猜。有非常多种策略可以实现七步之内猜出答案,但大部分跟算法相关,需要数据功底,这里介绍一个简单的反向穷举法,快速且易实现。
游戏规则:
0-9 10个数字设为4位密码数字不重复。现在猜这个密码,每次电脑猜完人会提示,如果数字且位置都对则显示为A,数字对位置错显示B,例如:密码为0123, 猜0134, 则提示为2A1B。可以9次以内保证可以猜对的策略。这次是电脑猜,人给提示。
人猜电脑给提示参考:1AB2链接
游戏基本步骤
首先需要说明游戏下规则
- 人写一个四位数的密码,每一位数都可以从0到9选一个,但四位数字不能有重复的。如4415这个密码就是不行的。0可以放到开头,如0123这样也是可以的。这是答案,电脑猜题人要猜的。
- 猜题电脑打印一个自己猜测的密码,格式同上。写完后展示给人观看。
- 人根据猜题电脑的数字回答几A几B,其中A的个数表示猜题电脑猜测的数字位置正确的数的个数,B表示数字正确而位置不对的数的个数。例如出题人写下的密码是1564,猜题人写下的是7514,这时人就要输入给电脑2A1B。
- 电脑根据出题人提供的提示继续猜一个密码,出题人后续也要给出相应提示。
- 直到电脑猜中出题人写下的数字,游戏结束。统计一下电脑猜了多少次。
算法思路
电脑最擅长的穷举法一个一个来猜,列出所有答案,通过提示一一排除不需要的错误答案,剩下最后一个即为正确答案,答案总共有5040种。即是:第一次,固定猜 0123;然后,人必定要给出一个正确的提示:XAXB(X代表数字)①。然后,电脑从0124开始搜索,每次加1,并分别和0123相比较,得到XAXB,找到与0123比较答案一样为XAXB的不重复所有新数字串,这里面的 数字串即有正确答案;选择这里面得第一个数字串为下一个猜的数字,如果对,则结束,如果不对,则人又会给出第二个提示XAXB-1,此时,我们从第二步中的新数字串猜的数的下一个开始继续搜索,对比获取满足XAXB-1的不重复数字串,把他存入又一个新的数字串里;以次类推····
可见,越往后猜,候选答案需要满足的约束条件越多,即越有可能是标准答案。如:当猜到第7步还没猜中时,第8步猜测的候选答案必须同时满足前7次的猜测记录,因此,越往后,排除掉的越多,加速越快。当搜索到数字串里没有数字时,则说明人的提示有误。
到这里,大家是否明白这种算法的原理呢?总结:就是,电脑从0124开始,假设它是正确答案,那么它和0123相比得到的XAXB,必然与正确答案和0123相比得到的XAXB相同;一直对比下去就会得到正确答案,即我们所猜测的候选答案与之前猜测的数字串相比,得到的XAXB序列,必然与历史记录完全相同,否则便可排除。
此算法的优点:实现简单,时间空间复杂度都不高,而且最多不超过9步可完成游侠,。
C代码实现
#include"stdio.h"
// 全局变量ab
int ab = 0;
/**
** @brief: 电脑猜数输出
** @param: number:表示猜的数字,a:记录提示A的值,b:记录提示B的值
**/
void Guess(int *num, int* a, int* b)
{
printf("I guess the number: %d%d%d%d ?\n", num[0],num[1],num[2],num[3]);
printf("the number of A B:");
scanf("%d %d", a, b);
}
/**
** @brief: 比较两个4位数的位置和数值情况
** @param: num1,num1:两个数组的指针,数组存了4个数字.ab:比较后AB的值
**/
int compare(int* num1, int* num2)
{
ab = 0;
for(int i=0; i<4; i++)
{
for(int j=0; j<4; j++)
{
if(num1[i]==num2[j] && i==j)
{
ab += 10;
}else if(num1[i]==num2[j] && i!=j)
{
ab += 1;
}
}
}
return ab;
}
/**
** @brief: 遍历与guessNumber比较得到AB相同的数,表示可能是正确数的新数组
** @param: arryCnt:被遍历的数组大小,guessNumber:猜数,
** Allarray:数组,AB:AB信息
**/
void enumrate(int *arryCnt,int* guessNumber,int Allarray[][4],int* AB)
{
int j, forCnt=0;
int tem_AB, tem_ab=0;
tem_AB = AB[0]*10 + AB[1];
printf("-- arryCnt:%d \n",*arryCnt);
printf("-- array[0]:%d%d%d%d \n",Allarray[0][0],Allarray[0][1],Allarray[0][2],Allarray[0][3]);
printf("-- tem_AB:%d \n",tem_AB);
j = *arryCnt;
for(int i=1; i<j; i++){
tem_ab = compare(guessNumber, Allarray[i]);
if(tem_ab == tem_AB){
Allarray[forCnt][0]=Allarray[i][0];
Allarray[forCnt][1]=Allarray[i][1];
Allarray[forCnt][2]=Allarray[i][2];
Allarray[forCnt][3]=Allarray[i][3];
// printf("%d --",i);
printf("%d Allarray: %d%d%d%d \n", i,Allarray[forCnt][0],Allarray[forCnt][1],Allarray[forCnt][2],Allarray[forCnt][3]);
forCnt++;
}
}
printf("--forCnt:%d \n",forCnt);
(*arryCnt) = forCnt;
if(*arryCnt == 0){
printf("Great! great! great! fuck... \n");
}
}
int main()
{
int Allarray[5040][4];
int guessNumber[4]={0,1,2,3};
int AB[2]={0};
int arryCnt = 0; // 数组的大小记录
// 遍历所有的值,存起来
for(int i=0;i<10;i++){
for(int j=0;j<10;j++){
if(i==j) continue;
for(int k=0;k<10;k++){
if(k==i || k==j) continue;
for(int l=0;l<10;l++){
if(l==i||l==j||l==k){
continue;
}else{
Allarray[arryCnt][0]=i;
Allarray[arryCnt][1]=j;
Allarray[arryCnt][2]=k;
Allarray[arryCnt][3]=l;
// printf("array:%d%d%d%d \n",Allarray[arryCnt-1][0],Allarray[arryCnt-1][1],Allarray[arryCnt-1][2],Allarray[arryCnt-1][3]);
arryCnt +=1;
}
}
}
}
}
printf("end of the array:%d%d%d%d \n",Allarray[arryCnt-1][0],Allarray[arryCnt-1][1],Allarray[arryCnt-1][2],Allarray[arryCnt-1][3]);
printf("arryCnt:%d \n",arryCnt);
// 处理数组和判断
arryCnt = 5040;
for(int i=0;i<10; i++){
printf("\n");
printf("RAND %d \n", i+1);
Guess(Allarray[0],&AB[0],&AB[1]);
enumrate(&arryCnt, guessNumber, Allarray, AB);
guessNumber[0] = Allarray[0][0];
guessNumber[1] = Allarray[0][1];
guessNumber[2] = Allarray[0][2];
guessNumber[3] = Allarray[0][3];
if(AB[0]==4){
printf("the number is %d%d%d%d \n",Allarray[0][0],Allarray[0][1],Allarray[0][2],Allarray[0][3]);
break;
}
}
printf("Game over!");
return 0;
}
总结
这里还有其他的算法:https://www.zhihu.com/question/279643347
这些算法最多再7布就能找到答案,不过算法比较抽象,数学需要比较好,逻辑强,有空可以写一写。