C++ AI解珠玑妙算(mastermind)算法

珠玑妙算这个 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 时 成功破译密码,游戏结束。

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
Python珠玑妙算是一道猜数字游戏,游戏规则如下:系统随机生成一个长度为4的字符串,字符串由RGBY四个字符组成,且字符可以重复。玩家需要在10次机会内猜出系统生成的字符串,每次猜测后系统会给出两个数字,分别表示猜对了几个字符且位置正确(称为A),以及猜对了几个字符但位置不正确(称为B)。玩家需要根据系统给出的A和B来推测系统生成的字符串。 以下是一个Python珠玑妙算的实现,其中引用和引用分别提供了两种不同的实现方式: ```python # 引入必要的库 import random from typing import List # 实现珠玑妙算游戏 class Solution: def masterMind(self, solution: str, guess: str) -> List[int]: # 初始化变量 j = 0 answer = [0, 0] # 遍历solution字符串 for _ in solution: # 如果当前字符与guess字符串中对应位置的字符相同 if _ == guess[j]: # A加1 answer[0] += 1 # 将guess和solution中对应位置的字符都替换为空 guess = guess.replace(_, "", 1) solution = solution.replace(_, "", 1) else: # 否则j加1 j += 1 # 遍历guess字符串 for _ in guess: # 如果当前字符不为空 if _ != "": # 计算guess和solution中当前字符的出现次数 count1 = guess.count(_) count2 = solution.count(_) # 如果guess中当前字符出现次数大于1,将guess中所有当前字符都替换为空 if count1 > 1: guess = list(filter(lambda x: x != _, guess)) # B加上guess和solution中当前字符出现次数的最小值 answer[1] += min(count2, count1) # 返回结果 return answer # 生成随机字符串 def generate_random_string(): colors = ['R', 'G', 'B', 'Y'] return ''.join(random.choices(colors, k=4)) # 主函数 if __name__ == '__main__': # 初始化变量 solution = generate_random_string() guess = '' count = 0 # 循环10次 while count < 10: # 获取用户输入 guess = input('请输入你猜测的字符串(由RGBY四个字符组成,且字符可以重复):') # 判断用户输入是否合法 if len(guess) != 4 or not all(c in 'RGBY' for c in guess): print('输入不合法,请重新输入!') continue # 调用珠玑妙算函数 result = Solution().masterMind(solution, guess) # 输出结果 print('A:{}, B:{}'.format(result[0], result[1])) # 如果猜对了,退出循环 if result[0] == 4: print('恭喜你猜对了!') break # 否则次数加1 count += 1 # 如果次数用完了,输出答案 if count == 10: print('很遗憾,你没有在规定次数内猜对,答案是:{}'.format(solution)) ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值