从“八数码”走进搜索

2009年9月25日 04:17

八数码是经典的搜索问题这已经是众所周知的 。

今天一天就做了这一个问题,感觉挺有收获的,于是想写点心得。

先来看下题目:

http://acm.pku.edu.cn/JudgeOnline/problem?id=1077

最先想到的是直接BFS,这也是最简单的想法。但一开始就遇到了一个公共的问题(为什么说是公共的问题呢。因为不管你用什么方式的搜索都会遇到的)——如何判重。最简单的就是直接将棋盘转化成一个9位的十进制数。但这用什么数据结构呢?数组,10^9!这个内存是不允许的;用set,这个时间是不允许的。后面听勇哥说,全排是有完美的hash函数的,于是在网上找到这个——http://bbs3.chinaunix.net/viewthread.php?tid=1283459,这上面解释得很清楚。于是按照上面的hash方法写了个单向的BFS,附:

 

#include <cstdio>
#include <cstring>

struct node {
    int pre; //上一个节点和如何变换的信息
	int p; // x即9的下标
    char ch[10];//用于记录棋盘信息
}st[400000];
//队列数组 ,9!= 362880

bool vis[400000];//判重数组

int perm[] = {1,1,2,6,24,120,720,5040,40320};//n!
int d[] = { -1 , -3, 1, 3};//四个方向的下标变换
bool move[][4] = {0,0,1,1, 1,0,1,1, 1,0,0,1, 0,1,1,1, 1,1,1,1, 1,1,0,1, 0,1,1,0, 1,1,1,0, 1,1,0,0};
//各个位置的可行变换

int hash(char x[])//用逆序数和变进制进行hash
{
    int h = 0;
    for(int i = 1;i<9;i++){
        int count = 0;
        for(int j=0;j<i;j++)
            if(x[j] > x[i])count ++;
        h += count * perm[i];
    }
    return h;
}

bool end (char x[])//判断是否到目标状态
{
    for(char i = 0;i<9;i++){
        if(x[i] != i + '1') return false;
    }
    return true;
}

int BFS(char *num,int p)
{
    int left ,right;
    left = right = 1;
    memset(vis,0,sizeof(vis));
    strcpy(st[right].ch,num);
    st[right].p = p;
    st[right ++].pre = 0;
    vis[hash(st[left].ch)] = 1;
    while(left < right){
        int p1 ,p2 ;
        p1 = st[left].p;
        for(int i = 0 ;i<4;i++)if(move[p1][i]){
            p2 = st[right].p = p1 + d[i];
            strcpy(st[right].ch,st[left].ch);
            char tp = st[right].ch[p2] ;
            st[right].ch[p2] = st[right].ch[p1] ;
            st[right].ch[p1] = tp ;
            st[right].pre = left << 2 | i;//将上一个节点的下标和变换规则压进一个数字
            if(end(st[right].ch)) return st[right].pre;
            int key = hash(st[right].ch);
            if( ! vis[key] ) { vis[key] = 1 ; right ++;}
        }
        left ++;
    }
    return -1;
}


int main()
{
    char num[10] ,der[]="lurd",step[100];
    int p ;
    for(int i =0 ;i<9;){
        num[i] = getchar();
        if(num[i] == 'x') { num[i] = '9' ; p = i; }//将x转化成'9',这样便于hash,和判结束
        if(num[i]<='9' && num[i]>='1') i++;
    }
    num[9] = '\0';
    int i = 0;
    if(!end(num)){
        int k = BFS(num,p);
        if( k == -1)  printf("unsolvable");
        else {
            while(k){
                step[ ++ i] = der[k&3];
                k = st[k>>2].pre;
            }
            for(;i>0;i--) putchar(step[i]);
            puts("");
        }
    }
    return 0;
}

 

在知道初状态(输入)和末状态(12345678x)的情况下还可以用双向BFS,无论从空间还是从时间的复杂度来说,双向BFS都是比单向BFS都优。单向的BFS的时间和空间都是k^n级的,k是一个状态分支的个数,这里理论是 k = 4,实际上2到3之间,n是迭代的层数,双向BFS的时间和空间的复杂度都是k^(n/2)级的。就是双向BFS的代码复杂度要高一些。在poj上单向BFS是4104K 266MS 2067B,双向BFS是1280K 16MS 2589B

附:双向BFS代码

 
 

#include <cstdio>
#include <cstring>
#define M 400000
struct node {
    int pre,p;
	int key; //这个节点状态对应的hash值
    char ch[10];
}st[2][M];

bool vis[2][M];

int perm[] = {1,1,2,6,24,120,720,5040,40320};
int d[] = { -1 , -3, 1, 3};
bool move[][4] = {0,0,1,1, 1,0,1,1, 1,0,0,1, 0,1,1,1, 1,1,1,1, 1,1,0,1, 0,1,1,0, 1,1,1,0, 1,1,0,0};

int hash(char x[])
{
    int h = 0;
    for(int i = 1;i<9;i++){
        int count = 0;
        for(int j=0;j<i;j++)
            if(x[j] > x[i])count ++;
        h += count * perm[i];
    }
    return h;
}

int BFS(char *num,int p)
{
    int l[2] ,r[2] ,key;
    l[0] = l[1] = r[0] = r[1] = 1;
    memset(vis,0,sizeof(vis));

    strcpy(st[0][r[0]].ch,num);
    st[0][r[0]].p = p;
    st[0][r[0]].pre = 0;
    key = hash(st[0][l[0]].ch);
    vis[0][key] = 1;
    st[0][r[0] ++].key = key;

    strcpy(st[1][r[1]].ch,"123456789");
    st[1][r[1]].p = 8;
    st[1][r[1]].pre = 0;
    key = hash(st[1][l[1]].ch);
    vis[1][key] = 1;
    st[1][r[1] ++].key = key;

    while(l[0] < r[0] && l[1]<r[1]){
        int p1 ,p2 ,now = (r[0] - l[0] > r[1] - l[1]);//将节点少的一边进行扩展
        p1 = st[now][l[now]].p;
        for(int i = 0 ;i<4;i++)if(move[p1][i]){
            p2 = st[now][r[now]].p = p1 + d[i];
            strcpy(st[now][r[now]].ch,st[now][l[now]].ch);
            char tp = st[now][r[now]].ch[p2] ;
            st[now][r[now]].ch[p2] = st[now][r[now]].ch[p1] ;
            st[now][r[now]].ch[p1] = tp ;
            st[now][r[now]].pre = l[now] << 2 | i;
            key = hash(st[now][r[now]].ch);
            st[now][r[now]].key = key;
            if( ! vis[now][key] ) { vis[now][key] = 1 ; r[now] ++;}
            if( vis[1-now][key]) return key;//检测当前扩展的状态在另一边是否已出现,
        }
        l[now] ++;
    }
    return -1;
}


int main()
{
    char num[10] ,der[]="lurd",step[100];
    int p ;
    for(int i =0 ;i<9;){
        num[i] = getchar();
        if(num[i] == 'x') { num[i] = '9' ; p = i; }
        if(num[i]<='9' && num[i]>='1') i++;
    }
    num[9] = '\0';
    int i = 0;
    int k = BFS(num,p);
    if( k == -1)  printf("unsolvable\n");
    else {
        int p = 1;
        while(st[0][p].key != k) p++;
        p = st[0][p].pre ;
        while(p){
            step[ ++ i] = der[p&3];
            p = st[0][p>>2].pre;
        }
        for(;i>0;i--) putchar(step[i]);
        p = 1;
        while(st[1][p].key != k) p++;
        p = st[1][p].pre;
        while(p){
            putchar(der[((p & 3) + 2)%4]);
            p = st[1][p>>2].pre;
        }
        puts("");
    }
    return 0;
}

 

这道题目还可以用A*来写,原来看过这方面的书籍,还没用自己动手写过A*,于是尝试着写了写。这里用搜索树的深度做g(s),用但前状态各个字符和目标状态这个字符的哈密尔顿距离之和做f(s),这里做了个比较危险的假设:我把路径压缩在一个long long 中,一个变换只有4种可能,用2个bit就能保存了,大家都知道long long 占64个bit,所以最多可以存32个变化,也就是说,一个long long 只能保存一天短于32的路径(不知道这样描述是否清楚)。我假设,对于任何状态,变到目标状态至多的步数不会超过32步(无解的状态除外),就像3阶魔方被证明20+步一定能被搞定一样,(如果哪个神牛能证明一下就好了……)。

同样这里也贴出代码(就不注释了,如果有没理解的发邮件给我吧,hustsxh@gmail.com):

 

#include <cstdio>
#include <cstring>
#include <cmath>
#include <queue>

struct node {
    long long path;
    char ch[10];
    int g,f,p;
    bool operator <(const node & a)const{
        return f>a.f || f==a.f &&  g > a.g;
    }
};

bool vis[400000];

int perm[] = {1,1,2,6,24,120,720,5040,40320};
int d[] = { -1 , -3, 1, 3};
bool move[][4] = {0,0,1,1, 1,0,1,1, 1,0,0,1, 0,1,1,1, 1,1,1,1, 1,1,0,1, 0,1,1,0, 1,1,1,0, 1,1,0,0};

int hash(char x[])
{
    int h = 0;
    for(int i = 1;i<9;i++){
        int count = 0;
        for(int j=0;j<i;j++)
            if(x[j] > x[i])count ++;
        h += count * perm[i];
    }
    return h;
}

bool end (char x[])
{
    for(char i = 0;i<9;i++){
        if(x[i] != i + '1') return false;
    }
    return true;
}

int h(char x[])
{
    int h = 0;
    for(int i=0;i<9;i++){
        h += abs( '1' + i -x[i]);
    }
    return h;
}

int Astar(char num[],int p,long long & path)
{
    memset(vis,0,sizeof(vis));
    std::priority_queue <node> c;
    node start ;
    strcpy(start.ch,num);
    start.path = 0;
    start.p = p;
    start.g = 0;
    start.f = start.g + h(start.ch);
    vis[hash(start.ch)] = 1;
    c.push(start);
    while(!c.empty()){
        node next, pre = c.top();
        c.pop();
        for(int i = 0;i<4;i++)if(move[pre.p][i]){
            next.p = pre.p + d[i];
            next.g = pre.g + 1;
            strcpy(next.ch,pre.ch);
            next.ch[pre.p] = next.ch[next.p];
            next.ch[next.p] = '9';
            next.path = pre.path<<2 | i;
            next.f = h(next.ch) + next.g ;
            if(end(next.ch)) { path = next.path ; return next.g;}
            int key = hash(next.ch);
            if(!vis[key]) { vis[key] = 1; c.push(next); }
        }
    }
    return -1;
}

int main()
{
    char num[10] ,der[]="lurd",step[100];
    int p ;
    for(int i =0 ;i<9;){
        num[i] = getchar();
        if(num[i] == 'x') { num[i] = '9' ; p = i; }
        if(num[i]<='9' && num[i]>='1') i++;
    }
    num[9] = '\0';
    int i = 0;
    if(!end(num)){
        long long path ;
        int k = Astar(num,p,path);
        if( k==-1) printf("unsolvable\n");
        else {
            for(int i = k-1;i>=0;--i) {
                step[i] = der[path&3];
                path = path >>2;
            }
            step[k] = '\0';
            puts(step);
        }
    }
    return 0;
}

 估计是我写得不够优,在poj的的各个参数是1012K 47MS 2459B

 从八数码中学到了一个精妙、完美的hash,第一次写双向BFS,第一次写A*,而且交了三份代码三份代码都是1Y的,很hanppy,在此留下一笔,作为成长的痕迹……