一、题目说明:在一个3×3的九宫中有1-8这8个数及一个空格随机的摆放在

其中的格子里,如图1-1所示。现在要求实现这个问题:将其调整为

如图1-1右图所示的形式。调整的规则是:每次只能将与空格(上、

下、或左、右)相邻的一个数字平移到空格中。试编程实现这一问题的

求解。

(图1-1)

二、题目分析:这是人工智能中的经典难题之一,问题是在3×3方格棋盘中,放8格

数,剩下的没有放到的为空,每次移动只能是和相邻的空格交换数。程序自

动产生问题的初始状态,通过一系列交换动作将其转换成目标排列(如下图

1-2到图1-3的转换)。

(图1-2) (图1-3)

该问题中,程序产生的随机排列转换成目标共有两种可能,而且这两种

不可能同时成立,也就是奇数排列和偶数排列。可以把一个随机排列的数组

从左到右从上到下用一个一维数组表示,如上图1-2我们就可以表示成{

8,7,1,5,2,6,3,4,0}其中0代表空格。

在这个数组中我们首先计算它能够重排列出来的结果,公式就是:

∑(F(X))=Y,其中F(X)

是一个数前面比这个数小的数的个数,Y为奇数和偶数时各有一种解法。

上面的数组可以解出它的结果。

F(8)=0;

F(7)=0;

F(1)=0;

F(5)=1;

F(2)=1;

F(6)=3;

F(3)=2;

F(4)=3;

Y=0+0+0+1+1+3+2+3=10

Y=10是偶数,所以其重排列就是如图1-3的结果,如果加起来的

结果是奇数重排的结果就是如图1-1最右边的排法。

三、算法分析求解方法就是交换空格(0)位置,直至到达目标位置为止。图形表示就

是:

(图3-1)

要想得到最优的就需要使用广度优先搜索,九宫的所以排列有9!种,

也就是362880种排法,数据量是非常大的,使用广度搜索,需要记住每一个

结点的排列形式,要是用数组记录的话会占用很多的内存,可以把数据进行

适当的压缩。使用DWORD形式保存,压缩形式是每个数字用3位表示,这样就

是3×9=27个字节,由于8的二进制表示形式1000,不能用3位表示,

使用了一个小技巧就是将8表示为000,然后用多出来的5个字表示8所在的

位置,就可以用DWORD表示了。用移位和或操作将数据逐个移入,比乘法速度

要快点。定义了几个结果来存储遍历到了结果和搜索完成后保存最优路径。

类结构如下:

class CNineGird

{

public:

struct PlaceList

{

DWORD Place;

PlaceList* Left;

PlaceList* Right;

};

struct Scanbuf

{

DWORD Place;

int ScanID;

};

struct PathList

{

unsigned char Path[9];

};

private:

PlaceList *m_pPlaceList;

Scanbuf *m_pScanbuf;

RECT m_rResetButton;

RECT m_rAutoButton;

public:

int m_iPathsize;

clock_t m_iTime;

UINT m_iStepCount;

unsigned char m_iTargetChess[9];

unsigned char m_iChess[9];

HWND m_hClientWin;

PathList *m_pPathList;

bool m_bAutoRun;

private:

inline bool AddTree(DWORD place , PlaceList

*& parent);

void FreeTree(PlaceList*& parent);

inline void ArrayToDword(unsigned char *array

, DWORD & data);

inline void DwordToArray(DWORD data , unsigned

char *array);

inline bool MoveChess(unsigned char *array ,

int way);

bool EstimateUncoil(unsigned char *array);

void GetPath(UINT depth);

public:

void MoveChess(int way);

bool ComputeFeel();

void ActiveShaw(HWND hView);

void DrawGird(HDC hDC , RECT clientrect);

void DrawChess(HDC hDC , RECT clientrect);

void Reset();

void OnButton(POINT pnt , HWND hView);

public:

CNineGird();

~CNineGird();

};

计算随机随机数组使用了vector模板用random_shuffle(,)

函数来打乱数组数据,并计算目标结果是什么。代码:

void CNineGird::Reset()

{

if(m_bAutoRun) return;

vector vs;

int i;

for (i = 1 ; i < 9 ; i ++)

vs.push_back(i);

vs.push_back(0);

random_shuffle(vs.begin(), vs.end());

random_shuffle(vs.begin(), vs.end());

for ( i = 0 ; i < 9 ; i ++)

{

m_iChess[i] = vs[i];

}

if (!EstimateUncoil(m_iChess))

{

unsigned char array[9] = {1,2,3,8,0,4,7,6,5};

memcpy(m_iTargetChess , array , 9);

}

else

{

unsigned char array[9] = {1,2,3,4,5,6,7,8,0};

memcpy(m_iTargetChess , array , 9);

}

m_iStepCount = 0;

}

数据压缩函数实现:

inline void

CNineGird::ArrayToDword(unsigned

char *array , DWORD& data)

{

unsigned char night = 0;

for ( int i = 0 ; i < 9 ; i ++)

{

if (array[i] == 8)

{

night = (unsigned char)i;

break;

}

}

array[night] = 0;

data = 0;

data = (DWORD)((DWORD)array[0] << 29 | (DWORD)

array[1] << 26 |

(DWORD)array[2] << 23 | (DWORD)array[3] << 20 |

(DWORD)array[4] << 17 | (DWORD)array[5] << 14 |

(DWORD)array[6] << 11 | (DWORD)array[7]

<

(DWORD)array[8] <

array[night] = 8;

}

解压缩时跟压缩正好相反,解压代码:

inline void CNineGird::DwordToArray(DWORD

data ,

unsigned char

*array)

{

unsigned char chtem;

for ( int i = 0 ; i < 9 ; i ++)

{

chtem = (unsigned char)(data >> (32 - (i + 1) * 3)

& 0x00000007);

array[i] = chtem;

}

chtem = (unsigned char)(data & 0x0000001F);

array[chtem] = 8;

}

由于可扩展的数据量非常的大,加上在保存的时候使用的是DWORD

类型,将每一步数据都记录在一个排序二叉树中,按从小到大从左到有的排

列,搜索的时候跟每次搜索将近万次的形式比较快几乎是N次方倍,把几个

在循环中用到的函数声明为内联函数,并在插入的时候同时搜索插入的数据

会不会在树中有重复来加快总体速度。二叉树插入代码:

inline bool CNineGird::AddTree(DWORD place

,

PlaceList*&

parent)

{

if (parent == NULL)

{

parent = new PlaceList();

parent->Left = parent->Right = NULL;

parent->Place = place;

return true;

}

if (parent->Place == place)

return false;

if (parent->Place > place)

{

return AddTree(place , parent->Right);

}

return AddTree(place , parent->Left);

}

计算结果是奇数排列还是偶数排列的代码:

bool

CNineGird::EstimateUncoil(unsigned

char *array)

{

int sun = 0;

for ( int i = 0 ; i < 8 ; i ++)

{

for ( int j = 0 ; j < 9 ; j ++)

{

if (array[j] != 0)

{

if (array[j] == i +1 )

break;

if (array[j] < i + 1)

sun++;

}

}

}

if (sun % 2 == 0)

return true;

else

return false;

}

移动到空格位的代码比较简单,只要计算是否会移动到框外面

就可以了,并在移动的时候顺便计算一下是不是已经是目标结果,

这是用来给用户手工移动是给与提示用的,代码:

inline bool

CNineGird::MoveChess(unsigned

char *array , int way)

{

int zero , chang;

bool moveok = false;

for ( zero = 0 ; zero < 9 ; zero ++)

{

if (array[zero] == 0)

break;

}

POINT pnt;

pnt.x = zero % 3;

pnt.y = int(zero / 3);

switch(way)

{

case 0 : //up

if (pnt.y + 1 < 3)

{

chang = (pnt.y + 1) * 3 + pnt.x ;

array[zero] = array[chang];

array[chang] = 0;

moveok = true;

}

break;

case 1 : //down

if (pnt.y - 1 > -1)

{

chang = (pnt.y - 1) * 3 + pnt.x ;

array[zero] = array[chang];

array[chang] = 0;

moveok = true;

}

break;

case 2 : //left

if (pnt.x + 1 < 3)

{

chang = pnt.y * 3 + pnt.x + 1;

array[zero] = array[chang];

array[chang] = 0;

moveok = true;

}

break;

case 3 : //right

if (pnt.x - 1 > -1)

{

chang = pnt.y * 3 + pnt.x - 1;

array[zero] = array[chang];

array[chang] = 0;

moveok = true;

}

break;

}

if (moveok && !m_bAutoRun)

{

m_iStepCount ++ ;

DWORD temp1 ,temp2;

ArrayToDword(array , temp1);

ArrayToDword(m_iTargetChess , temp2);

if (temp1 == temp2)

{

MessageBox(NULL , "你真聪明这么快就搞定

了!" , "^_^" , 0);

}

}

return moveok;

}

在进行广度搜索时候,将父结点所在的数组索引记录在

子结点中了,所以得到目标排列的时候,只要从子结点逆向

搜索就可以得到最优搜索路径了。用变量m_iPathsize来记录

总步数,具体函数代码:

void CNineGird::GetPath(UINT depth)

{

int now = 0 , maxpos = 100 ;

UINT parentid;

if (m_pPathList != NULL)

{

delete[] m_pPathList;

}

m_pPathList = new PathList[maxpos];

parentid = m_pScanbuf[depth].ScanID;

DwordToArray(m_pScanbuf[depth].Place ,

m_pPathList[++now].Path);

while(parentid != -1)

{

if (now == maxpos)

{

maxpos += 10;

PathList * temlist = new PathList[maxpos];

memcpy(temlist , m_pPathList , sizeof(PathList)

* (maxpos - 10));

delete[] m_pPathList;

m_pPathList = temlist;

}

DwordToArray(m_pScanbuf[parentid].Place ,

m_pPathList[++now].Path);

parentid = m_pScanbuf[parentid].ScanID;

}

m_iPathsize = now;

}

动态排列的演示函数最简单了,为了让主窗体有及时刷新的机

会,启动了一个线程在需要主窗体刷新的时候,用Slee(UINT)

函数来暂停一下线程就可以了。代码:

unsigned __stdcall MoveChessThread(LPVOID

pParam)

{

CNineGird * pGird = (CNineGird *)pParam;

RECT rect;

pGird->m_iStepCount = 0;

::GetClientRect(pGird->m_hClientWin , &rect);

for ( int i = pGird->m_iPathsize ; i > 0 ; i --)

{

memcpy(pGird->m_iChess , pGird->m_pPathList[i].Path ,

9);

pGird->m_iStepCount ++;

InvalidateRect( pGird->m_hClientWin , &rect , false);

Sleep(300);

}

char msg[100];

sprintf(msg , "^_^ ! 搞定了!\r\n计算步骤用时%d毫秒"

, pGird->m_iTime);

MessageBox(NULL , msg , "~_~" , 0);

pGird->m_bAutoRun = false;

return 0L;

}

最后介绍一下搜索函数的原理,首先得到源数组,将其转换成DWORD型

,与目标比较,如果相同完成,不同就交换一下数据和空格位置,加入二叉树,

搜索下一个结果,直到没有步可走了,在搜索刚刚搜索到的位置的子位置,这样

直到找到目标结果为止,函数:

bool CNineGird::ComputeFeel()

{

unsigned char *array = m_iChess;

UINT i;

const int MAXSIZE = 362880;

unsigned char temparray[9];

DWORD target , fountain , parent , parentID = 0 ,

child = 1;

ArrayToDword(m_iTargetChess , target);

ArrayToDword(array , fountain);

if (fountain == target)

{

return false;

}

if (m_pScanbuf != NULL)

{

delete[] m_pScanbuf;

}

m_pScanbuf = new Scanbuf[MAXSIZE];

AddTree(fountain ,m_pPlaceList);

m_pScanbuf[ 0 ].Place = fountain;

m_pScanbuf[ 0 ].ScanID = -1;

clock_t tim = clock();

while(parentID < MAXSIZE && child < MAXSIZE)

{

parent = m_pScanbuf[parentID].Place;

for ( i = 0 ; i < 4 ; i ++) // 0 :UP , 1:Down

,2:Left,3:Right

{

DwordToArray(parent , temparray);

if (MoveChess(temparray,i)) //是否移动成功

{

ArrayToDword(temparray , fountain);

if (AddTree(fountain, m_pPlaceList)) //加入搜索数

{

m_pScanbuf[ child ].Place = fountain;

m_pScanbuf[ child ].ScanID = parentID;

if (fountain == target) //是否找到结果

{

m_iTime = clock() - tim;

GetPath(child);//计算路径

FreeTree(m_pPlaceList);

delete[] m_pScanbuf;

m_pScanbuf = NULL;

return true;

}

child ++;

}

}

} // for i

parentID++;

}

m_iTime = clock() - tim;

FreeTree(m_pPlaceList);

delete[] m_pScanbuf;

m_pScanbuf = NULL;

return false;

}

重要函数的介绍结束;下面是程序的运行结果和运算结果:

源代码:

文件:

9Ggird_src.zip

大小:

33KB

下载:

html八数码问题求解过程的可视化,(转贴)数字拼图问题(八数码)求解过程动态演示...相关推荐

  1. 多视图几何总结——基础矩阵、本质矩阵和单应矩阵的求解过程

    多视图几何总结--基础矩阵.本质矩阵和单应矩阵的求解过程 多视图几何总结--基础矩阵.本质矩阵和单应矩阵的求解过程 1. 说明--其实求解过程大同小异 2. 单应矩阵求解过程 2.1 基于代数误差的线 ...

  2. 使用Muduo完成数独和八数码问题求解服务器

    在剖析完Muduo网络库源码之后,我们试着完成一个高效的数独和八数码问题求解服务器. 先说说为什么要选择这两个问题?数独问题一直是陈硕老师很喜欢的问题,在muduo网络库中多次提到并有示例.八数码问题 ...

  3. 数据挖掘机器学习[七]---2021研究生数学建模B题空气质量预报二次建模求解过程:基于Stacking机器学习混合模型的空气质量预测{含码源+pdf文章}

    相关文章: 特征工程详解及实战项目[参考] 数据挖掘---汽车车交易价格预测[一](测评指标:EDA) 数据挖掘机器学习---汽车交易价格预测详细版本[二]{EDA-数据探索性分析} 数据挖掘机器学习 ...

  4. 需求分析的过程是什么?_7大需求分析方法与5大分析过程

    面对业务部门层出不穷的需求,如何入手进行需求分析?有没有需求分析的标准方法论可供参考?以下就是为大家推荐的8大类需求分析方法: 流程图 原型 用例图 用户故事(3C原则) 词汇表 实体关系图ERD 分 ...

  5. 单片机按键控制数码管c语言程序,基于单片机的按键控制LED数码管共阴极动态显示电路设计报告(毕业论文).doc...

    基于单片机的按键控制LED数码管共阴极动态显示电路设计报告(毕业论文) 物理与电子工程学院2014级课程设计 PAGE IV 物理与电子工程学院 <单片机原理与接口技术> 课程设计报告书 ...

  6. 51单片机数码管滚动显示学号_数协微课 | LED数码管与51单片机应用

    遇见工学,学在工学 停课不停学 数协电子组教学微课 它又来了 让我们开始学起来吧! 本期学习内容 LED数码管的结构与工作原理及相关原理图介绍 51单片机的应用:静态数码管显示.动态数码管显示 LED ...

  7. 深度学习笔记(五) 代价函数的梯度求解过程和方法

    作为自己的笔记系列,方便自己查阅和理解. 1)什么是梯度 梯度 本意是一个向量(矢量) 当某一函数在某点处沿着该方向的方向导数取得该点处的最大值,即函数在该点处沿方向变化最快,变化率最大(为该梯度的模 ...

  8. 三阶魔方自动求解及动态可视化matlab代码

    三阶魔方自动求解及动态可视化matlab代码 思路与步骤 总结 思考 参考链接 源代码 第一次写博客,想总结分享下以前做过的一些有趣的东西,目的是为了回望过去与展望未来,同时为了提高自己的写作表达能力 ...

  9. 数值分析——三角矩阵排序向量及求解过程

    三角矩阵排序向量及求解过程 问题 对于一个三角矩阵,如何得到排序向量 ppp ?给出算法并用程序实现它: 请实现三角系统的向前带入算法,给出程序并通过算例验证: 请给出三角系统的向后带入算法,给出程序 ...

最新文章

  1. 不同版本GCC编译器之间的切换
  2. Boost:不受约束的bimap双图的测试程序
  3. 2小时撸完代码之后,所有程序员都逃不过的一天... (强共鸣)
  4. 【C#】获取网页内容及HTML解析器HtmlAgilityPack的使用
  5. 如何将 Microsoft Bot Framework 链接至微信公共号
  6. DRF url控制 解析器 响应器 版本控制 分页(常规分页,偏移分页,cursor游标分页)...
  7. python 多进程 多核_go/node/python 多进程与多核cpu
  8. iPhone开发之self.的用法
  9. 【php】 php 的注释和结束符号之间的关系
  10. ExtJS6-项目创建
  11. [Bada开发]使用共享库
  12. 微积分专项----MIT GS老师
  13. 【哈士奇赠书活动 - 22期】-〖ChatGPT时代:ChatGPT全能应用一本通〗
  14. linux下搜索文件名,Linux系统中怎么搜索文件命令大全
  15. OaisimWithS1搭建笔记(2019.5)
  16. PowerShell自动加载账号密码
  17. mysql 聚簇索引 和聚簇索引 (二级索引)的 那些事
  18. HDU-1278-逃离迷宫
  19. 安装RHEL7.5超详细教程
  20. redhat linux中文,Redhat 中文解决方案

热门文章

  1. CISP-PTE靶场搭建
  2. Win10家庭版添加组策略gpedit.msc的办法
  3. 流程系统的设计与实现
  4. 什么叫做工作票?什么是工作票和操作票制度?
  5. python之鸭子类型(22)
  6. 一个早起的程序员简介
  7. 2022-2027年中国生物实验室耗材行业市场深度分析及投资战略规划报告
  8. 珠海化学分析实验室建设思路
  9. java打出的代码如何保存_如何使用java代码导出word
  10. mysql 记录执行的sql_MySQL监控全部执行过的sql语句