八数码问题

编号为1-8的8个正方形滑块被摆成三行三列(有一个格子留空),如图所示:

每次可以把与空格相邻的滑块移到空格中,而它原来的位置就成了新的空格。给定初始局面和目标局面,你的任务是计算出最少的移动步数,如果无法达到目标局面,输出-1。

 

 

264
137
 58

 

815
736
4 2

 

样例输入:2 6 4 1 3 7 0 5 8

                    8 1 5 7 3 6 4 0 2

样例输出:31

 

        不难把八数码问题归结为图上的最短路问题。其中每个状态就是9个格子中的滑块编号(从上到下,从左到右地把它们放到一个包含9个元素的数组中)。具体程序如下:

#include <iostream>
#include <cstring>
using namespace std;
typedef int State[9];    /**定义状态类型*/
const int MAXSTATE=1000000;
State st[MAXSTATE],goal; /**状态数组,所有状态都保存在这里**/
int dist[MAXSTATE];      /**距离数组*/
int fa[MAXSTATE];        /**父亲编号数组,用于打印*/
int vis[36288],fact[9];
void init_lookup_table();
int try_to_insert(int s);
const int dx[]={-1,1,0,0};
const int dy[]={0,0,-1,1};
/**BFS返回目标状态 在st数组下标**/
int bfs()
{
    init_lookup_table();  /*初始化查找表*/
    int front=1,rear=2;
    while (front < rear)
    {
        State& s =st[front];
        if ( memcmp( goal, s,sizeof (s) )==0 ) return front;
        int z;
        for (z=0; z<9 ; z++)
            if (!s[z]) break;
        int x=z/3,y=z%3;
        for (int d=0;d<4;d++)
        {
            int newx=x+dx[d];
            int newy=y+dy[d];
            int newz=newx*3+newy;
            if (newx>=0 && newx<3 && newy>=0 && newy<3 )
            {
                State& t = st[rear];
                memcpy(&t,&s,sizeof(s));
                t[newz]=s[z];
                t[z]=s[newz];
                dist[rear] = dist[front]+1 ;
                if (try_to_insert(rear)) rear++;
            }
        }
        front++;
    }
    return 0;
}


        注意,此处用到了 cstring 的 memcmp 和 memcpy 来完成整块内存的比较和复制,比用循环比较和循环赋值要快。主程序很容易实现:

int main()
{
    for (int i=0;i<9;i++) cin>>st[1][i];   /*起始状态*/
    for (int i=0;i<9;i++) cin>>goal[i];    /*目标状态*/
    int ans=bfs();                          /*返回目标状态的下标*/
    if (ans>0) cout<<dist[ans]<<endl;
    else cout<<"-1"<<endl;
    return 0;
}


        注意,应该在调用bfs函数之前设置好st[1]和goal。上面的代码几乎是完整的,唯一没有涉及的是init_lookup_taible()和try_to_insert(rear)的实现。为什么会有这个东西呢?还记得bfs中的vis数组吗?我们用它来进行bfs中的判重。这里的查找表和它的功能类似,也是避免我们将同一个节点结构访问多次。树的bfs不需要判重,因为根本不会重复,但对于图来说,如果不判重,时间和空间都将产生极大的浪费。

    如何判重呢?难道要声明一个9维数组vis,然后 if(vis[s[0]][s[1]][s[2]][s[3]]...[s[8]])  ? 无论程序好不好看,9维数组的每维都要包含9个元素,一共有9^9=387420489项,太多了,数组开不下。实际的节点数并没有这么多。(0-8的排列总共只有9!=362880个),为什么9维数组开不下呢?原因在于,数组中有很多项都没有用到,但却占据了空间。

        下面讨论3种常见的方法来解决这个问题,同时也将其用到八数码问题中。

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

int vis[36288],fact[9];

void init_lookup_table()
{
    fact[0]=1;
    for (int i=1;i<9;i++) fact[i]=fact[i-1] * i ;
}
int try_to_insert(int s)
{
    int code =0; /*把st[s]映射到整数code*/
    for (int i=0 ; i<9 ; i++)
    {   int cnt=0;
        for (int j=i+1;j<9;j++) if (st[s][j] < st[s][i]) cnt++;
        code += fact[8-i] * cnt;
    }
    if (vis[code]) return 0;
    return vis[code]=1;
}


        尽管原理巧妙,时间效率也非常高,但编码解码法的适用范围并不大。如果隐式图的总节点数非常大,编码也将会很大,数组还是开不下。

 

        第二种方法是适用哈希技术。简单地说,就是把节点变成整数,但不必一一对应,换句话说,只要设计一个所谓的哈希函数h(x),然后将任意节点x映射到某个给定范围[0,M-1]的整数即可。其中M是程序员根据可用内存的大小自选的。在理想情况下,只需开一个大小为M的数组就能完成判重,但此时往往有不同节点的哈希值相同,因此需要把哈希值相同的状态组织成链表,代码如下:

const int MAXHASHSIZE = 1000007;
int head[MAXHASHSIZE],next[MAXHASHSIZE];

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位数*/
    return v &MAXHASHSIZE;  /**确保hash函数值是不超过hash表的大小的非负整数*/
}
int try_to_insert(int s)
{
    int h = hash(st[s]);
    int u = head[h];
    while(u)
    {
        if ( memcmp(st[u],st[s],sizeof (st[s]))==0 ) return 0; /**找到了,插入失败**/
        u=next[u];                                            /**那就顺着链表再找下一个**/
    }
    next[s] = head[h];
    head[h] = s;
    return 1;
}


        哈希表的执行效率很高,适用范围也很广。除了BFS中的结点判重外,你还可以把它用到其他需要快速查找的地方。不过需要注意的是:在哈希表中,对效率起到关键作用的是哈希函数。如果哈希函数选取得当,几乎不会有结点的哈希值相同,且此时链表查找的速度也较快。但如果冲突严重,整个哈希表会退化成少数几条长长的链表,查找速度将非常缓慢。有趣的是,前面的编码函数可以看做是一个完美的哈希函数,不需要解决冲突。不过,如果你事先并不知道它是完美的,也就不敢像前面一样只开一个vis数组。哈希技术还有很多值得探讨的地方。

        第三种方法是使用STL中的集合。如果你用过STL的栈和队列,就可以理解下面的定义:set<State> vis 。它声明了一个类型为 state 的集合vis 。这样,只需用 if(vis.count(s)) 来判断 s 是否在集合vis中,并用vis.insert(s)加入集合,用vis.remove(s)从集合中移除s。但问题在于,并不是所有类型的State都可以作为set中的元素类型。STL要求set的元素类型必须定义"<"运算符,如int,string,但C语言原生的数组(包括字符数组)却不行。下面是一种使用int的方法:

#include <set>
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;
}

        但在很多其他场合中,数组是没有办法简单地转化成整数的。只能声明一个结构体,并重载"括号运算"来比较两个状态。这种实现方法,只能用两个整数读出两个状态在状态数组st中的下标,在比较时直接使用memcpy来比较整个内存块。

        这种实现方法明显比刚才的要慢很多。因为调用memcmp直接比较两个整数要慢得多。事实上,在刚才的三种实现中,使用STL集合的代码最简单,但时间效率也最低。建议在做题时,仅仅把STL作为“跳板”——先写一个STL版的程序,确保主算法正确,然后把set替换成自己写的哈希表。

        研读了这么多的方法,虽然脑子还有点乱,但是感觉越来越有趣了!一定要好好研究!

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值