启发式搜索浅谈,解决八数码问题
博客迁移至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; }