学更好的别人,做更好的自己。——《微卡智享》

前言

上一期《整活!我是如何用OpenCV做了数字华容道游戏!(附源码)》实现数字华容道游戏的制作,相对来说也比较简单,所以本篇是在这个基础上我们提升一下难度,用代码来实现数字华容道的AI自动还原。

实现效果

完整的视频思路讲解可以点下面的视频进行观看。‍

趣玩算法--数字华容道AI自动解题

Q1

华容道自动还原的核心点?

想要用程序实现数字华容道的自动还原,需要掌握什么?

1.数字华容道的解题方法,这个网上教学有不少。

2.怎么用程序实现数字移动到指定位置。(核心就是路径规划算法)。

路径规划算法

微卡智享

数字华容道的路径规划算法是也是基于A星算法原理实现的,区别就是A星算法是允许斜线移动,在计算当前要规划的点时,需要计算周围8个邻近点,而数字华容道行动时不允许走斜线,所以只能计算上下左右四个直线方向的点。

图中计算当前要行动的点是数字10,数字华容道计算时只计算6、9、11、14,而A星算法除了上面4个还要计算5、7、13、15。

01

直线移动距离相等问题

也正是因为数字华容道只允许走直线,所以要计算的点离终点距离有可能会存在2个路径是相等的,怎么解决这个问题呢?

上图中13到6的移动,红色线路和绿色线路用的体力是一样的都是3

解决上图这个问题,算法里加入了优先移动方向的参数,如上图取出13邻居可移动的数字为9和14,默认13到9和14的用的体力都是1,如果设置优先移动方向为上,那就变为13到9的体力为0.9,而13到14的体力还是1,这样整个计算下来红色线路要比绿色线路节省体力,最终规划的线路就是红色线路。

优先移动方向枚举

计算消耗的体力,加入优先行动方向的区分

02

数据结构

路径规划中每取到一个到终点用的体力最少的点时,就用这个点继续开始往下查找,所以数据结构上用链表的方式,每个点都有其父节点,直接找到终点后通过终点的父节点一步步的反推到起点,即是我们的行动路径。

定义的数据结构

加入地图障碍、开户列表、关闭列表

定义了一个开启列表和一个关闭列表,其中开启列表中存放着还未计算的所有点,关闭列表中存放着已经计算过的点。每次从开启列表中取出到终点体力最少的点,都存放到关闭列表中,还有找出这个点的上下左右可移动的点,当这几个点中超过地图范围或是在障碍点中,以及已经在关闭列表中时,就不做为可移动的点了,这样可以减少循环的计算量。

推荐看视频中动画的讲解

核心代码

CalcPathPlan.h

#pragma once
#include <iostream>
#include <vector>
#include <list>using namespace std;//修改后简化版的A*路径规划,不存在斜线移动,计算的邻近点也只为上下左右4个
struct CalcPos
{
public:int col; //X坐标位置  int row; //Y坐标位置float F, G, H; //F=G+H   G为起点到当前点已经移动的距离,H为当前点到终点的距离 CalcPos* parent; //父节点CalcPos(int _row, int _col) : row(_row), col(_col), F(0), G(0), H(0), parent(NULL){}
};//优先移动方向
enum DirectFirst {Up = 0,Down = 1,Left = 2,Right = 3,None = 9
};class CalcPathPlan
{
private:CalcPos* findPath(CalcPos& startPos, CalcPos& endPos);//计算临近的上下左右4个点vector<CalcPos*> getSurroundPos(const CalcPos* pos);//判断当前的点是否可以列入计算列表bool isCanCalc(const CalcPos* pos, const CalcPos* target);//判断点是否在开启或是关闭列表中CalcPos* isInList(const list<CalcPos*>& list, const CalcPos* pos);//检测障碍点bool isInSites(const int row, const int col) const;//从开启列表中返回F值最小的节点 CalcPos* getLeastFpoint();//计算FGH值 float calcG(CalcPos* temp_start, CalcPos* pos);float calcH(CalcPos* pos, CalcPos* end);float calcF(CalcPos* pos);vector<vector<int>> sites;  //华容道棋盘 0-可通行,1-障碍点list<CalcPos*> openList;  //开启列表 list<CalcPos*> closeList; //关闭列表 public://参数优先移动方向int DirectFirst = DirectFirst::Up;//初始化地图void InitSites(vector<vector<int>> _sites);//获取到路径vector<pair<int,int>> GetPath(pair<int, int>& startPos, pair<int, int>& endPos);
};

CalcPathPlan.cpp

#include "CalcPathPlan.h"CalcPos* CalcPathPlan::findPath(CalcPos& startPos, CalcPos& endPos)
{//首先写入起点,拷贝开启一个节点,内外隔离CalcPos* firstpt = new CalcPos(startPos.row, startPos.col);firstpt->H = calcH(firstpt, &endPos);firstpt->F = calcF(firstpt);openList.push_front(firstpt);while (!openList.empty()) {//找到F值最小的点auto curPoint = getLeastFpoint();//从开启列表中删除openList.remove(curPoint);//存放到关闭列表中closeList.push_front(curPoint);//1.找到当前点周围四个点中可以通过的点auto surroundPos = getSurroundPos(curPoint);for (auto& target : surroundPos) {//2.对某一个点,如果不在开启列表中,加入到开启列表中,设置当前格为父节点,计算F,G,H的值CalcPos* targetpos = isInList(openList, target);if (!targetpos) {//计算F,G,H的值target->G = calcG(curPoint, target);target->H = calcH(target, &endPos);target->F = calcF(target);target->parent = curPoint;//插入到开启列表中openList.push_front(target);}//3.对某个点在开启列表中计算G值,如果比原来的大,就什么都不做//否则设置它的父节点为当前点,并更新G和Felse {int tempG = calcG(curPoint, targetpos);if (tempG < targetpos->G) {targetpos->parent = curPoint;targetpos->G = tempG;targetpos->F = calcF(targetpos);}}CalcPos* resPoint = isInList(openList, &endPos);//返回列表里的节点指针,不要用原来传入的endpoint指针,因为发生了深拷贝 if (resPoint)return resPoint;}}return NULL;
}//计算当前点的可移动的上下左右点
vector<CalcPos*> CalcPathPlan::getSurroundPos(const CalcPos* pos)
{vector<CalcPos*> surroundPos;//上方点if (isCanCalc(pos, new CalcPos(pos->row - 1, pos->col))) {surroundPos.push_back(new CalcPos(pos->row - 1, pos->col));}//下方点if (isCanCalc(pos, new CalcPos(pos->row + 1, pos->col))) {surroundPos.push_back(new CalcPos(pos->row + 1, pos->col));}//左方点if (isCanCalc(pos, new CalcPos(pos->row, pos->col - 1))) {surroundPos.push_back(new CalcPos(pos->row, pos->col - 1));}//右方点if (isCanCalc(pos, new CalcPos(pos->row, pos->col + 1))) {surroundPos.push_back(new CalcPos(pos->row, pos->col + 1));}return surroundPos;
}bool CalcPathPlan::isCanCalc(const CalcPos* pos, const CalcPos* target)
{//坐标小于0直接不计算了if (target->col < 0 || target->row < 0//计算的点与当前一致也不计算|| (target->col == pos->col && target->row == pos->row)//判断点在障碍点中不计算|| isInSites(target->row, target->col)//如果点在关闭列表中也不计算|| isInList(closeList, target))return false;else {return true;}
}//判断开启/关闭列表中是否包含某点
CalcPos* CalcPathPlan::isInList(const list<CalcPos*>& list, const CalcPos* pos)
{//判断某个节点是否在列表中,这里不能比较指针,因为每次加入列表是新开辟的节点,只能比较坐标for (auto p : list)if (p->col == pos->col && p->row == pos->row)return p;return NULL;
}bool CalcPathPlan::isInSites(const int row, const int col) const
{if (col < 0 || row < 0 || row >= sites.size()|| col >= sites[0].size()) return true;return sites[row][col] == 1;
}//获取开启列表中F值最小的点才行动计算点
CalcPos* CalcPathPlan::getLeastFpoint()
{if (!openList.empty()){auto resPos = openList.front();for (auto& pos : openList)if (pos->F < resPos->F)resPos = pos;return resPos;}return NULL;
}//计算已经走的距离
float CalcPathPlan::calcG(CalcPos* temp_start, CalcPos* pos)
{float tempG = 1.0f;//判断移动方向,是否有优先级//因为移动距离计算时采用曼哈顿距离,两个点的距离有可能一样,所以这里//加入优先级判断,如果优先往上走,则移动时消耗的比原来1.0小一点,这样//计算距离时更近了switch (DirectFirst){case DirectFirst::Up: {tempG = temp_start->row > pos->row ? 0.9f : tempG;break;}case DirectFirst::Down: {tempG = temp_start->row < pos->row ? 0.9f : tempG;break;}case DirectFirst::Left: {tempG = temp_start->col > pos->col ? 0.9f : tempG;break;}case DirectFirst::Right: {tempG = temp_start->col < pos->col ? 0.9f : tempG;break;}default:tempG = 1.0f;break;}//判断是不是初始的节点,如果是初始节约,则其父节点为空int parentG = pos->parent == NULL ? 0 : pos->parent->G;//两个G相加用于判断是走直线和斜线所消耗的总Greturn parentG + tempG;
}float CalcPathPlan::calcH(CalcPos* pos, CalcPos* end)
{//计算终点到当前点的距离,因为不用斜着走,所以采用曼哈顿距离计算return abs(end->col - pos->col) + abs(end->row - pos->row);
}float CalcPathPlan::calcF(CalcPos* pos)
{//公式 F=G+Hreturn pos->F = pos->G + pos->H;
}//初始化地图
void CalcPathPlan::InitSites(vector<vector<int>> _sites)
{sites = _sites;
}vector<pair<int, int>> CalcPathPlan::GetPath(pair<int, int>& startPos, pair<int, int>& endPos)
{CalcPos sPos = CalcPos(startPos.first, startPos.second);CalcPos ePos = CalcPos(endPos.first, endPos.second);CalcPos* result = findPath(sPos, ePos);vector<pair<int, int>> path;//返回路径,如果没有找到路径,返回空链表while (result) {pair<int, int> curpath(result->row, result->col);path.insert(path.begin(), curpath);result = result->parent;}return path;
}

华容道代码编写

微卡智享

01

根据规划路径怎么移动

从上图中可以看到,通过路径规划已经计算出还原数字8的行动路径了,需要实现8在这个行动路径上移动,最终就是要把数字0(也就是空白格)移动到8的下一步行动格上(即现在9的位置),移动时就是根据当前格与到移动的格进行数字互换即可。

当移动完后0和数字8再进行位置交换,继续刚才的步骤,0再移动到数字9的位置,这时8再设置成为障碍点,这样规划0到9的位置时不会经过数字8(上一步也这样处理),等移动到位置后再把障碍点取消进行互换。

数字0移动的核心代码

int Puzzles4x4::ZeroMove(vector<vector<int>>& vts, vector<vector<int>>& sites, pair<int, int>& endPos, int MoveDirect)
{pair<int, int> ZeroPos = GetPos(vts, 0);vector<pair<int, int>> zeropath;if (ZeroPos.first == endPos.first && ZeroPos.second == endPos.second) {return 0;}zeropath = FindPath(sites, ZeroPos, endPos, MoveDirect);//3.3.1 开始移动0到当前位置,从第一步开始,因为起点不动for (int k = 1; k < zeropath.size(); ++k) {//cout << "zeropath:" << zeropath[k].first << " " << zeropath[k].second << endl;//当前位置与前一位置与换SwapPos(vts, zeropath[k], zeropath[k - 1]);}return zeropath.size();
}//两个点交换位置
void Puzzles4x4::SwapPos(vector<vector<int>>& vts, pair<int, int>& firstPos, pair<int, int>& secondPos)
{//插入还原步骤pair<pair<int, int>, pair<int, int>> curstep(firstPos, secondPos);VetRestoreSteps.push_back(curstep);int tmpnum = vts[firstPos.first][firstPos.second];vts[firstPos.first][firstPos.second] = vts[secondPos.first][secondPos.second];vts[secondPos.first][secondPos.second] = tmpnum;
}

02

华容道中特殊步骤处理

第一、二行最右侧处理

数字0无法移动到数字7的位置

上图中可以看出,数字4按路径规划移动到第二行第四格时,按我们定义的规则,现在数字4已经设置为障碍点了,同时因为数字1、2、3已经归位,所以也设置为障碍点,此时数字0移动到数字7是无路可走的,这里就需要进入到特殊步骤的环节了。

首先将数字0移动到数字9的位置

然后数字0移动到数字7的位置

按上面的方法,首先把数字1、2、3解锁障碍点,然后将数字0移动到数字7的位置,这里就可以体现到路径规划中加入方向参数的作用了,优先按向上行走,走的路线肯定是上图中的方案

0到达位置后和4进行互换

重新移动0的位置到7

将0按优先左移的路径规划移动到1的位置,这样数字1、2、3就可以正常还原回去了

按上面的流程就处理好数字4了,数字8的原理和4是一样的,所以代码中我们写了一个方法,只是在开始判断当前处理的是第几行即可。

核心函数

void Puzzles4x4::DealTopTwoRows(vector<vector<int>>& vts, vector<vector<int>>& sites, int row)
{//1.先将0移动到当前要处理的行的下面格pair<int, int> endPos(row + 1, 0);ZeroMove(vts, sites, endPos, DirectFirst::Left);//2.解锁处理行前面的障碍点,用0的位置优先移动到计算点for (int i = 0; i < sites[row].size(); ++i) {sites[row][i] = 0;}endPos.first = row;endPos.second = 2;ZeroMove(vts, sites, endPos, DirectFirst::Up);//3.数字0再和当前要处理的点进行位置互换,将我们4或8位置移动到对应后锁定endPos.first = row + 1;endPos.second = 3;//防止移动点是锁定的,将改为可移动sites[endPos.first][endPos.second] = 0;ZeroMove(vts, sites, endPos, DirectFirst::Right);//设置为锁定障碍点sites[row][3] = 1;//4.将数字0移动到第二步位置后还原当前行前三个数字endPos.first = row;endPos.second = 2;ZeroMove(vts, sites, endPos);//5.将0优先按左移的方式把原来行前面的数字还原回来endPos.first = row + 1;endPos.second = 0;ZeroMove(vts, sites, endPos, DirectFirst::Left);
}

调用时的处理

第三和、四行处理

三、四行把9和10、11和12移动到两侧后,即可实现还原

第三和第四行要一起处理,基本都是按上图上的方法将9和10,还有11和12移动到两侧,然后把13--15移动好后统一还原即可。这两天基本就是固定套路,不过在还原(9,10)或是(11,12)时,有机率会出现下面的情况:

移动一步后

当遇到上面这种情况时,也算进入了异常处理,需要将12的障碍点先解锁,然后让11先移动到15的位置(因为在右下四个小格中,无论怎么转也不会出现我们要还源的11在12的上面这样情况

按上面的步骤,就可以把11和12的位置按到最右侧了,同理,9和10在排到左侧时有机率也会出现这样的情况,处理的方式和上面的是一样的。

处理异常函数

//处理11和12的异常
void Puzzles4x4::DealElevenTwelve(vector<vector<int>>& vts, vector<vector<int>>& sites)
{if ((vts[2][3] == 12) && (vts[2][2] = 11)) {//1.锁定11个12当前位置,然后将0移动到右下角sites[2][2] = 1;sites[2][3] = 1;pair<int, int> endPos(3, 3);ZeroMove(vts, sites, endPos, DirectFirst::Right);//2.解锁11个12的位置,将0移动到第三行第三个sites[2][2] = 0;sites[2][3] = 0;endPos.first = 2;endPos.second = 2;ZeroMove(vts, sites, endPos, DirectFirst::Up);}
}//处理9和10的异常
void Puzzles4x4::DealNineTen(vector<vector<int>>& vts, vector<vector<int>>& sites)
{if ((vts[2][0] == 9) && (vts[2][1] = 10)) {//1.锁定9个10当前位置,然后将0移动到左下角sites[2][0] = 1;sites[2][1] = 1;pair<int, int> endPos(3, 0);ZeroMove(vts, sites, endPos, DirectFirst::Left);//2.解锁9个10的位置,将0移动到第三行第二个sites[2][0] = 0;sites[2][1] = 0;endPos.first = 2;endPos.second = 1;ZeroMove(vts, sites, endPos, DirectFirst::Up);}
}

最后就是所以有数字都还原的固定走法了,感兴趣的朋友可以下载源码查看,这里就不再列出来了。

03

还原总的处理时间

为了大家观看实现的方式,视频中我加入了展示移动效果,并在每一步显示后加入了200毫秒延时。

上图中IsShowStep的值为true,现在把它改为false后,关闭图像展示,经过多次测试,平均还原的速度在0.05秒内,速度还是挺快的。

华容道AI自动还原的方法就讲到这里了,最后来放一下源码地址。

源码地址

https://github.com/Vaccae/OpenCVNumPuzzles.git

GitHub上不去的朋友,可以击下方的原文链接跳转到码云的地址,关注【微卡智享】公众号,回复【源码】可以下载我的所有开源项目。

扫描二维码

获取更多精彩

微卡智享

「 往期文章 」

整活!我是如何用OpenCV做了数字华容道游戏!(附源码)

C++ OpenCV去燥函数fastNlMeansDenoising的使用

C++ OpenCV实现图像去阴影

趣玩算法--OpenCV华容道AI自动解题相关推荐

  1. python人物抠图算法_Python实现AI自动抠图实例解析

    一.简介 抠图是用PS? 用魔棒和快速选择工具? 遇到复杂背景怎么办? 最近发现一个神奇的工具--Remove Image Background https://www.remove.bg/zh 它是 ...

  2. 自动解题批改与自适应学习 AI在教育行业的应用实践

    自动解题批改与自适应学习 AI在教育行业的应用实践 因校内业务的需求,学霸君一直非常注重人工智能相关技术的研发.通用人工智能技术非常难做,但与垂直行业场景结合起来,降低算法的难度,使得算法找到用武之地 ...

  3. 【普通玩家VS高手玩家】一带传奇游戏《俄罗斯方块儿》新作—实现AI自动玩游戏~

    导语 提到<俄罗斯方块>(Tetris),那真是几乎无人不知无人不晓. ​ 其历史之悠久,可玩性之持久,能手轻轻一挥,吊打一大波游戏. 对于绝大多数小友而言,<俄罗斯方块>的规 ...

  4. Python+OpenCv实现AI人脸识别身份认证系统(2)——人脸数据采集、存储

    原 Python+OpenCv实现AI人脸识别身份认证系统(2)--人脸数据采集.存储 2019年07月02日 08:47:52 不脱发的程序猿 阅读数 602更多 所属专栏: 人脸识别身份认证系统设 ...

  5. Python+OpenCv实现AI人脸识别身份认证系统(3)——训练人脸识别模型

    目录 案例引入 本节项目 最近有小伙伴们一直在催本项目的进度,好吧,今晚熬夜加班编写,在上一节中,实现了人脸数据的采集,在本节中将对采集的人脸数据进行训练,生成识别模型. 案例引入 首先简要讲解数据集 ...

  6. python自动抠头像图_Python实现AI自动抠图实例解析

    一.简介 抠图是用PS? 用魔棒和快速选择工具? 遇到复杂背景怎么办? 最近发现一个神奇的工具--Remove Image Background 它是基于Python.Ruby和深度学习技术开发,通过 ...

  7. 汉诺塔自动解题动画中的iOS开发技巧

    引 前段时间做了一道题,要求实现汉诺塔游戏的自动解题动画: 汉诺塔游戏应该都了解规则: 1.将盘子全部移动到塔C 2.每次只能移动一个圆盘: 3.大盘不能叠在小盘上面. 要求由用户输入盘子的数量,绘制 ...

  8. AI自动剪辑生成视频探索实践

     点击上方"LiveVideoStack"关注我们 ▲扫描图中二维码或点击阅读原文▲了解音视频技术大会更多信息 // 编者按:随着短视频时代的到来,以短视频作为载体进行音乐推广具备 ...

  9. python人工自动抠图_Python - AI自动抠图

    一.简介 抠图是用PS? 用魔棒和快速选择工具? 遇到复杂背景怎么办? 最近发现一个神奇的工具--Remove Image Background 它是基于Python.Ruby和深度学习技术开发,通过 ...

最新文章

  1. Java中比较对象的两个接口Comparable接口和Comparator接口
  2. standford lessons
  3. 第五章 常用Lua开发库3-模板渲染
  4. 【数字逻辑设计】Logisim构建三种多路选择器
  5. fastdfs-配置存储服务器storage
  6. 设置android:sharedUserId=android.uid.system 支持 sdcard读写
  7. Python中使用libsvm
  8. opencv一些常用的操作
  9. Oracle iops升高查看,一则简单的磁盘的iops测试
  10. rdd数据存内存 数据量_Spark 性能优化(二)——数据倾斜优化
  11. MySQL索引优化面试题
  12. DDR3内存频率标识对应
  13. CornerStone的使用
  14. C/C++复习:向量的数量积
  15. 外汇买入价、外汇卖出价、现钞买入价有什么区别?
  16. Ubuntu系统安装时grub修复操作
  17. ios10之后的一些新特性
  18. EGE基础入门篇(六):基本图形
  19. 个人随笔/记录一个博友推荐的截图工具《FSCapture》
  20. 线性分类器良恶性乳腺癌肿瘤预测

热门文章

  1. BIOM:生物观测矩阵——微生物组数据通用数据格式
  2. 【调剂】燕山大学电气工程学院付荣荣老师接收调剂-仪器科学与技术、仪器仪表工程、人工智能...
  3. 从源码一步步学习,Ryan Dahl的Deno实现原理
  4. 如何用3天时间快速摸清一个行业
  5. 2345王牌输入法的卸载
  6. php许愿墙参考文献,许愿墙论文,关于我家的许愿墙相关参考文献资料-免费论文范文...
  7. Vue项目硅谷外卖(一)项目准备
  8. 华为平板可不可以更新鸿蒙,恭喜了,这37款华为支持升级鸿蒙,你的可以吗?...
  9. 【分享】10 个“疯狂”的 Python 项目创意
  10. Python 2-05 高阶函数