C++ AI解珠玑妙算(mastermind)算法_珠玑妙算最简单算法_传说C罗的博客-程序员宝宝

技术标签: 算法  c++  

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

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/weixin_45648862/article/details/117794675

智能推荐

各个版本Microsoft Visual C++运行库下载_QQ秦政的博客-程序员宝宝

https://blog.csdn.net/weixin_42831477/article/details/81429004

小议WebRTC拥塞控制算法:GCC介绍_weixin_30721077的博客-程序员宝宝

网络拥塞是基于IP协议的数据报交换网络中常见的一种网络传输问题,它对网络传输的质量有严重的影响,网络拥塞是导致网络吞吐降低,网络丢包等的主要原因之一,这些问题使得上层应用无法有效的利用网络带宽获得高质量的网络传输效果。特别是在通信领域,网络拥塞导致的丢包,延迟,抖动等问题,严重的影响了通信质量,如果不能很好的解决这些问题,一个通信产品就无法在现实环境中正常使用。在这方面WebRTC中的...

面试中关于Redis的问题看这篇就够了_面试里常考的redis内容在哪本书里_Bolon0708的博客-程序员宝宝

目录什么是Redis?Redis与Memcached的区别与比较Redis与Memcached的选择使用redis有哪些好处?Redis常见数据结构使用场景1. String2.Hash3.List4.Set5.Sorted SetMySQL里有2000w数据,Redis中只存20w的数据,如何保证Redis中的数据都是热点数据(redis有哪些数...

import caffe报错,ImportError: dynamic module does not define module export function (PyInit__caffe)_CODER8R的博客-程序员宝宝

在编译caffe时,错误是多个python版本环境导致的。解决办法:在新建的build文件夹下执行:cmake -D python_version=3 …然后make all -j4然后make pycaffe编译成功后,再import caffe,错误消失。

The prefix "aop" for element "aop:aspectj-autoproxy" is not bound._dengmm的博客-程序员宝宝

出现这个问题的原因是在头文件中没有添加“xmlns:aop”的命名申明,并在“xsi:schemaLocation”中指定aop配置的schema的地址    修改之后详细为:      xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-insta

测验3: Python基本语法元素 (第3周)——Python语言程序设计(MOOC第8次开课)_xld01的博客-程序员宝宝

一、平方根格式化描述获得用户输入的一个整字,a,计算a的平方根,保留小数点后3位,并打印输出。‪‬‪‬‪‬‪‬‪‬‮‬‪‬‫‬‪‬‪‬‪‬‪‬‪‬‮‬‪‬‪‬‪‬‪‬‪‬‪‬‪‬‮‬‭‬‪‬‪‬‪‬‪‬‪‬‪‬‮‬‫‬‮‬‪‬‪‬‪‬‪‬‪‬‮‬‫‬‪‬输出结果采用宽度30个字符、右对齐输出、多余字符采用加号(+)填充。‪‬‪‬‪‬‪‬‪‬‮‬‪‬‫‬‪‬‪‬‪‬‪‬‪‬‮‬‪‬‪‬‪‬...

随便推点

ECAPA-TDNN_java_crocodile的博客-程序员宝宝

实现流程ECAPA-TDNN由三部分组成:1-Dimensional Squeeze-Excitation Res2Blocks传统的x-vector的frame-layers只考虑了15帧的信息,而我们想要其考虑全局的信息,因此使用了 Squeeze-Excitation (SE) blocks首先是squeeze操作:将每一帧 frame-level features按时间取平均,输入特征为[N, C, L], 其中N为batch size,L为特征帧数, C为channel数,则通过求平均值,

Navicat Premium 12.1.11.0安装与激活_QQ秦政的博客-程序员宝宝

声明:本文所提供的所有软件均来自于互联网,个人存放在此作为备用,以备将来不时之需,同时作为大家的分享和学习成果,仅供个人研究和学习使用,请勿用于商业用途,下载后请于24小时内删除,请支持正版!附:二零零二年一月一日《计算机软件保护条例》第十七条规定:为了学习和研究软件内含的设计思想和原理,通过安装、显示、传输或者存储软件等方式使用软件的,可以不经...

2235_tnczae的博客-程序员宝宝

#include#include#includestruct Node{ int x,y; int peanuts;};int compare(const void* a,const void* b){ Node* m = (Node*) a; Node* n = (Node*) b; return n->peanuts-m->peanuts;}int main(){ const int

TAP_restart_nb map中没有的区县怎么办_初于青丝mc终于白发的博客-程序员宝宝

重新跑了一遍,但是貌似还是缺了一个区县,gan。步骤:一、先把给的数据展示到gis中,然后把这个数据和不缺的标准区县的地图进行空间连接(这次没有做FID_county,主要是这个都是唯一的,没有重复的,其实也不需要用这个来,用这个只是简单一点)二、把数据导出来删除不要的列,然后留做合并数据,通过代码跑出来不缺的区县的数据tq &lt;- function(input,output){ c &lt;- dir(input) cd &lt;-

太阳天顶角、太阳方位角、日地距离、时差、太阳赤纬角_王郁北辰的博客-程序员宝宝

(1)太阳天顶角太阳天顶角是太阳光线入射方向和天顶垂直方向的夹角,范围为:0°~180°,其计算公式和太阳赤纬角、目标地区的纬度、太阳时角有关,具体表达式为:式中,为黄赤交角,为目标地区纬度,其取值范围为-90°~90°正负取决于南北半球,南半球为负,北半球反之,为太阳时角,时角由时差决定,在计算时还需要对时差进行修正。(2)太阳方位角方位角的计算公式如下:式中以中午 ...

关于jsp连接MySQL5.7出现 Access denied for user 'root'@'localhost' (using password: YES)问题(附:数据库查询编码问题)_jsp access denied for user 'localhost_3366'@'local_Q_CAMEL的博客-程序员宝宝

经过多次尝试:在连接数据库时总是出现这个问题,经过一点点的修改终于解决了这个烦人的问题不说多直接贴代码&lt;%@ page contentType="text/html; charset=utf-8" %&gt; &lt;%@ page language="java" %&gt; &lt;%@ page import="com.mysql.jdbc.Driver" %&gt...

推荐文章

热门文章

相关标签