A*搜索算法C++实现八数码问题---启发式搜索

(一)问题描述

在一个3*3的方棋盘上放置着1,2,3,4,5,6,7,8八个数码,每个数码占一格,且有一个空格。这些数码可以在棋盘上移动,其移动规则是:与空格相邻的数码方格可以移入空格。现在的问题是:对于指定的初始棋局和目标棋局,给出数码的移动序列。该问题称八数码难题或者重排九宫问题。

在这里插入图片描述

(二)问题分析

八数码问题是个典型的状态图搜索问题。搜索方式有两种基本的方式,即树式搜索和线式搜索。搜索策略大体有盲目搜索和启发式搜索两大类。盲目搜索就是无“向导”的搜索,启发式搜索就是有“向导”的搜索。

1、启发式搜索

由于时间和空间资源的限制,穷举法只能解决一些状态空间很小的简单问题,而对于那些大状态空间的问题,穷举法就不能胜任,往往会导致“组合爆炸”。所以引入启发式搜索策略。启发式搜索就是利用启发性信息进行制导的搜索。它有利于快速找到问题的解。

由八数码问题的部分状态图可以看出,从初始节点开始,在通向目标节点的路径上,各节点的数码格局同目标节点相比较,其数码不同的位置个数在逐渐减少,最后为零。所以,这个数码不同的位置个数便是标志一个节点到目标节点距离远近的一个启发性信息,利用这个信息就可以指导搜索。即可以利用启发信息来扩展节点的选择,减少搜索范围,提高搜索速度。

启发函数设定。对于八数码问题,可以利用棋局差距作为一个度量。搜索过程中,差距会逐渐减少,最终为零,为零即搜索完成,得到目标棋局。

(三)数据结构与算法设计

该搜索为一个搜索树。为了简化问题,搜索树节点设计如下:

struct Chess//棋盘

{

       int cell[N][N];//数码数组

       int Value;//评估值

       Direction BelockDirec;//所屏蔽方向

       struct Chess * Parent;//父节点

};
int cell[N][N];    数码数组:记录棋局数码摆放状态。

int Value;        评估值:记录与目标棋局差距的度量值。

Direction BelockDirec; 所屏蔽方向:一个屏蔽方向,防止回推。

Direction :enum Direction{None,Up,Down,Left,Right};//方向枚举

struct Chess * Parent;  父节点:指向父亲节点。

下一步可以通过启发搜索算法构造搜索树。

1、局部搜索树样例:
在这里插入图片描述
2、搜索过程

搜索采用广度搜索方式,利用待处理队列辅助,逐层搜索(跳过劣质节点)。搜索过程如下:

(1)、把原棋盘压入队列;

(2)、从棋盘取出一个节点;

(3)、判断棋盘估价值,为零则表示搜索完成,退出搜索;

(4)、扩展子节点,即从上下左右四个方向移动棋盘,生成相应子棋盘;

(5)、对子节点作评估,是否为优越节点(子节点估价值小于或等于父节点则为优越节点),是则把子棋盘压入队列,否则抛弃;

(5)、跳到步骤(2);

3、算法的评价

完全能解决简单的八数码问题,但对于复杂的八数码问题还是无能为力。现存在的一些优缺点。

1、可以改变数码规模(N),来扩展成N*N的棋盘,即扩展为N数码问题的求解过程。

2、 内存泄漏。由于采用倒链表的搜索树结构,简化了数据结构,但有部分被抛弃节点的内存没有很好的处理,所以会造成内存泄漏;

3、 采用了屏蔽方向,有效防止往回搜索(节点的回推),但没能有效防止循环搜索,所以不能应用于复杂度较大的八数码问题;
在这里插入图片描述
源代码:

#include "stdio.h"
#include "stdlib.h"
#include "time.h"
#include "string.h"
#include <queue>
#include <stack>
using namespace std;

const int N=3;//3*3棋盘
const int Max_Step=30;//最大搜索深度

enum Direction{None,Up,Down,Left,Right};//方向
struct Chess//棋盘
{
    int cell[N][N];//数码数组
    int Value;//评估值
    Direction BelockDirec;//所屏蔽方向
    struct Chess * Parent;//父节点
};

//打印棋盘
void PrintChess(struct Chess *TheChess)
{
    printf("------------------------------------------------------------------------/n");
    for(int i=0;i<N;i++)
    {
        printf("/t");
        for(int j=0;j<N;j++)
        {
            printf("%d/t",TheChess->cell[i][j]);
        }
        printf("/n");
    }
    printf("/t/t/t/t差距:%d/n",TheChess->Value);
}
//移动棋盘
struct Chess * MoveChess(struct Chess * TheChess,Direction Direct,bool CreateNewChess)
{
    struct Chess * NewChess;

    //获取空闲格位置
    int i,j;
    for(i=0;i<N;i++)
    {
        bool HasGetBlankCell=false;
        for(j=0;j<N;j++)
        {
            if(TheChess->cell[i][j]==0)
            {
                HasGetBlankCell=true;
                break;
            }
        }
        if(HasGetBlankCell)
            break;
    }

    //移动数字
    int t_i=i,t_j=j;
    bool AbleMove=true;
    switch(Direct)
    {
    case Up:
        t_i++;
        if(t_i>=N)
            AbleMove=false;
        break;
    case Down:
        t_i--;
        if(t_i<0)
            AbleMove=false;
        break;
    case Left:
        t_j++;
        if(t_j>=N)
            AbleMove=false;
        break;
    case Right:
        t_j--;
        if(t_j<0)
            AbleMove=false;
        break;
    };
    if(!AbleMove)//不可以移动则返回原节点
    {
        return TheChess;
    }

    if(CreateNewChess)
    {
        NewChess=new Chess();
        for(int x=0;x<N;x++)
        {
            for(int y=0;y<N;y++)
                NewChess->cell[x][y]=TheChess->cell[x][y];
        }
    }
    else
        NewChess=TheChess;
    NewChess->cell[i][j]=NewChess->cell[t_i][t_j];
    NewChess->cell[t_i][t_j]=0;

    return NewChess;
}

//初始化一个初始棋盘
struct Chess * RandomChess(const struct Chess * TheChess)
{
    int M=30;//随机移动棋盘步数
    struct Chess * NewChess;
    NewChess=new Chess();
    memcpy(NewChess,TheChess,sizeof(Chess));
    srand((unsigned)time(NULL));
    for(int i=0;i<M;i++)
    {   
        int Direct=rand()%4;
        //printf("%d/n",Direct);
        NewChess=MoveChess(NewChess,(Direction) Direct,false);
    }
    return NewChess;
}

//估价函数
int Appraisal(struct Chess * TheChess,struct Chess * Target)
{
    int Value=0;
    for(int i=0;i<N;i++)
    {
        for(int j=0;j<N;j++)
        {
            if(TheChess->cell[i][j]!=Target->cell[i][j])
                Value++;
        }
    }
    TheChess->Value=Value;
    return Value;
}

//搜索函数
struct Chess * Search(struct Chess* Begin,struct Chess * Target)
{
    Chess * p1,*p2,*p;
    int Step=0;//深度
    p=NULL;
    queue<struct Chess *> Queue1;
    Queue1.push(Begin);
    //搜索
    do{
        p1=(struct Chess *)Queue1.front();
        Queue1.pop();
        for(int i=1;i<=4;i++)//分别从四个方向推导出新子节点
        {
            Direction Direct=(Direction)i;
            if(Direct==p1->BelockDirec)//跳过屏蔽方向
                continue;
       
            p2=MoveChess(p1,Direct,true);//移动数码
            if(p2!=p1)//数码是否可以移动
            {
                Appraisal(p2,Target);//对新节点估价
                if(p2->Value<=p1->Value)//是否为优越节点
                {
                    p2->Parent=p1;
                    switch(Direct)//设置屏蔽方向,防止往回推
                    {
                    case Up:p2->BelockDirec=Down;break;
                    case Down:p2->BelockDirec=Up;break;
                    case Left:p2->BelockDirec=Right;break;
                    case Right:p2->BelockDirec=Left;break;
                    }
                    Queue1.push(p2);//存储节点到待处理队列
                    if(p2->Value==0)//为0则,搜索完成
                    {
                        p=p2;
                        i=5;
                    }
                }
                else
                {
                    delete p2;//为劣质节点则抛弃
                    p2=NULL;
                }
            }
        }
        Step++;
        if(Step>Max_Step)
            return NULL;
    }while(p==NULL || Queue1.size()<=0);

    return p;
}

main()
{
    Chess * Begin,Target,* T;
    //设定目标棋盘 [0 1 2],[3 4 5],[6 7 8]
    for(int i=0;i<N;i++)
    {
        for(int j=0;j<N;j++)
        {
            Target.cell[i][j]=i*N+j;
        }
    }
    //获取初始棋盘
    Begin=RandomChess(&Target);
    Appraisal(Begin,&Target);
    Begin->Parent=NULL;
    Begin->BelockDirec=None;
    Target.Value=0;
    printf("目标棋盘:/n");
    PrintChess(&Target);
    printf("初始棋盘:/n");
    PrintChess(Begin);
    //图搜索
    T=Search(Begin,&Target);
    //打印
    if(T)
    {
        /*把路径倒序*/
        Chess *p=T;
        stack<Chess *>Stack1;
        while(p->Parent!=NULL)
        {
            Stack1.push(p);
            p=p->Parent;
        }
        printf("搜索结果:/n");
        while(!Stack1.empty())
        {
            PrintChess(Stack1.top());
            Stack1.pop();
        }
        printf("/n完成!");
    }else
        printf("搜索不到结果.深度为%d/n",Max_Step);

    scanf("%d",T);
}

  • 4
    点赞
  • 49
    收藏
    觉得还不错? 一键收藏
  • 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在两个状态中的偏移距离。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值