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

浙公网安备 33010602011771号