暴力求解法_隐式图搜索(八数码问题)

八数码问题

题目:编号为1~8的8个正方形被摆成3行3列(由一个格子留空),每次可以把与空格相邻的滑块(有公共边相邻才算)移到空格,而它原来的位置就成为了新的空格。给定初始局面和目标局面(用0表示空格),你的任务是计算出最少移动步数。如果无法达到,则输出-1。

输入:

2 6 4 1 3 7 0 5 8
8 1 5 7 3 6 4 0 2

输出:

31

映射(编码和解码)

第一种方法:把排列“变成”整数,然后只开一个一维数组。也就是说,我们设计一套排列的编码和解码函数,把0~8的全排列和0~362879的整数一一对应起来。

#include 
#include 
#include 

#define MAXSIZE 1000000

typedef int State[9];
/*
s的数据类型是长度为100的数组,数组元素s[i]的数据类型是State是长度为10的数组,等同于定义了一个二维数组s[100]=iarr[100][9].
int iarr[100][9],arr的数据类型是长度为100的数组,数组元素是arr[i],arr[i]的数据类型是长度为10的数组.
*/
State st[MAXSIZE];//状态数一定要多定义,否则一不小心就超了
State goal;
int dist[MAXSIZE];//距离数组
int go[][2] =//上下左右方向的数组
{
    {-1,0},{
    1,0},{
    0,-1},{
    0,1}};

int iVis[362880],fact[9];//9!=362880,8!=40320,9*8!=9!共有这么多排序,然后我们寻找,我们初始化fact
//节点查找表
void init_lookup_table()//初始化查找表
{
    fact[0] = 1;
    for(int i = 1 ; i < 9; i++)
    {fact[i] = fact[i-1]*i;}
    //fact[0]=1,fact[1]=1!,fact[2]=2!...fact[8]=8!
}
//bool isInsert(State state)
int try_to_insert(int n)
//去重,采用编码与解码机制,确保一个9维状态只能映射到一个数字,并且映射的数字最大值不能超过9!
{
    int iCode = 0;//编码值
    for(int i = 0 ; i < 9 ; i++)
    {
        int iCnt = 0;
        for(int j = i+1; j < 9;j++)
        {
            if(st[n-1][j] < st[n-1][i])//统计每个排列中,后面小于前面排列的数字个数
            {   iCnt++;}
        }
        iCode += fact[8-i]*iCnt;
    }
    if(iVis[iCode])//如果已经访问过
        return 0;
    else{
        return iVis[iCode] = 1;//同时完成赋值和返回值操作
    }
}

int bfs()//宽搜,队列队列队列。
{
    int rear = 2,front = 1;
    init_lookup_table();//初始化。这里进行判重,对于树不需要判断重复,但是对于图需要判断。
    while(front < rear)
    {
        State& state = st[front];
        /*
         引用就是某一变量(目标)的一个别名,对引用的操作与对变量直接操作完全一样。
        引用的声明方法:类型标识符 &引用名=目标变量名;
        例如:int a; int &ra=a; //定义引用ra,它是变量a的引用,即别名
         */
        if(memcmp(goal,state,sizeof(state)) == 0)
        {
    //判断是否找到的工作要放在开头,找到返回
            return front;
        }
        int iZ,iX,iY;
        for(int i = 0 ; i < 9; i++)//确定0所在的位置
        {
            if(!state[i])
            {   iZ = i;
                iX=iZ/3;iY=iZ%3;
                break;//凡是寻找类的问题,一旦找到,必须用break跳出
            }
        }
        //生成下一步位置
        int newz,newx,newy;
        for(int i = 0; i < 4; i++)
        {
            newx = go[i][0] + iX;
            newy = go[i][1] + iY;
            newz = newx*3 + newy;//确定0的新位置
            if(newx >= 0 && newx < 3 && newy >= 0 && newy < 3)//剪枝
            {
                State& newState = st[rear];
                //这里应该从队尾提前将原来老的状态拷贝给新的状态,再将新状态中需要修改0元素的地方进行修改,需要用引用,为修改做准备
                memcpy(&newState,&state,sizeof(state));
                newState[newz] = state[iZ];//新矩阵0元素的位置上放0元素
                newState[iZ] = state[newz];//新矩阵原来放0元素的位置上现在放上新生成的0元素的坐标,这里必须用原来被交换元素的值替换
                dist[rear] = dist[front] + 1;//更新移动的步数
            }
            if(try_to_insert(rear))//修改队尾指针
            {
                rear++;
            }
        }
        front++;//不管是否成功,修改队头
    }
    return 0;
}

void process()
{
    //初始化队头和队尾元素
    for(int i = 0 ; i < 9;i++)
    {
        scanf("%d",&st[1][i]);
    }
    for(int j = 0 ; j < 9; j++)
    {
        scanf("%d",&goal[j]);
    }
    dist[1] = 0;//设置第一步移动的距离为0
    memset(iVis,0,sizeof(iVis));//初始化访问内存块,就是这句话没加导致错误的
}

int main(int argc,char* argv[])
{
    process();
    int ans = bfs();
    //返回的是front的值,但不是移动次数,移动次数得用dist来计算,因为这里是宽度优先搜索,如果用front的值,那么中间尝试的节点也算了
    if(ans > 0)
        printf("%d\n",dist[ans]);
    else printf("-1\n");
    return 0;
}

注意引用的涵义,一开始不知道就很奇怪,那个st[rear]是如何赋值的。

哈希技术

第二种方法方法是使用哈希(hash)函数。简单的说,就是要把借点变成整数,但不必一一对应。换句话说,只需要设计一个所谓的哈希函数h(x),然后将任意节点x映射到某个给定的范围[0,M-1]的整数即可。

#include 
#include 
#include 
typedef int State[9];
State st[MAXSIZE];//状态数一定要多定义,否则一不小心就超了
State goal;
int dist[MAXSIZE];//距离数组
int go[][2] =//上下左右方向的数组
{
    {-1,0},{
    1,0},{
    0,-1},{
    0,1}};
int iVis[362880],fact[9];//9!=362880,8!=40320,9*8!=9!共有这么多排序,然后我们寻找,我们初始化fact
//修改处
#define MAXSIZE 1000000
int head[MAXSIZE],next[MAXSIZE];
void init_lookup_table()//初始化查找表
{
    memset(head,0,sizeof(head));
}
int hash(State &s){
    int v=0;
    for(int i=0;i<9;i++) v=v*10+s[i];
    //随便算,例如把9个数字组合成9位数
    return v%MAXSIZE;
}
int try_to_insert(int s)
{
    int h=hash(st[s]);//求第s个状态的hash值
    int u=head[h];//从表头开始查找链表
    while(u){
    //这个while的作用是遍历具有相同哈希值的一条链,如果能找到,说明插入失败,如果找不到,则采用头插法插入新来的节点  
        if(memcmp(st[u],st[s],sizeof(st[s]))==0)//找到了,插入失败
            return 0;
        u=next[u];//竖着链表继续查找
    }
    next[s]=head[h];//插入到链表中
    head[h]=s;
    return 1;
}

int bfs()//宽搜,队列队列队列。
{
    int rear = 2,front = 1;
    init_lookup_table();//初始化。这里进行判重,对于树不需要判断重复,但是对于图需要判断。
    while(front < rear)
    {
        State& state = st[front];
        /*
         引用就是某一变量(目标)的一个别名,对引用的操作与对变量直接操作完全一样。
        引用的声明方法:类型标识符 &引用名=目标变量名;
        例如:int a; int &ra=a; //定义引用ra,它是变量a的引用,即别名
         */
        if(memcmp(goal,state,sizeof(state)) == 0)
        {
    //判断是否找到的工作要放在开头,找到返回
            return front;
        }
        int iZ,iX,iY;
        for(int i = 0 ; i < 9; i++)//确定0所在的位置
        {
            if(!state[i])
            {   iZ = i;
                iX=iZ/3;iY=iZ%3;
                break;//凡是寻找类的问题,一旦找到,必须用break跳出
            }
        }
        //生成下一步位置
        int newz,newx,newy;
        for(int i = 0; i < 4; i++)
        {
            newx = go[i][0] + iX;
            newy = go[i][1] + iY;
            newz = newx*3 + newy;//确定0的新位置
            if(newx >= 0 && newx < 3 && newy >= 0 && newy < 3)//剪枝
            {
                State& newState = st[rear];
                //这里应该从队尾提前将原来老的状态拷贝给新的状态,再将新状态中需要修改0元素的地方进行修改,需要用引用,为修改做准备
                memcpy(&newState,&state,sizeof(state));
                newState[newz] = state[iZ];//新矩阵0元素的位置上放0元素
                newState[iZ] = state[newz];//新矩阵原来放0元素的位置上现在放上新生成的0元素的坐标,这里必须用原来被交换元素的值替换
                dist[rear] = dist[front] + 1;//更新移动的步数
            }
            if(try_to_insert(rear))//修改队尾指针
            {
                rear++;
            }
        }
        front++;//不管是否成功,修改队头
    }
    return 0;
}

void process()
{
    //初始化队头和队尾元素
    for(int i = 0 ; i < 9;i++)
    {
        scanf("%d",&st[1][i]);
    }
    for(int j = 0 ; j < 9; j++)
    {
        scanf("%d",&goal[j]);
    }
    dist[1] = 0;//设置第一步移动的距离为0
    memset(iVis,0,sizeof(iVis));//初始化访问内存块,就是这句话没加导致错误的
}

int main(int argc,char* argv[])
{
    process();
    int ans = bfs();
    //返回的是front的值,但不是移动次数,移动次数得用dist来计算,因为这里是宽度优先搜索,如果用front的值,那么中间尝试的节点也算了
    if(ans > 0)
        printf("%d\n",dist[ans]);
    else printf("-1\n");
    return 0;
}

STL集合

第三种STL中的集合。我觉得这个方法算是最简单了,你只要知道set 是一个集合的容器。init初始化函数,只需要对vis进行清空。插入时也只需要判断vis.count(v)是否在容器中。

别人家 set 头文件的整理

#include 
#include 
#include 
#include 
#include
using namespace std;
#define MAXSIZE 1000000
typedef int State[9];
State st[MAXSIZE];//状态数一定要多定义,否则一不小心就超了
State goal;
int dist[MAXSIZE];//距离数组
int go[][2] =//上下左右方向的数组
{
    {-1,0},{
    1,0},{
    0,-1},{
    0,1}};
int iVis[362880],fact[9];//9!=362880,8!=40320,9*8!=9!共有这么多排序,然后我们寻找,我们初始化fact
//修改处
set<int> vis;
void init_lookup_table()//初始化查找表
{
    vis.clear();
}
int try_to_insert(int s)
{
    int v=0;
    for(int i=0;i<9;i++) v=v*10+st[s][i];
    if(vis.count(v)) return 0; //判断是否在容器中。是,返回错误。
    vis.insert(v);
    return 1;
}

/*另一种方式,声明一个结构体,并重载“括号运算”来比较两个状态。
struct cmp{
    bool operator()(int a,int b) const{//重新定义 a vis;
void init_lookup_table(){vis.clear();}
int try_to_insert(int s){
    if(vis.count(s)) return 0;
    vis.insert(s);
    return 1;
}
*/
int bfs()//宽搜,队列队列队列。
{
    int rear = 2,front = 1;
    init_lookup_table();//初始化。这里进行判重,对于树不需要判断重复,但是对于图需要判断。
    while(front < rear)
    {
        State& state = st[front];
        if(memcmp(goal,state,sizeof(state)) == 0)
        {
    //判断是否找到的工作要放在开头,找到返回
            return front;
        }
        int iZ,iX,iY;
        for(int i = 0 ; i < 9; i++)//确定0所在的位置
        {
            if(!state[i])
            {   iZ = i;
                iX=iZ/3;iY=iZ%3;
                break;//凡是寻找类的问题,一旦找到,必须用break跳出
            }
        }
        //生成下一步位置
        int newz,newx,newy;
        for(int i = 0; i < 4; i++)
        {
            newx = go[i][0] + iX;
            newy = go[i][1] + iY;
            newz = newx*3 + newy;//确定0的新位置
            if(newx >= 0 && newx < 3 && newy >= 0 && newy < 3)//剪枝
            {
                State& newState = st[rear];
                //这里应该从队尾提前将原来老的状态拷贝给新的状态,再将新状态中需要修改0元素的地方进行修改,需要用引用,为修改做准备
                memcpy(&newState,&state,sizeof(state));
                newState[newz] = state[iZ];//新矩阵0元素的位置上放0元素
                newState[iZ] = state[newz];//新矩阵原来放0元素的位置上现在放上新生成的0元素的坐标,这里必须用原来被交换元素的值替换
                dist[rear] = dist[front] + 1;//更新移动的步数
            }
            if(try_to_insert(rear))//修改队尾指针
            {
                rear++;
            }
        }
        front++;//不管是否成功,修改队头
    }
    return 0;
}

void process()
{
    //初始化队头和队尾元素
    for(int i = 0 ; i < 9;i++)
    {
        scanf("%d",&st[1][i]);
    }
    for(int j = 0 ; j < 9; j++)
    {
        scanf("%d",&goal[j]);
    }
    dist[1] = 0;//设置第一步移动的距离为0
    memset(iVis,0,sizeof(iVis));//初始化访问内存块,就是这句话没加导致错误的
}

int main(int argc,char* argv[])
{
    process();
    int ans = bfs();
    //返回的是front的值,但不是移动次数,移动次数得用dist来计算,因为这里是宽度优先搜索,如果用front的值,那么中间尝试的节点也算了
    if(ans > 0)
        printf("%d\n",dist[ans]);
    else printf("-1\n");
    return 0;
}

你可能感兴趣的:(算法竞赛入门,暴力求解法,搜索,编码,八数码问题,算法竞赛入门经典第二版)