问题描述
八数码问题,简单地来描述是这样的:在一个九宫格内,填有1、2、3、4、5、6、7、8,八个阿拉伯数字,有一个格子为空白。就下面这样,这是一个没有归位的九宫格。
1 | 2 | 3 |
---|---|---|
7 | 8 | |
4 | 6 | 5 |
(未归位的九宫格)
对于上面这个九宫格,我们要通过移动数字来使之归位,每次移动都只能是与空白格相邻的数字移到空白格里面。最终得到归位的九宫格应该是像下面这样的。
1 | 2 | 3 |
---|---|---|
8 | 4 | |
7 | 6 | 5 |
(已归位的九宫格)
我们的目标是,给出一种初始状态,如果有解的话,就把移动的步骤记录(或者输出)下来。值得注意的是,有的初始状态,是无论如何都没有办法使九宫格归位的。
解题思路
问题抽象
对于这个问题,我们不妨这样来看:每一种状态,最多有四种移动方式,也就是说,一种状态最多可以衍生出四种子状态。我们把问题的整体看成一个图,一个状态是图中的一个节点,每个节点生成的自节点由一条有向边来指向。图中总有一个节点的状态是我们要的归位状态,我们要做的就是找到初始节点(即初状态)到这个归位节点的路径。
图的宽度优先搜索
为了找到我们要的路径,我们可以使用宽度优先搜索。我们需要三样东西:集合close
,用于保存访问过的节点。集合open
,用于保存待访问的节点。每次访问一个节点,就会产生子节点,那么有如下三种情况:
如果子节点存在于
open
中,就把该子节点的深度与open
中的那个进行对比,若该子节点的深度较小,就替换。如果子节点存在于
close
中,就把该子节点的深度与close
中的那个进行对比,若该子节点的深度较小,就替换,并且该子节点的所有后代节点深度都要进行修改。(但是在该问题的A*搜索算法中,我们可以不用考虑这件事。把弱遇到子节点存在于close
中的情况,直接将其扔掉就可以了)如果子节点不在
open
中,也不在close
,那么将该节点加入到open
中等待访问。
如此进行下去,一直到找到我们想要的节点(说明有解),或者open
变为空为止(说明无解)。
A*搜索
如果问题有解的话,有没有什么办法可以让我们的搜索变得更快呢?
有,我们可以设置一个评估函数F(n) = G(n) + H(n)
,G(n)
给出的是节点的深度。H(n)
它给出的是当前状态下将所有数字归位所需的最小步数。每次要从open
中取出一个节点的时候,按照F(n)
给节点拍一个序,然后选取函数值最小的节点。评估函数H(n)
所给出的步数,比实际让九宫格归位所需的步数要少,所以这种启发式搜索叫做A*搜索。
代码实现
我们使用一个整数表示一种状态,整数中的每一位对应了九宫格中的一位,这样可以缩减比较九宫格是否一样所使用的时间。
比如九宫格 :
1 | 2 | 3 |
---|---|---|
7 | 8 | |
6 | 5 | 4 |
被表示为整数123780654。
数据结构
每一个节点里面的信息包含:
state
:当前的状态、
prev
:父节点的指针、
next
:一个存放子节点指针的向量、
cost
:H(n)
的函数值、
zeropos
:空白格子的位置、
depth
:节点深度。
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
#ifndef NODE_H
#define NODE_H
struct Node{
int state;
Node* prev;
vector<Node*> next;
int cost;
int zeropos;
int dept;
};
#endif
change
函数
输入一个状态,以及空白格子的位置,以及空白格子的移动方向,如果移动方向合法,就可以产生子状态,否则返回-1.
int change(int state,int zeropos,int direction){
int ansstate = 0;
int usestate = state;
int zerorow = (zeropos-1)/3;
int zerocol = (zeropos-1)%3;
int matrix[3][3] = {1,2,3,
8,0,4,
7,6,5};
int mo = 100000000;
for(int i = 0;i<3;i++)
for(int j = 0;j<3;j++)
{
matrix[i][j] = usestate/mo;
usestate = usestate%mo;
mo = mo/10;
}
switch (direction) { //0是左移,1是上移,2是右移,3是下移
case 0:{
if(zeropos==1||zeropos==4||zeropos==7) return -1;
int target = matrix[zerorow][zerocol-1];
matrix[zerorow][zerocol-1] = 0;
matrix[zerorow][zerocol] = target;
break;
}
case 1:{
if(zeropos==1||zeropos==2||zeropos==3) return -1;
int target = matrix[zerorow-1][zerocol];
matrix[zerorow-1][zerocol] = 0;
matrix[zerorow][zerocol] = target;
break;
}
case 2:{
if(zeropos==3||zeropos==6||zeropos==9) return -1;
int target = matrix[zerorow][zerocol+1];
matrix[zerorow][zerocol+1] = 0;
matrix[zerorow][zerocol] = target;
break;
}
case 3:{
if(zeropos==7||zeropos==8||zeropos==9) return -1;
int target = matrix[zerorow+1][zerocol];
matrix[zerorow+1][zerocol] = 0;
matrix[zerorow][zerocol] = target;
break;
}
default:
break;
}
int mul = 100000000;
for(int i = 0;i<3;i++)
for(int j = 0;j<3;j++)
{
ansstate = ansstate + matrix[i][j]*mul;
mul = mul/10;
}
return ansstate;
}
H(n)
函数
输入一个状态,返回H(n)
函数值。
int Hn(int t){
int crr[3][3];
int brr[3][3]={1,2,3,
8,0,4,
7,6,5};
for(int i=8;i>=0;i--){
int mod=t%10;
t=t/10;
crr[i/3][i%3]=mod;
}
int count=0;
for(int i=0;i<3;i++){
for(int j=0;j<3;j++){
for(int p=0;p<3;p++){
for(int q=0;q<3;q++){
if(crr[i][j]==brr[p][q]){
int sum1=p-i;
if(sum1<0) sum1=-sum1;
int sum2=q-j;
if(sum2<0) sum2=-sum2;
int sum=sum1+sum2;
count+=sum;
break;
}
}
}
}
}
return count;
}
findZeroPos
函数
输入一个状态,返回空白格子的位置。
int findZeropos(int state){ int ans = 0; int usestate = state; int matrix[3][3] = {1,2,3, 8,0,4, 7,6,5};
int mo = 100000000;
for(int i = 0;i<3;i++)
for(int j = 0;j<3;j++)
{ matrix[i][j] = usestate/mo; usestate = usestate%mo; mo = mo/10; }
for(int i = 0;i<3;i++)
for(int j = 0;j<3;j++)
if(matrix[i][j]==0){ ans = 3 * i + j + 1; }
return ans;
}
mySearch
函数
输入一个状态,用广度优先搜索找到使九宫格归位的路径,并返回结果节点的指针。路径用节点组成的链表来储存。
Node* mySearch(int inistate){
vector<Node*> open;
vector<Node*> close;
//初始化第一个节点
Node* ini = new Node;
ini->zeropos = findZeropos(inistate);
ini->state = inistate;
ini->dept = 0;
ini->cost = Hn(inistate);
ini->prev = NULL;
open.push_back(ini);
//开始广搜
while(!open.empty()){
sort(open.begin(),open.end(),cmp);
Node* temp = open.back();
open.pop_back();
if(temp->state==123804765) return temp;
for(int i = 0;i < 4;i++){
int nextstate = change(temp->state, temp->zeropos, i);
if(nextstate == -1) continue;
//如果存在于close中,本题不做此考虑
bool inClose = false;
for(int j = 0;j<close.size();j++){
if(close[j]->state == nextstate) inClose = true;
}
//如果存在于open中
bool inOpen = false;
for(int j = 0;j<open.size();j++){
if((open[j]->state == nextstate)&&(open[j]->dept>temp->dept+1)){
open[j]->prev = temp; //修改它的父节点
inOpen = true;
}
}
if(inOpen||inClose) continue; //如果不在open且不在close中,则创建新的节点
Node *son = new Node;
son->state = nextstate;
son->dept = temp->dept+1;
son->cost = Hn(nextstate);
son->prev = temp;
son->next.push_back(son);
son->zeropos = findZeropos(nextstate);
open.push_back(son);
}
close.push_back(temp);
}
return NULL;
}
主函数(用于测试)
int main(){
//输入一个九宫格
int testmatrix[3][3] = {0};
for(int i = 0;i<3;i++)
for(int j = 0;j<3;j++)
cin>>testmatrix[i][j];
//转换成整数
int test = 0;
int mul = 100000000;
for(int i = 0;i<3;i++)
for(int j = 0;j<3;j++){
test = test + testmatrix[i][j]*mul;
mul = mul/10;
}
//搜索
Node* ans = mySearch(test);
//按照九宫格的形式输出生成的链表
while(ans){
int usestate = ans->state;
int matrix[3][3];
int mo = 100000000;
for(int i = 0;i<3;i++)
for(int j = 0;j<3;j++)
{
matrix[i][j] = usestate/mo;
usestate = usestate%mo;
mo = mo/10;
}
for(int i = 0;i<3;i++){
for(int j = 0;j<3;j++)
{
cout<<matrix[i][j]<<" ";
}
cout<<endl;
}
cout<<endl;
ans = ans->prev;
}
return 0;
}
结果
- 对于有解的情况,该算法的确可以很快找到使九宫格归位的路径。
- 对于无解的情况,往往要跑很久,遍历很多节点。程序的运行时间长达十多分钟。
(完)