九宫问题(八数码问题)的解法

一、题目说明:
  
(九宫问题)在一个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)=1;
F(1)=2;
F(5)=2;
F(2)=3;
F(6)=2;
F(3)=4;
F(4)=4;
Y=0+1+2+2+3+2+4+4=18

  Y=18是偶数,所以他的重排列就是如图1-3的结果,如果加起来的结果是奇数重排的结果就是如图1-1最右边的排法。

一.八数码问题
八数码问题也称为九宫问题。在3×3的棋盘,摆有八个棋子,每个棋子上标有1至8的某一数字,不同棋子上标的数字不相同。棋盘上还有一个空格,与空格相邻的棋子可以移到空格中。要求解决的问题是:给出一个初始状态和一个目标状态,找出一种从初始转变成目标状态的移动棋子步数最少的移动步骤。
所谓问题的一个状态就是棋子在棋盘上的一种摆法。棋子移动后,状态就会发生改变。解八数码问题实际上就是找出从初始状态到达目标状态所经过的一系列中间过渡状态。
八数码问题一般使用搜索法来解。
搜索法有广度优先搜索法、深度优先搜索法、A*算法等。这里通过用不同方法解八数码问题来比较一下不同搜索法的效果。
二.搜索算法基类
1.八数码问题的状态表示
八数码问题的一个状态就是八个数字在棋盘上的一种放法。每个棋子用它上面所标的数字表示,并用0表示空格,这样就可以将棋盘上棋子的一个状态存储在一个一维数组p[9]中,存储的顺序是从左上角开始,自左至右,从上到下。也可以用一个二维数组来存放。
2.结点
搜索算法中,问题的状态用结点描述。结点中除了描述状态的数组p[9]外,还有一个父结点指针last,它记录了当前结点的父结点编号,如果一个结点v是从结点u经状态变化而产生的,则结点u就是结点v的父结点,结点v的last记录的就是结点u的编号。在到达目标结点后,通过last 可以找出搜索的路径。
3.类的结构
在C++中用类来表示结点,类将结点有关的数据操作封装在一起。
不同的搜索算法具有一定共性,也有各自的个性,因此这里将不同搜索算法的共有的数据和功能封装在一个基类中,再通过继承方式实现不同的搜索算法。
4.结点扩展规则
搜索就是按照一定规则扩展已知结点,直到找到目标结点或所有结点都不能扩展为止。
八数码问题的结点扩展应当遵守棋子的移动规则。按照棋子移动的规则,每一次可以将一个与空格相邻棋子移动到空格中,实际上可以看作是空格作相反移动。空格移动的方向可以是右、下、左、上,当然不能移出边界。
棋子的位置,也就是保存状态的数组元素的下标。空格移动后,它的位置发生变化,在不移出界时,空格向右、下、左和上移动后,新位置是原位置分别加上1、3、-1、-3,如果将空格向右、下、左和上移动分别用0、1、2、3表示,并将-3、3、-1、1放在静态数组d[4]中,空格位置用spac表示,那么空格向方向i移动后,它的位置变为spac+d[i]。
空格移动所产生的状态变化,反映出来则是将数组p[]中,0的新位置处的数与0交换位置。
5.八数码问题的基类
八数码问题的基类及其成员函数的实现如下:
#define Num 9

class TEight
{
public:
TEight(){}
TEight(char *fname);
virtual void Search()=0;
protected:
int p[Num];
int last,spac;
static int q[Num],d[],total;
void Printf();
bool operator==(const TEight &T);
bool Extend(int i);
};
int TEight::q[Num];
int TEight::d[]={1,3,-1,-3};
int TEight::total=0;

TEight::TEight(char *fname)
{
ifstream fin;
fin.open(fname,ios::in | ios::nocreate);
if(!fin)
{
cout<<"不能打开数据文件!"< return;
}
for(int i=0;i fin>>p[i++];
fin>>spac;
for(i=0;i fin>>q[i++];
fin.close();
last=-1;
total=0;
}

void TEight::Printf()
{
ofstream fout;
fout.open(".\Eightr.txt",ios::ate);
fout< for(int i=0;i fout< fout< fout.close();
}

bool TEight::operator==(const TEight &T)
{
for(int i=0;i if(T.p[i]!=p[i++])
return 0;
return 1;
}

bool TEight::Extend(int i)
{
if(i==0 && spac%3==2 || i==1 && spac>5
|| i==2 && spac%3==0 || i==3 && spac<3)
return 0;
int temp=spac;
spac+=d[i];
p[temp]=p[spac];
p[spac]=0;
return 1;
}
数据文件的结构:
一共三行,第一行是用空格隔开的九个数字0~8,这是初始状态。第二行是一个数字,空格(数字0)的位置,第三行也是用空格隔开的九个数字0~8,这是目标状态。

三.线性表
搜索法在搜索过程中,需要使用一个队列存储搜索的中间结点,为了在找到目标结点后,能够找到从初始结点到目标结点的路径,需要保留所有搜索过的结点。另一方面,不同问题甚至同一问题的不同搜索方法中,需要存储的结点数量相差很大,所以这里采用链式线性表作为存储结构,同时,为适应不同问题,线性表设计成类模板形式。
template class TList; //线性表前视定义

template class TNode //线性表结点类模板
{
friend class TList;
public:
TNode(){}
TNode(const Type& dat);
private:
TNode* Next;
Type Data;
};

template class TList
{
public:
TList(){Last=First=0;Length=0;} //构造函数
int Getlen()const{return Length;} //成员函数,返回线性表长度
int Append(const Type& T); //成员函数,从表尾加入结点
int Insert(const Type& T,int k); //成员函数,插入结点
Type GetData(int i); //成员函数,返回结点数据成员
void SetData(const Type& T,int k); //成员函数,设置结点数据成员
private:
TNode *First,*Last; //数据成员,线性表首、尾指针
int Length; //数据成员,线性表长度
};

template int TList::Append(const Type& T)
{
Insert(T,Length);
return 1;
}

template int TList::Insert(const Type& T,int k)
{
TNode *p=new TNode;
p->Data=T;
if(First)
{
if(k<=0)
{
p->Next=First;
First=p;
}
if(k>Length-1)
{
Last->Next=p;
Last=Last->Next;
Last->Next=0;
}
if(k>0 && k {
k--;
TNode *q=First;
while(k-->0)
q=q->Next;
p->Next=q->Next;
q->Next=p;
}
}
else
{
First=Last=p;
First->Next=Last->Next=0;
}
Length++;
return 1;
}

template Type TList::GetData(int k)
{
TNode *p=First;
while(k-->0)
p=p->Next;
return p->Data;
}

template void TList::SetData(const Type& T,int k)
{
TNode *p=First;
while(k-->0)
p=p->Next;
p->Data=T;
}
线性表单独以头文件形式存放。

四.广度优先搜索法
在搜索法中,广度优先搜索法是寻找最短路经的首选。
1.广度优先搜索算法的基本步骤
1)建立一个队列,将初始结点入队,并设置队列头和尾指针
2)取出队列头(头指针所指)的结点进行扩展,从它扩展出子结点,并将这些结点按扩展的顺序加入队列。
3)如果扩展出的新结点与队列中的结点重复,则抛弃新结点,跳至第六步。
4)如果扩展出的新结点与队列中的结点不重复,则记录其父结点,并将它加入队列,更新队列尾指针。
5)如果扩展出的结点是目标结点,则输出路径,程序结束。否则继续下一步。
6)如果队列头的结点还可以扩展,直接返回第二步。否则将队列头指针指向下一结点,再返回第二步。
2.搜索路径的输出
搜索到目标结点后,需要输出搜索的路径。每个结点有一个数据域last,它记录了结点的父结点,因此输出搜索路径时,就是从目标结点Q出发,根据last找到它的父结点,再根据这个结点的last找到它的父结点,....,最后找到初始结点。搜索的路径就是从初始结点循相反方向到达目标结点的路径。
3.广度优先搜索法TBFS类的结构
广度优先搜索法TBFS类是作为TEight类的一个子类。其类的结构和成员函数的实现如下:
class TBFS:public TEight
{
public:
TBFS(){}
TBFS(char *fname):TEight(fname){}
virtual void Search();
private:
void Printl(TList &L);
int Repeat(TList &L);
int Find();
};

void TBFS::Printl(TList &L)
{
TBFS T=*this;
if(T.last==-1)
return;
else
{
T=L.GetData(T.last);
T.Printl(L);
T.Printf();
}
}

int TBFS::Repeat(TList &L)
{
int n=L.Getlen();
for(int i=0;i if(L.GetData(i)==*this)
break;
return i;
}

int TBFS::Find()
{
for(int i=0;i if(p[i]!=q[i++])
return 0;
return 1;
}

void TBFS::Search()
{
TBFS T=*this;
TList L;
L.Append(T);
int head=0,tail=0;
while(head<=tail)
{
for(int i=0;i<4;i++)
{
T=L.GetData(head);
if(T.Extend(i) && T.Repeat(L)>tail)
{
T.last=head;
L.Append(T);
tail++;
}
if(T.Find())
{
T.Printl(L);
T.Printf();
return;
}
}
head++;
}
}
4.广度优先搜索法的缺点
广度优先搜索法在有解的情形总能保证搜索到最短路经,也就是移动最少步数的路径。但广度优先搜索法的最大问题在于搜索的结点数量太多,因为在广度优先搜索法中,每一个可能扩展出的结点都是搜索的对象。随着结点在搜索树上的深度增大,搜索的结点数会很快增长,并以指数形式扩张,从而所需的存储空间和搜索花费的时间也会成倍增长。

五.双向广度优先搜索法
1.双向广度优先搜索法
八数码问题具有可逆性,也就是说,如果可以从一个状态A扩展出状态B,那么同样可以从状态B扩展出状态A,这种问题既可以从初始状态出发,搜索目标状态,也可以从目标状态出发,搜索初始状态。对这类问题如果采用双向广度优先搜索法,将可以大大节省搜索的时间。
所谓双向广度优先搜索法,是同时从初始状态和目标状态出发,采用广度优先搜索的策略,向对方搜索,如果问题存在解,则两个方向的搜索会在中途相遇,即搜索到同一个结点。将两个方向的搜索路径连接起来,就可以得到从初始结点到目标结点的搜索路径。
2.双向广度优先搜索算法
双向广度优先搜索算法的基本步骤如下:
1)建立两个队列,一个是正向搜索的队列,另一个是反向搜索的队列。将初始结点放入正向队列,将目标结点放入反向队列,并设置两个队列的头和尾指针。
2)从正向队列取出队列头(头指针所指)的结点进行扩展。
3)如果扩展出的新结点与队列中的结点重复,则抛弃新结点,跳至第六步。
4)如果扩展出的新结点与队列中的结点不重复,则记录其父结点,并将它加入队列,更新队列尾指针。
5)检查扩展出的结点是否在另一方向的队列中,如果是则两个方向的搜索相遇,显示搜索路径,程序结束。否则继续下一步。
6)如果队列头的结点还可以扩展,直接返回第二步。否则将队列头指针指向下一结点,然后对另一方向搜索的队列,按照第二步开始的同样步骤处理。
3.双向广度优先搜索法的优势
广度优先搜索法搜索时,结点不断扩张,深度越大,结点数越多。如果从两个方向向对方搜索,就会在路径中间某个地方相会,这样,双方的搜索的深度都不大,所搜索过的结点数就少得多,搜索时间也就节省不少。
从理论上说,如果每一结点可扩展的子结点数为m,广度优先搜索的搜索树就是一颗m叉树,也就是每个结点都由m个分支。按完全m叉树计算,如果目标结点在第n层,广度优先搜索就必须在搜索树上扩展完n-1层的所有结点,扩展的结点数为m(mn-1)/(m-1)。对于双向广度优先搜索来说,如果两个方向的搜索在第i层生成同一子结点,那么正向搜索扩展的结点数为m(mi-1)/(m-1),反向搜索扩展的结点数为m(mn-i-1)/(m-1),搜索的结点总数为m(mi+mn-i-1)/(m-1)(其中n是最优解路径长度,i=(m+1) div 2,)。设n为偶数(n=2*i),广度优先双向搜索扩展的结点数约是广度优先搜索的2/(mi/2+1)*100%,相对减少(mi/2-1)/(mi/2+1)*100%。
4.判断两个方向的搜索相遇
在双向广度优先搜索法中,如何判断两个方向的搜索相遇呢?只要我们在生成结点的同时,判断该结点是否出现在相反方向的搜索树上即可,也就是说,在某个方向搜索中扩展出一个新结点,如果它与另一个方向已扩展出的结点重复,也就找到了解。
5.双向广度优先搜索法的TDBFS类结构
双向广度优先搜索法的TDBFS和广度优先搜索法类似,也是TEight类的子类,类结构及其成员函数的实现如下:
class TDBFS:public TEight
{
public:
TDBFS(){}
TDBFS(char *fname):TEight(fname){}
virtual void Search();
private:
void Printp(TList &L);
void Printb(TList &L);
int Repeat(TList &L);
};

void TDBFS::Printp(TList &L)
{
TDBFS T=*this;
if(T.last==-1)
return;
else
{
T=L.GetData(T.last);
T.Printp(L);
T.Printf();
}
}

void TDBFS::Printb(TList &L)
{
TDBFS T=*this;
while(T.last>-1)
{
T=L.GetData(T.last);
T.Printf();
}
}

int TDBFS::Repeat(TList &L)
{
int n=L.Getlen();
for(int i=0;i if(L.GetData(i++)==*this)
break;
return i;
}

void TDBFS::Search()
{
TDBFS T1=*this;
TDBFS T2;
for(int i=0;i {
T2.p[i]=q[i];
if(q[i]==0)
T2.spac=i;
}
T2.last=-1;
TList L1,L2;
L1.Append(T1);
L2.Append(T2);
int head1=0,tail1=0,head2=0,tail2=0;
while(head1<=tail1 || head2<=tail2)
{
for(int i=0;i<4;i++)
{
T1=L1.GetData(head1);
if(T1.Extend(i) && T1.Repeat(L1)>tail1)
{
T1.last=head1;
L1.Append(T1);
tail1++;
}
int m=T1.Repeat(L2);
if(m {
T1.Printp(L1);
T1.Printf();
T2=L2.GetData(m);
T2.Printb(L2);
return;
}
}
head1++;
for(i=0;i<4;i++)
{
T2=L2.GetData(head2);
if(T2.Extend(i) && T2.Repeat(L2)>tail2)
{
T2.last=head2;
L2.Append(T2);
tail2++;
}
int m=T2.Repeat(L1);
if(m {
T1=L1.GetData(m);
T1.Printb(L1);
T1.Printf();
T2.Printp(L2);

return;
}
}
head2++;
}
}

六.A*算法
1.启发式搜索
广度优先搜索和双向广度优先搜索都属于盲目搜索,这在状态空间不大的情况下是很合适的算法,可是当状态空间十分庞大时,它们的效率实在太低,往往都是在搜索了大量无关的状态结点后才碰到解答,甚至更本不能碰到解答。
搜索是一种试探性的查寻过程,为了减少搜索的盲目性引,增加试探的准确性,就要采用启发式搜索了。所谓启发式搜索就是在搜索中要对每一个搜索的位置进行评估,从中选择最好、可能容易到达目标的位置,再从这个位置向前进行搜索,这样就可以在搜索中省略大量无关的结点,提高了效率。
2.A*算法
A*算法是一种常用的启发式搜索算法。
在A*算法中,一个结点位置的好坏用估价函数来对它进行评估。A*算法的估价函数可表示为:
f'(n) = g'(n) + h'(n)
这里,f'(n)是估价函数,g'(n)是起点到终点的最短路径值(也称为最小耗费或最小代价),h'(n)是n到目标的最短路经的启发值。由于这个f'(n)其实是无法预先知道的,所以实际上使用的是下面的估价函数:
f(n) = g(n) + h(n)
其中g(n)是从初始结点到节点n的实际代价,h(n)是从结点n到目标结点的最佳路径的估计代价。在这里主要是h(n)体现了搜索的启发信息,因为g(n)是已知的。用f(n)作为f'(n)的近似,也就是用g(n)代替g'(n),h(n)代替h'(n)。这样必须满足两个条件:(1)g(n)>=g'(n)(大多数情况下都是满足的,可以不用考虑),且f必须保持单调递增。(2)h必须小于等于实际的从当前节点到达目标节点的最小耗费h(n)<=h'(n)。第二点特别的重要。可以证明应用这样的估价函数是可以找到最短路径的。
3.A*算法的步骤
A*算法基本上与广度优先算法相同,但是在扩展出一个结点后,要计算它的估价函数,并根据估价函数对待扩展的结点排序,从而保证每次扩展的结点都是估价函数最小的结点。
A*算法的步骤如下:
1)建立一个队列,计算初始结点的估价函数f,并将初始结点入队,设置队列头和尾指针。
2)取出队列头(队列头指针所指)的结点,如果该结点是目标结点,则输出路径,程序结束。否则对结点进行扩展。
3)检查扩展出的新结点是否与队列中的结点重复,若与不能再扩展的结点重复(位于队列头指针之前),则将它抛弃;若新结点与待扩展的结点重复(位于队列头指针之后),则比较两个结点的估价函数中g的大小,保留较小g值的结点。跳至第五步。
4)如果扩展出的新结点与队列中的结点不重复,则按照它的估价函数f大小将它插入队列中的头结点后待扩展结点的适当位置,使它们按从小到大的顺序排列,最后更新队列尾指针。
5)如果队列头的结点还可以扩展,直接返回第二步。否则将队列头指针指向下一结点,再返回第二步。
4.八数码问题的A*算法的估价函数
估价函数中,主要是计算h,对于不同的问题,h有不同的含义。那么在八数码问题中,h的含意是各什么?八数码问题的一个状态实际上是数字0~8的一个排列,用一个数组p[9]来存储它,数组中每个元素的下标,就是该数在排列中的位置。例如,在一个状态中,p[3]=7,则数字7的位置是3。如果目标状态数字3的位置是8,那么数字7对目标状态的偏移距离就是3,因为它要移动3步才可以回到目标状态的位置。
八数码问题中,每个数字可以有9个不同的位置,因此,在任意状态中的每个数字和目标状态中同一数字的相对距离就有9*9种,可以先将这些相对距离算出来,用一个矩阵存储,这样只要知道两个状态中同一个数字的位置,就可查出它们的相对距离,也就是该数字的偏移距离:
0 1 2 3 4 5 6 7 8
0 0 1 2 1 2 3 2 3 4
1 1 0 1 2 1 2 3 2 3
2 2 1 0 3 2 1 4 3 2
3 1 2 3 0 1 2 1 2 3
4 2 1 2 1 0 1 2 1 2
5 3 2 1 2 1 0 3 2 1
6 2 3 4 1 2 3 0 1 2
7 3 2 3 2 1 2 1 0 1
8 4 3 2 3 2 1 2 1 0
例如在一个状态中,数字8的位置是3,在另一状态中位置是7,那么从矩阵的3行7列可找到2,它就是8在两个状态中的偏移距离。
估价函数中的h就是全体数字偏移距离之和。
显然,要计算两个不同状态中同一数字的偏移距离,需要知道该数字在每个状态中的位置,这就要对数组p[9]进行扫描。由于状态发生变化,个数字的位置也要变化,所以每次计算h都沿线扫描数组,以确定每个数字在数组中的位置。为了简化计算,这里用一个数组存储状态中各个数字的位置,并让它在状态改变时随着变化,这样就不必在每次计算h时,再去扫描状态数组。
例如,某个状态中,数字5的位置是8,如果用数组r[9]存储位置,那么就有r[5]=8。
现在用数组r[9]存储当前状态的数字位置,而用s[9]存储目标状态的数字位置,那么当前状态数字i对目标状态的偏移距离就是矩阵中r[i]行s[i]列对应的值。
5.A*算法的类结构
A*算法的类声明如下:
class TAstar:public TEight
{
public:
TAstar(){} //构造函数
TAstar(char *fname); //带参数构造函数
virtual void Search(); //A*搜索法
private:
int f,g,h; //估价函数
int r[Num]; //存储状态中各个数字位置的辅助数组
static int s[Num]; //存储目标状态中各个数字位置的辅助数组
static int e[]; //存储各个数字相对距离的辅助数组
void Printl(TList L); //成员函数,输出搜索路径
int Expend(int i); //成员函数,A*算法的状态扩展函数
int Calcuf(); //成员函数,计算估价函数
void Sort(TList& L,int k); //成员函数,将新扩展结点按f从小到大
//顺序插入待扩展结点队列
int Repeat(TList &L); //成员函数,检查结点是否重复
};

int TAstar::s[Num],TAstar::e[Num*Num];

TAstar::TAstar(char *fname):TEight(fname)
{
for(int i=0;i {
r[p[i]]=i; //存储初始状态个个数字的位置
s[q[i]]=i++; //存储目标状态个个数字的位置
}
ifstream fin;
fin.open(".\Eightd.txt",ios::in | ios::nocreate);//打开数据文件
if(!fin)
{
cout<<"不能打开数据文件!"< return;
}
for(i=0;i fin>>e[i];
fin.close();
f=g=h=0; //估价函数初始值
}

void TAstar::Printl(TList L)
{
TAstar T=*this;
if(T.last==-1)
return;
else
{
T=L.GetData(T.last);
T.Printl(L);
T.Printf();
}
}

int TAstar::Expend(int i)
{
if(Extend(i)) //结点可扩展
{
int temp=r[p[r[0]]]; //改变状态后数字位置变化,存储改变后的位置
r[p[r[0]]]=r[0];
r[0]=temp;
return 1;
}
return 0;
}


int TAstar::Calcuf()
{
h=0;
for(int i=0;i h+=e[Num*r[i]+s[i]];
return ++g+h;
}

void TAstar::Sort(TList& L,int k)
{
int n=L.Getlen();
for(int i=k+1;i {
TAstar T=L.GetData(i);
if(this->f<=T.f)
break;
}
L.Insert(*this,i);
}

int TAstar::Repeat(TList &L)
{
int n=L.Getlen();
for(int i=0;i if(L.GetData(i)==*this)
break;
return i;
}

void TAstar::Search()
{
TAstar T=*this; //初始结点
T.f=T.Calcuf(); //初始结点的估价函数
TList L; //建立队列
L.Append(T); //初始结点入队
int head=0,tail=0; //队列头和尾指针
while(head<=tail) //队列不空则循环
{
for(int i=0;i<4;i++) //空格可能移动方向
{
T=L.GetData(head); //去队列头结点
if(T.h==0) //是目标结点
{
T.Printl(L);//输出搜索路径
T.Print(); //输出目标状态
return; //结束
}
if(T.Expend(i)) //若结点可扩展
{
int k=T.Repeat(L); //返回与已扩展结点重复的序号 if(k continue; //丢弃
T.last=head; //不是不能扩展的结点,记录父结点
T.f=T.Calcuf(); //计算f
if(k<=tail) //新结点与可扩展结点重复
{
TAstar Temp=L.GetData(k);
if(Temp.g>T.g) //比较两结点g值
L.SetData(T,k); //保留g值小的
continue;
}
T.Sort(L,head) ; //新结点插入可扩展结点队列 tail++; //队列尾指针后移
}
}
head++; //一个结点不能再扩展,队列头指针指向下一结点
}
}

七.测试程序
八.算法运行结果
1.BFS算法只能适用于到达目标结点步数较少的情况,如果步数超过15步,运行时间太长,实际上不再起作用。
2.对于随机生成的同一个可解状态,BFS算法最慢,DBFS算法较慢,A*算法较快。但在15步以内,DBFS算法与A*算法相差时间不大,超过15步后,随步数增加,A*算法的优势就逐渐明显,A*算法要比DBFS算法快5倍以上,并随步数增大而增大。到25步以上,DBFS同样因运行时间过长而失去价值。
3.一般来说,解答的移动步数每增加1,程序运行时间就要增加5倍以上。由于八数码问题本身的特点,需要检查的节点随步数增大呈指数形式增加,即使用A*算法,也难解决移动步数更多的问题。
九.问题可解性
八数码问题的一个状态实际上是0~9的一个排列,对于任意给定的初始状态和目标,不一定有解,也就是说从初始状态不一定能到达目标状态。因为排列有奇排列和偶排列两类,从奇排列不能转化成偶排列或相反。
如果一个数字0~8的随机排列871526340,用F(X)表示数字X前面比它小的数的个数,全部数字的F(X)之和为Y=∑(F(X)),如果Y为奇数则称原数字的排列是奇排列,如果Y为偶数则称原数字的排列是偶排列。
例如871526340这个排列的
Y=0+0+0+1+1+3+2+3+0=10
10是偶数,所以他偶排列。871625340
Y=0+0+0+1+1+2+2+3+0=9
9是奇数,所以他奇排列。
因此,可以在运行程序前检查初始状态和目标状态的窘是否相同,相同则问题可解,应当能搜索到路径。否则无解。


#include
#include
/* \Author:Kenny wong
* \Email:huangweilook@21cn.com
* \version:0.1
* \Date:2006-11-26
* \bref:八数码问题A*算法解法
*/

class node
{
public:
int data[9];
int zeroPos;
int distanceToOri;
int distanceToDis;
node *parent;
public:
node(){}
node(int *data, int zeroPos, node *parent, int disToOri, int disToDis)
:zeroPos(zeroPos),parent(parent),distanceToOri(disToOri),
distanceToDis(disToDis)
{
memcpy(this -> data, data,sizeof(int)*9);
}
void setParent(node *parent)
{
this -> parent = parent;
}
int getRight()
{
return distanceToDis + distanceToOri;
}
};
class AStar
{
private:
std::vector openList;
std::vector closeList;
int grid[9];//当前棋盘
int target[9];//目标棋盘
int zeroPos;
int finZeroPos;
typedef void (AStar::*p)(int*,int);
p direction[4];
public:
AStar(int *grid,int *target,int pos,int finPos)
{
memcpy(this -> grid, grid, sizeof(int)*9);
memcpy(this -> target, target, sizeof(int)*9);
zeroPos = pos;
finZeroPos = finPos;
direction[0] = &AStar::up;
direction[1] = &AStar::down;
direction[2] = &AStar::left;
direction[3] = &AStar::right;
}
~AStar(){}
void swap(int *a, int *b)
{
int temp = *a;
*a = *b;
*b = temp;
}
void up(int *input,int pos)
{
memcpy(grid,input,sizeof(int)*9);
zeroPos = pos;
swap(&grid[zeroPos], &grid[zeroPos - 3]);
zeroPos -= 3;
}
void down(int *input,int pos)
{
memcpy(grid,input,sizeof(int)*9);
zeroPos = pos;
swap(&grid[zeroPos], &grid[zeroPos + 3]);
zeroPos += 3;
}
void left(int *input,int pos)
{
memcpy(grid,input,sizeof(int)*9);
zeroPos = pos;
swap(&grid[zeroPos], &grid[zeroPos - 1]);
zeroPos -= 1;
}
void right(int *input,int pos)
{
memcpy(grid,input,sizeof(int)*9);
zeroPos = pos;
swap(&grid[zeroPos], &grid[zeroPos + 1]);
zeroPos += 1;
}
bool check(int i,int Pos)
{
if(Pos - 3 < 0)//第一行
{
switch(Pos)
{
case 0:
if( i == 0 || i == 2)
return false;
break;
case 1:
if( i == 0)
return false;
break;
case 2:
if( i == 0 || i == 3)
return false;
break;
default:
break;
}
}
if(Pos - 3 >= 3)//第三行
{
switch(Pos)
{
case 6:
if( i == 1 || i == 2)
return false;
break;
case 7:
if( i == 1)
return false;
break;
case 8:
if( i == 1 || i == 3)
return false;
break;
default:
break;
}
}
else
{
switch(Pos)
{
case 3:
if( i == 2 )
return false;
break;
case 5:
if( i == 3 )
return false;
break;
default:
break;
}
}
return true;
}
bool complete()
{
}
node *findNext()//寻找下一个待处理结点
{
std::vector::iterator it = openList.begin();
node *result = NULL;
//printf("%d\n",openList.size());
if(openList.size() == 1)
{
result = *it;
addToClose(result);
openList.clear();
}
if(openList.size() > 1)
{
++it;
std::vector::iterator end = openList.end();
std::vector::iterator minNode = openList.begin();
int minRight = (*minNode) -> getRight();
for( ; it != end; ++it)
{
int cur = (*it) -> getRight();
if(cur < minRight)
{
minRight = cur;
minNode = it;
}
}
result = *minNode;
addToClose(result);
openList.erase(minNode);
}
return result;
}
void addToClose(node *n)
{
closeList.push_back(n);
}
void addToOpen(node *n)
{
openList.push_back(n);
}
void processCur(node *cur)//处理当前结点
{
for(int i = 0 ; i < 4; ++i)
{
//检测是否可达
if(check(i,cur -> zeroPos))
{
(this ->* direction[i])(cur -> data,cur -> zeroPos);
node *tem1 = createNode();

//检测是否在closeList中
if(!inCloseList(tem1))
{
node *tem2 = NULL;
//检测是否在openList中
if( (tem2 = findinOpenList(tem1)) == NULL)
{
tem1 -> distanceToOri = cur -> distanceToOri + calDistance();
tem1 -> distanceToDis = calDistanceToDis();
tem1 -> parent = cur;
openList.push_back(tem1);
}
else
{
int temToOri = cur -> distanceToOri + calDistance();//计算经过当前点到达那个点的距离
if(temToOri < tem2 -> distanceToOri)//如果经过当前点到那个点更好
{
tem2 -> parent = cur;
tem2 -> distanceToOri = temToOri;
tem2 -> distanceToDis = calDistanceToDis();
}
}
}
}
else
{
continue;
}
}
}
int calDistanceToDis()
{
int result = 0;
for(int i = 0; i < 9; ++i)
if(grid[i] == target[i])
++result;
return result;
}
int calDistance()
{
return 1;
}
node *createNode()
{
return new node(grid,zeroPos,NULL,0,0);
}
bool inCloseList(node *_node)
{
std::vector::iterator it = closeList.begin();
std::vector::iterator end = closeList.end();
for( ; it != end; ++it)
if(memcmp(_node -> data, (*it) -> data,sizeof(int)*9) == 0 && _node -> zeroPos == (*it) -> zeroPos)
return true;
return false;
}
node *findinOpenList(node *_node)
{
std::vector::iterator it = openList.begin();
std::vector::iterator end = openList.end();
for( ; it != end; ++it)
if(memcmp(_node -> data, (*it) -> data,sizeof(int)*9) == 0 && _node -> zeroPos == (*it) -> zeroPos)
return *it;
return NULL;
}
void begin()
{
node *ori = new node(grid,zeroPos,NULL,0,0);
addToOpen(ori);
node *distination = NULL;
node *finish = new node(target,finZeroPos,NULL,0,0);
int first[9];
memcpy(first,grid,sizeof(int)*9);
while(openList.size() > 0)
{
if( (distination = findinOpenList(finish)) != NULL)
{
break;
}
node *temp = findNext();
if(temp != NULL)
processCur(temp);
}

std::vector path;
if(distination != NULL && distination -> parent != NULL)
{
node *temp = distination;
while(temp -> parent != NULL)
{
path.push_back(temp -> data);
temp = temp -> parent;
}
}
path.push_back(first);
std::vector::iterator it = path.end();
--it;
std::vector::iterator begin = path.begin();
for( ; it != begin; --it)
{
for(int i = 0; i < 9; ++i)
{
printf(" %d",(*it)[i]);
if( (i + 1) % 3 == 0)
printf("\n");
}
printf("\n");
}
for(int i = 0; i < 9; ++i)
{
printf(" %d",(*it)[i]);
if( (i + 1) % 3 == 0)
printf("\n");
}
}
};
int main()
{
int ori[9] = {1,2,5,7,6,4,0,8,3};
int tar[9] = {7,1,5,8,2,4,6,0,3};
AStar A(ori,tar,6,7);
A.begin(); return 0;
}

A*算法求解八数码问题 1、A*算法基本思想: 1)建立一个队列,计算初始结点的估价函数f,并将初始结点入队,设置队列头和尾指针。 2)取出队列头(队列头指针所指)的结点,如果该结点是目标结点,则输出路径,程序结束。否则对结点进行扩展。 3)检查扩展出的新结点是否与队列中的结点重复,若与不能再扩展的结点重复(位于队列头指针之前),则将它抛弃;若新结点与待扩展的结点重复(位于队列头指针之后),则比较两个结点的估价函数中g的大小,保留较小g值的结点。跳至第五步。 4)如果扩展出的新结点与队列中的结点不重复,则按照它的估价函数f大小将它插入队列中的头结点后待扩展结点的适当位置,使它们按从小到大的顺序排列,最后更新队列尾指针。 5)如果队列头的结点还可以扩展,直接返回第二步。否则将队列头指针指向下一结点,再返回第二步。 2、程序运行基本环境: 源程序所使用编程语言:C# 编译环境:VS2010,.net framework 4.0 运行环境:.net framework 4.0 3、程序运行界面 可使用程序中的test来随机生成源状态与目标状态 此停顿过程中按Enter即可使程序开始运行W(n)部分; 此停顿部分按Enter后程序退出; 4、无解问题运行情况 这里源程序中是先计算源状态与目标状态的逆序对的奇偶性是否一致来判断是否有解的。下面是无解时的运行画面: 输入无解的一组源状态到目标状态,例如: 1 2 3 4 5 6 7 8 0 1 2 3 4 5 6 8 7 0 运行画面如下: 5、性能比较 对于任一给定可解初始状态,状态空间有9!/2=181440个状态;当采用不在位棋子数作为启发函数时,深度超过20时,算法求解速度较慢; 其中启发函数P(n)与W(n)的含义如下: P(n): 任意节点与目标结点之间的距离; W(n): 不在位的将牌数; 源状态 目标状态 P(n) 生成节点数 W(n) 生成节点数 P(n) 扩展节点数 W(n) 扩展节点数 2 8 3 1 6 4 7 0 5 1 2 3 8 0 4 7 6 5 11 13 5 6 1 2 3 8 0 4 7 6 5 0 1 3 8 2 4 7 6 5 6 6 2 2 4 8 2 5 1 6 7 0 3 7 4 2 8 5 6 1 3 0 41 79 22 46 6 2 5 8 7 0 3 1 4 0 3 6 7 1 8 4 5 2 359 10530 220 6769 7 6 3 1 0 4 8 5 2 2 8 7 1 3 4 6 5 0 486 8138 312 5295 下图是解决随机生成的100中状态中,P(n)生成函数的生成节点与扩展节点统计图: 由上图可知,P(n)作为启发函数,平均生成节点数大约在1000左右,平均扩展节点数大约在600左右; 下图是解决随机生成的100中状态中,W(n)生成函数的生成节点与扩展节点统计图: 由上图可知,W (n)作为启发函数,平均生成节点数大约在15000左右,是P(n)作为启发函数时的平均生成节点的15倍;W (n)作为启发函数,平均扩展节点数大约在10000左右,是P(n)作为启发函数时的平均扩展节点的15倍; 下图是解决随机生成的100中状态中,两个生成函数的生成节点与扩展节点统计图: 由上述图表可以看到,将P(n)作为启发函数比将W(n)作为启发函数时,生成节点数与扩展节点数更稳定,相比较来说,采用P(n)作为启发函数的性能比采用W(n)作为启发函数的性能好。 6、源代码说明 1)AStar-EightDigital-Statistics文件夹:用来随机生成100个状态,并对这100个状态分别用P(n)与W(n)分别作为启发函数算出生成节点以及扩展节点,以供生成图表使用;运行界面如下: 2)Test文件夹:将0-8这9个数字随机排序,用来随机生成源状态以及目标状态的;运行界面如下: 3)AStar-EightDigital文件夹:输入源状态和目标状态,程序搜索出P(n)与W(n)分别作为启发函数时的生成节点数以及扩展节点数,并给出从源状态到目标状态的移动步骤;运行界面如下: 提高了运行速度的几处编码思想: 1、 在维护open以及close列表的同时,也维护一个类型为hashtable的open以及close列表,主要用来提高判断当前节点是否在open列表以及close列表中出现时的性能; 2、 对于每个状态,按照从左到右,从上到下,依次将数字拼接起来,形成一个唯一标识identify,通过该标识,可以直接判断两个状态是否是同一个状态,而不需要循环判断每个位置上的数字是否相等 3、 在生成每个状态的唯一标识identify时,同时计算了该状态的空格所在位置,通过空格所在位置,可以直接判断能否进行上移、下移、左移、右移等动作; 4、 只计算初始节点的h值,其它生成的节点的h值是根据当前状态的h值、移动的操作等计算后得出的,规则如下: a) 采用W(n)这种方式,不在位置的将牌数,共有以下3中情况: i. 该数字原不在最终位置上,移动后,在其最终位置上 这种情况下,生成的子节点的h值= 父节点的h值-1 ii. 该数字原在最终位置上,移动后,不在其最终位置上 这种情况下,生成的子节点的h值= 父节点的h值 +1 iii. 该数字原不在最终位置上,移动后,还是不在其最终位置上 这种情况下,生成的子节点的h值= 父节点的h值 iv. 该数字原在最终位置上,移动后,还在其最终位置 这种情况不存在 b) 采用P(n)这种方式,节点与目标距离,可通过下面3步完成 i. 首先计算在原位置时,与目标位置的距离,命名为Distance1 ii. 移动后,计算当前位置与目标位置的距离,命名为Distance2 iii. 计算子节点的h值: 子节点的h值 = 父节点的h值- Distance1+ Distance2 5、 在任意状态中的每个数字和目标状态中同一数字的相对距离就有9*9种,可以先将这些相对距离算出来,用一个矩阵存储,这样只要知道两个状态中同一个数字的位置,就可查出它们的相对距离,也就是该数字的偏移距离;例如在一个状态中,数字8的位置是3,在另一状态中位置是7,那么从矩阵的3行7列可找到2,它就是8在两个状态中的偏移距离。
好的,以下是用C++编写的深度优先搜索解决九宫格重排问题的代码: ```c++ #include <iostream> #include <vector> #include <algorithm> using namespace std; const int N = 3; // 九宫格的边长 const int N2 = N * N; // 九宫格的格子数 int MDT[N2][N2]; // 预处理的曼哈顿距离表格 // 计算曼哈顿距离 int manhattan(int *state) { int sum = 0; for (int i = 0; i < N2; i++) { if (state[i] == 0) continue; // 跳过空格 sum += MDT[i][state[i] - 1]; // 按照表格查找曼哈顿距离 } return sum; } // 判断是否已经到达目标状态 bool is_target(int *state) { for (int i = 0; i < N2; i++) { if (state[i] != i + 1) return false; // 如果有格子不在目标位置上,返回false } return true; } // 搜索函数 bool dfs(int depth, int *state, int prev) { if (depth + manhattan(state) > prev) return false; // 剪枝:如果当前深度+曼哈顿距离已经超过了前一次搜索的深度,返回false if (is_target(state)) return true; // 如果已经到达目标状态,返回true int pos; // 空格的位置 for (int i = 0; i < N2; i++) { if (state[i] == 0) { pos = i; break; } } int x = pos / N, y = pos % N; // 空格的行列坐标 static const int dx[] = {1, 0, -1, 0}; // 方向数组 static const int dy[] = {0, 1, 0, -1}; for (int dir = 0; dir < 4; dir++) { // 四个方向依次尝试 int nx = x + dx[dir], ny = y + dy[dir]; int npos = nx * N + ny; if (nx < 0 || nx >= N || ny < 0 || ny >= N) continue; // 越界了,跳过 swap(state[pos], state[npos]); // 交换空格和相邻格子的位置 if (dfs(depth + 1, state, prev)) return true; // 递归搜索 swap(state[pos], state[npos]); // 恢复当前状态 } return false; } int main() { // 预处理曼哈顿距离表格 for (int i = 0; i < N2; i++) { for (int j = 0; j < N2; j++) { MDT[i][j] = abs(i / N - j / N) + abs(i % N - j % N); } } int state[N2]; // 初始状态 cout << "请输入初始状态(按行输入,空格用0表示):" << endl; for (int i = 0; i < N2; i++) { cin >> state[i]; } int depth = 0; // 初始深度为0 while (!dfs(0, state, depth)) { depth++; // 如果搜索不到解,增加搜索深度 } cout << "最少需要移动" << depth << "步才能完成重排" << endl; return 0; } ``` 该代码用了一个预处理的曼哈顿距离表格来加速计算曼哈顿距离,使用深度优先搜索策略来解决九宫格重排问题,同时加入了一些剪枝以提高搜索效率。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值