启发式搜索浅谈,解决八数码问题

博客迁移至www.amoa400.com

 

相信很多人都接触过九宫格问题,也就是八数码问题。问题描述如下:在3×3的棋盘,摆有八个棋子,每个棋子上标有1至8的某一数字,不同棋子上标的数字不相同。棋盘上还有一个空格,与空格相邻的棋子可以移到空格中。要求解决的问题是:给出一个初始状态和一个目标状态,找出一种从初始转变成目标状态的移动棋子步数最少的移动步骤。



其实在很早之前我就做过这道题了,当时我用的是双向广搜,后来又一知半解的模仿了一个启发式搜索(A*)。昨天在图书馆看书的时候,翻阅了一下人工智能的书籍,又发现了这个经典的八数码问题。于是便看了下去,渐渐的明白了启发式搜索的真正含义,才知道自己几年前模仿的那个代码是什么意思。
在这篇文章里,我把昨天的所学东西稍稍的记录一下,顺便分享给大家,如果有什么错误,欢迎各位指出。


平时,我们所使用的搜索算法大多数都是广搜(BFS)和深搜(DFS),其实他们都是盲目搜索,相对来说搜索的状态空间比较大,效率比较低。而启发式搜索则相对的智能一些,它能对所有当前待扩展状态进行评估,选出一个最好的状态、最容易出解的状态进行搜索。这样,我们就可以避免扩展大量的无效状态,从而提高搜索效率。

在对状态进行评估的时候,我们会使用到一个这样的等式f(n)=g(n)+h(n)。那这个等式是什么含义呢?其中g(n)代表到达代价,即从初始状态扩展到状态n的代价值。h(n)代表状态n的估计解代价,即从状态n扩展到目标状态的代价估计值。所以,f(n)代表估计整体代价,即一个搜索路径上经过状态n且成功出解的估计代价值。

于是,在启发式搜索算法中,我们只要对每一个状态,求出其f(n),每次取f(n)最小的状态进行扩展即可。那现在还有一个问题,就是如何确定g(n)和h(n)到底是什么函数?否则f(n)也无法求出。其实g(n)相对来说比较好确定,因为在到达状态n之后,必定有一条从初始状态到n的搜索路径,于是我们可以从这条路径上找出到达代价g(n),一般的我们就取路径长度即可。估计出解代价h(n)比较难确定,因为之后的状态我们都没有搜索,于是我们只能估计,这也是为什么我把h(n)命名为估计出解代价而不是出解代价的原因。这里有一个更专业的术语,叫做估价函数,是h(n)更准确的定义。我们正是要通过一定的算法进行估价,确定h(n),从而确定f(n),从而找出最好的状态。在不同的情况下,h(n)的定义都不一样,下面我以八数码问题为例,介绍一下八数码问题里的h(n)。

八数码问题里,把空格看成0,并把他们平铺成一个序列,比如1 2 3 4 5 6 0 8 7。于是我们这样定义h(n),定义其为和目标状态错位的格子数,在样例里,我们发现有0 8 7都错位了,于是h(n)=3。但这样也产生一个问题,如果当前状态是 1 2 3 4 5 6 0 7 8的话,其h(n)也是3,但明显后者比前者更好,更容易出解。于是,我们重新定义h(n)为:疏略障碍的情况下,每个数回到自己位置所需要的步数。于是,前者的h(n)=2+1+2=5,后者的h(n)=2+1+1=4,后者更好。

通过对八数码问题中h(n)的求解,不知道各位对估价函数有没有进一步的认识呢?启发式搜索的效率很大程度上取决于估价函数,所以我们在使用启发式搜索的时候一定要找一个好的估价函数。于是,一个有了好的估价函数的启发式搜索,就能很智能的搜索出想要的答案,不妨去试试。
 


额外补充:有这样一个等式f*(n)=g*(n)+h*(n),其中g*(n)代表确定到达代价,h*(n)代表确定出解代价,f*(n)代表确定整体代价,每一项都是固定,也就是说一旦状态确定,这三项都是固定的常量。我们从初始状态到达n,最优的情况下会消耗f*(n)的代价。于是,有这样一个定理,如果我们设计的估计整体代价f(n)<确定整体代价h*(n),那么到达目标状态后,搜索路径一定是最短的。(具体的证明略)于是,在八数码问题中,如果要求最短的路径,那么f(n)必须要小于f*(n),也就是说h(n)要小于h*(n),实际上我们上面定义的h(n)显然满足此条件,所以如果用上面的h(n)作为估价函数就会求出一个最短的路径。但有些时候,我们只要求一个可行解即可,并不要求最短,例如在八数码问题中,我们可以适当把h(n)的权重加大,乘以一个常数,这样就会面临更少的状态,加大搜索效率,具体的可以见后面的代码。

八数码提交地址:POJ1077 HDOJ1043
我的启发式搜索八数码源码:

#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
#include <vector>
#include <queue>
#include <iostream>
using namespace std;

struct node {
       int id,f,g,h;
};

char c[10];
int a[10],b[10],jie[10];
int tot,start_id;
int v[400000];
int pre[400000],w[400000];
int row[9]={3,1,1,1,2,2,2,3,3},
    col[9]={3,1,2,3,1,2,3,1,2};
node heap[400000];

// 康托展开 
inline int cantor() {
    int ans = 0;
    bool v[10] = {0};
    for ( int i=1; i<=9; i++ ) {
        int rank = 0;
        for ( int j=0; j<a[i]; j++ )
        if ( !v[j] ) rank++;
        ans += rank*jie[9-i];
        v[a[i]] = true;
    }
    return ans;
}

// 康托展开 
void un_cantor( int d ) {
     bool v[10] = {0};
     for ( int i=1; i<=9; i++ ) {
         int t = d/jie[9-i];
         d %= jie[9-i];
         int rank = 0;
         for ( int j=0; j<=8; j++ )
         if ( !v[j] ) {
              
             if ( rank==t ) {
                  b[i] = j;
                  v[j] = true;
                  break;
             }
             rank++;
         }
     }
}

// 插入新元素
void insert_heap( node t ) {
     tot++;
     heap[tot] = t;
     int i = tot;
     while ( i>1 ) {
           if ( heap[i].f<heap[i/2].f ) {
                swap( heap[i], heap[i/2] );
                i = i/2;
           } else break;
     }
} 

// 调整堆 
void update_heap() {
     int i = 1;
     while ( i<tot ) {
           int cnt_i = i;
           if ( i*2<=tot && heap[i*2].f<heap[cnt_i].f ) cnt_i = i*2;
           if ( i*2+1<=tot && heap[i*2+1].f<heap[cnt_i].f ) cnt_i = i*2+1;
           if ( i==cnt_i ) break;
           swap( heap[i], heap[cnt_i] );
           i = cnt_i;
     }
}

// 是否有解 
bool have_solution() {
     int cnt = 0;
     for( int i=1; i<=9; i++ )
     for( int j=1; j<i; j++ )
     if( a[j]<a[i] && a[j] ) cnt++;
     if( cnt&1 ) return false;
     return true;
}

// 初始化 
void init() {
     // 初始化数组
     memset( v, 0, sizeof(v) ); 
     // 读入数据 
     for ( int i=2; i<=9; i++ ) scanf( " %c", &c[i] );
     scanf( "\n" );
     // 处理数据 
     for ( int i=1; i<=9; i++ ) {
         if ( c[i]=='x' ) a[i] = 0;
         else a[i] = c[i]-'0';
         b[i] = i;
     }
     b[9] = 0;
     // 计算阶乘
     jie[0] = 1;
     for ( int i=1; i<=9; i++ ) jie[i] = jie[i-1]*i;
     // 建堆     
     tot = 1;
     heap[1].id = cantor();
     heap[1].f = 0;
     heap[1].g = 0;
     heap[1].h = 0;
     start_id = heap[1].id;
     v[heap[1].id] = true;
}

// 输出答案 
void output( int id ) {
     if ( id==start_id ) return; 
     output( pre[id] );
     if ( w[id]==1 ) cout << "u";
     if ( w[id]==2 ) cout << "d";
     if ( w[id]==3 ) cout << "l";
     if ( w[id]==4 ) cout << "r";
}

// 操作算子 
bool work( node cnt, int ww ) {
     int id = cantor();
     if ( v[id] ) return id==46233;
     v[id] = true;
     pre[id] = cnt.id;
     w[id] = ww;
     node t;
     t.id = id;
     t.g = cnt.g+1;
     t.h = 0;
     for ( int i=1; i<=3; i++ ) 
     if ( a[i]!=0 ) t.h += abs( row[a[i]]-1 )+abs( col[a[i]]-((i-1)%3+1) );
     for ( int i=4; i<=6; i++ ) 
     if ( a[i]!=0 ) t.h += abs( row[a[i]]-2 )+abs( col[a[i]]-((i-1)%3+1) ); 
     for ( int i=7; i<=9; i++ ) 
     if ( a[i]!=0 ) t.h += abs( row[a[i]]-3 )+abs( col[a[i]]-((i-1)%3+1) ); 
     t.f = t.g+6*t.h;
     // 根据可采纳性,如果想求最短路径,应该把上面的语句改成,t.f = t.g+t.h、 
     insert_heap( t );
     return t.id==46233;
}

// 启发式搜索 
void bfs() {
     int sum = 0;
     bool find = false;
     while ( tot!=0 ) {
           sum++;
           // 取出堆顶元素 
           node cnt = heap[1];
           swap( heap[1], heap[tot] );
           tot--;
           update_heap();
           // 对当前元素进行康托展开
           un_cantor( cnt.id );
           // 寻找零点 
           int cnt_zero;
           for ( int i=1; i<=9; i++ )
           if ( b[i]==0 ) { cnt_zero = i; break; }
           // 向上移动
           if ( cnt_zero>3 ) {
              for ( int i=1; i<=9; i++ ) a[i] = b[i];
              swap( a[cnt_zero], a[cnt_zero-3] );
              if ( work( cnt, 1 ) ) find = true;
           }
           // 向下移动
           if ( cnt_zero<7 ) {
              for ( int i=1; i<=9; i++ ) a[i] = b[i];
              swap( a[cnt_zero], a[cnt_zero+3] );
              if ( work( cnt, 2 ) ) find = true;
           }
           // 向左移动
           if ( cnt_zero%3!=1 ) {
              for ( int i=1; i<=9; i++ ) a[i] = b[i];
              swap( a[cnt_zero], a[cnt_zero-1] );
              if ( work( cnt, 3 ) ) find = true;
           }
           // 向右移动
           if ( cnt_zero%3!=0 ) {
              for ( int i=1; i<=9; i++ ) a[i] = b[i];
              swap( a[cnt_zero], a[cnt_zero+1] );
              if ( work( cnt, 4 ) ) find = true;
           }
           // 找到答案
           if ( find ) break;
     }
     output( 46233 );
     cout << endl;
}

int main() {
    //freopen("h.in","r",stdin);
    //freopen("h.out","w",stdout);
    
    while ( scanf( "%c", &c[1] )!=EOF ) {
          init();
          if ( have_solution() ) bfs();
          else cout << "unsolvable" << endl;
    }
    
    return 0;
}

  

 

posted @ 2011-10-20 19:44  amoa400  阅读(12248)  评论(1编辑  收藏  举报