珠玑妙算这个 NP-hard 问题大家都不陌生,规则就不多说了,直接进主题:
需要破译的密码由四位组成,每位可以是0~9中的任意数字。eg. (1544,4301,0918,...)
此算法可在15步内猜测出结果。平均11次(已统计过全部可能)。
算法大致步骤分为两步:
第一步:求出这串密码是由哪四个数字组成。依次猜 0000,1111,2222, ... ,8888 就行,返回的真猜中数便是当前数字在密码里的出现次数。第一步最多需要九次猜测。(之所以不需要再猜9999,是因为猜到8888时如果还没找全4个数字,那剩下的数字必然都出现在9999里面,所以无需再多次一举去试9999了。剩下还没找到的数字必然都是9)
从这里开始,下面提到的所有 “猜中” 均为 “真猜中”,该算法用不到 “伪猜中” 因为数字已经全部找出来了,剩下的问题就是找到这四个数字的正确排序,所以 “真猜中”+“伪猜中”必然等于4,我们只需要知道真猜中的个数就足够了。
第二步:将由第一步得出的四个数字全排列,把全排列结果存在一个集合里。然后依次返回集合里的猜测并通过回溯猜测结果,对集合进行进一步的消除从而减少猜测次数。
消除的规则如下:
当该次猜测的猜中数>0时:因为有至少一位猜中,所以我们可以把结果集里四个数字都跟本次猜测不一样的字符串消除掉。举例:如果字符串“1234” 里有至少一位正确,那么字符串“2341”必然是错误的,因为 2341 没有一位数字和 1234 同位且相同,所以可以把“2341”从结果集里消除掉。
当该次猜测的猜中数为0时:因为0位猜中,所以四个位的数字都是错的。从而我们可以直接消除集合里所有与此猜测有相同位的元素。举例:假设猜测为“1234”并且猜中数为0,我们就可以把集合里所有第一位为1,第二位为2,第三位为3,第四位为4 的字符串全部消除掉。
当该次猜测的猜中数为1时:这种情况稍微复杂点,我们需要用历史的猜测来与该猜测做比对来消除元素。 我们需要找到一个历史猜过的且猜中数为2的字符串。让其与我们该次的猜中数为1的字符串对比,如果这两个字符串的同位数量为2,这2位中必然有一位为真猜中位,所以我们可以把结果集内不满足条件的字符串消除掉. (两个字符串有同位指的是 strA[i] == strB[i] for some i). 举例:假设 “1234” 有一位猜中,“4231” 有两位猜中,中间的 “23” 是同位,所以这2位中必然有一位是真猜中位。我们可以断定正确密码中的第二位为2或第三位为3. 因此我们可以把结果集中第二位不为2且第三位不为3的字符串消除。
当结果猜中数为2时,我们需要拿其和历史猜测里猜中数为2和1的字符串做比较,
把该猜测和之前猜中数为2的猜测做比较:
若二者中有且仅有一位数字同位,那么该位就是真猜中位。举例:假设“1234”的猜中数为2,“4132”猜中数也为2,通过比对这两串数字得知它们只有第三位是相同的,都为3,所以可以得知真正密码的第三位就是3。然后回到我们的集合把第三位不为3的元素全部消除。
若二者中有两位数字同位,那么这2位中至少一位是真猜中位,把结果集里对应的消除。
把该猜测和猜中数为1的猜测做比较也和上面类似,计算两个字符串同位的数量,如果同位数=2,那么2位的其中之一必定是真猜中位。
当结果猜中数为3,4时,无需采取如何操作。(4位猜中时游戏结束。3位是不可能的,因为当三位猜中时剩下那一位必然属于剩下的那一个数字,所以不存在猜中数为3的情况)
整体算法如上所述。我的算法是以一个函数的形式运行的:string mastermindAI(int rr, int rw)
这个函数每次被呼叫都会返回一个猜测(string),然后主程序将会以参数形式将猜测结果传给函数(rr为真猜中数量,rw为伪猜中数量)。第一次被呼叫时rr和rw为0。主程序会随机生成一个密码然后调用该函数来破译,函数每次返回猜测主程序都会用eval函数来统计猜测结果,然后把rr和rw传回给函数。以此往复直至密码被破解为止。
程序:
#include <iostream>
#include <cstdlib>
#include <ctime>
#include <string>
#include <iomanip>
#include <cstring>
#include <vector>
#include <algorithm>
#include <set>
#include <map>
using namespace std;
//Function Prototypes
string AI(int,int);
bool eval(string,string,int &,int &);
string setCode();
vector<int> findMatchIdxs(string, string);
int main(int argc, char** argv) {
//Set the random number seed
srand(static_cast<unsigned int>(time(0)));
//Declare variables
string code,guess; //code to break, and current guess
int rr,rw; //right digit in right place vs. wrong place
int nGuess; //number of guesses
//Initialize Values
nGuess=0;
code=setCode();
rr=rw=0;
//Loop until solved and count to find solution
do{
nGuess++;
guess=AI(rr,rw);
}while(eval(code,guess,rr,rw));
//Check evaluation
cout<<"Count :"<<nGuess<<endl;
cout<<"Answer: "<<code<<endl;
cout<<"Guess : "<<guess<<endl;
//Exit the program
return 0;
}
string AI(int rr,int rw){
map<string,int> resultHistory; //历史猜测
static string sGuess="0000"; //上次返回的猜测
static int guessCounter=0; //总猜测数
static char bit[4]; //字符的字典,结果集的所有字符串由这个字典的字符组成
static int foundedBit=0; //已确定字符的数量
static set<string> guessBase; //结果集
static bool once=true; //记录第一次进入第二步的flag
//第一步:迭代返回0-8,求出密码是由哪四个数字组成
if(foundedBit<4)
{
if(rr>0)
{
for(int i=0;i<rr;i++)
{
bit[foundedBit++]=sGuess[1];
}
}
if(foundedBit<4)
{
if(sGuess[0]=='8')
{ //当返回了8888还是没找全4个数字的时候,剩下的数字必然出现在9999里,所以无需返回9999
while(foundedBit<4)
{
bit[foundedBit++]='9';
}
}
else
{
fill(sGuess.begin(),sGuess.end(),guessCounter+'0');
}
}
}
//当四位数字都找到时,进入第二步:
if(foundedBit>=4)
{
if(once) //进入第二步后,首先把四位数字全排列,所有排列组合将被存入名为guessbase的容器中
{ //全排列
sort(bit,bit+4);
do{
guessBase.insert(bit);
} while (next_permutation(bit,bit+4));
sGuess=*guessBase.begin(); //全排列结束,返回guessbase容器里的第一个元素
guessBase.erase(guessBase.begin());
once=false;
}
else //下面进入到排除环节
{
if(rr)
{ //只要猜中数>1时,至少有一个位是对的。所以把结果集中全部位都不满足的消除掉。
for(string guessInBase:guessBase)
{
if(guessInBase[0]!=sGuess[0]&&guessInBase[1]!=sGuess[1]&&guessInBase[2]!=sGuess[2]&&guessInBase[3]!=sGuess[3])
{
guessBase.erase(guessInBase);
}
}
}
switch(rr) //当真猜中数为0,1和2时可以进一步消除,其他猜中数无需处理
{
case 0:
for(string guessInBase:guessBase)
{ //rr为0时,全部位都是错的,把结果集里对应位相同的都消除掉
if(guessInBase[0]==sGuess[0]||guessInBase[1]==sGuess[1]||guessInBase[2]==sGuess[2]||guessInBase[3]==sGuess[3])
{
guessBase.erase(guessInBase);
}
}
break;
case 1:
for(auto hisGuess:resultHistory)
{ //rr为1时,找到历史猜测中rr为2的猜测,统计二者的对位数量
if(hisGuess.second==2)
{
vector<int>matchIdxs= findMatchIdxs(sGuess,hisGuess.first);
if(matchIdxs.size()==2)
{ //对位数量为2时,那两位中至少有一位是正确的,把结果集里不满足的消除掉
for(string guessInBase:guessBase)
{
if(guessInBase[matchIdxs[0]]!=sGuess[matchIdxs[0]] && guessInBase[matchIdxs[1]]!=sGuess[matchIdxs[1]])
{
guessBase.erase(guessInBase);
}
}
}
}
}
break;
case 2:
for(auto hisGuess:resultHistory)
{
if(hisGuess.second==2)
{ //如果历史猜测的猜中数为2时,统计对位数量
vector<int> matchIdxs= findMatchIdxs(hisGuess.first,sGuess);
switch(matchIdxs.size())
{
case 1:
//如果有且只有一位对位,那一位定是正确的,把结果集里那一位不对的字符串删除掉
for(string guessInBase:guessBase)
{
if(guessInBase[matchIdxs.front()]!=sGuess[matchIdxs.front()])
{
guessBase.erase(guessInBase);
}
}
break;
case 2:
//如果有且只有两位对位,二位之中至少一位是对的,把结果集里那两个位置都不对的字符串删除掉
for(string guessInBase:guessBase)
{
if(guessInBase[matchIdxs[0]]!=sGuess[matchIdxs[matchIdxs[0]]] &&
guessInBase[matchIdxs[1]]!=sGuess[matchIdxs[matchIdxs[1]]])
{
guessBase.erase(guessInBase);
}
}
break;
default:
break;
}
}
if(hisGuess.second==1)
{ //当历史猜测结果为1时,对其做和上面类似的处理
vector<int>matchIdxs= findMatchIdxs(sGuess,hisGuess.first);
if(matchIdxs.size()==2)
{
for(string guessInBase:guessBase)
{
if(guessInBase[matchIdxs[0]]!=sGuess[matchIdxs[0]] && guessInBase[matchIdxs[1]]!=sGuess[matchIdxs[1]])
{
guessBase.erase(guessInBase);
}
}
}
}
}
break;
default:
break;
}
resultHistory.insert(make_pair(sGuess,rr));
sGuess=*guessBase.begin();
guessBase.erase(guessBase.begin());
}
}
guessCounter++;
return sGuess.substr(0,4);
}
vector<int> findMatchIdxs(string a, string b) //统计两个字符串同位的数量
{
vector<int> matchIdxs;
for(int i=0;i<4;i++)
{
if(a[i]==b[i])
{
matchIdxs.push_back(i);
}
}
return matchIdxs;
}
bool eval(string code,string guess,int &rr,int &rw){ //为猜测统计真伪猜中的数量
string check=" ";
rr=0,rw=0;
//Check how many are right place
for(int i=0;i<code.length();i++){
if(code[i]==guess[i]){
rr++;
check[i]='x';
guess[i]='x';
}
}
//Check how many are wrong place
for(int j=0;j<code.length();j++){
for(int i=0;i<code.length();i++){
if((i!=j)&&(code[i]==guess[j])&&(check[i]==' ')){
rw++;
check[i]='x';
break;
}
}
}
//Found or not
if(rr==4)return false;
return true;
}
string setCode(){
string code="0000";
for(int i=0;i<code.length();i++){
code[i]=rand()%10+'0';
}
return code;
}
总结:
第一轮:持续去试单进制字符串直到找到密码是由哪四个数字组成为止。用那四个数字的全排列来生成结果集。
第二轮:针对每次不同猜测的rr信息来缩小结果集。
rr=0 时我们知道四位全错所以可以直接进行消除。
rr>0 时我们知道至少一位是对的所以可以把一些与其对位为0的字符串消除。
rr=1,2 时我们需要和之前猜测过的字符串来比较从而确定rr的具体是哪一位,
从而进一步做精确消除。
rr 不可能=3
rr=4 时 成功破译密码,游戏结束。