八数码问题的 A* 算法解答

 搞了一个通宵,终于搞定了!偶也!
Sicily 的 1379 那题,就是求给定的初始状态到目标状态,转换至少需要多少步。
例如状态

1 2 3

4 5 0

7 8 6

到达目标状态

1 2 3

4 5 6

7 8 0

只需一步(将 0 下移一格)即可。


在周赛的时候用广度搜索生成所有状态,耗时 0.6S 过了。但是这样不是聪明的办法(不过原理简单,实现也简单)。解这个问题的经典方法是使用 A* 算法进行启发式搜索,一般的人工智能原理的书都会介绍,在此不多说了。
看了下百度之星 2005 的比赛里 ACRush 的 代码,只看懂一小部分 orz... (注释都没有的!),不过也收获不少,hmhm。。也看了 Ray 的这篇 报告,受益匪浅。
下面是对该报告的一些 补充(最好自己先看看那篇报告和人工智能的书关于启发式搜索的那章):
此题关键之一是使用曼哈顿距离来作启发函数。两个状态中对应的数字的的横向距离和纵向距离之和,定义为该数字的距离,而除了 0 的各个数字的距离之和定义为两个状态的距离,如上面提及的两个状态的距离为 1(只有数字 6 位置不同,而横向距离为 0, 纵向距离为 1)。
不难证明两个状态之间转换所需的步数不少于它们的距离。这样就满足了一般的 A* 算法的要求。采用评价函数 f (state) = g (state) + distance (state, goalState),越小就越有希望到达目标状态,其中 g (state) 为从初始状态到达该状态的步数(随着搜索的进行,会发现到达该状态的更短的转换步骤,因此 g (state) 会逐渐减少至最小值 g* (state))。
从开始状态开始搜索,按照将 0 上下左右移动的四种方式生成新的状态(当然要考虑可行性,如 0 已在最右边就不能再右移了)。在新的状态中找出评价函数值 f (state) 最小的状态来继续搜索(我们需要维护一个最小优先级队列 -- OPEN 表 -- 来找出 f 值最小的状态),直至搜索到目标状态。如果两个状态是可以互相达到的,那么可证 A* 算法一定可以找到最优解。
不难证明曼哈顿距离满足单调性,即在状态转换 state 1 -> 2 -> 3 中,distance (state1, state3) < distance (state2, state3) + cost (state1, state2)。
在满足这样的条件下,我们的搜索将满足以下两个有趣且有用的特性:
当要选择一个状态生成新状态时,它的 g (state) = g* (state),即找到到达该状态的最少步数(如果你记录转换的上下左右记录的话,也将得到最少步数的转换步骤)。
选择的状态的评价函数值 f 是逐渐递增的。
如果不满足这个条件,那么在选择一个状态生成新状态时,新状态可能已经被生成过而且还被选择过,而达到这个新状态的步数可能会更少,即发现了更少的转换步骤,这样就有重复的工作了。而在满足条件下,一旦被选择则步数就已经是最少的了,就像最短路的 Dijkstra 算法那样。
而为了查询一个状态是否选择过,我们可以用一个 CLOSED 表 -- 来记录状态,在 C ++ 的 STL 中,我们可以选择 map 这种数据结构(一种平衡的二叉查找树),将一个状态对应的数字(如目标状态表示为 123456780)映射到对应的耗费 g 值。当然我们可以使用 hash_map 或者 trie 这两种数据结构,,或者自己实现一个哈希表,甚至使用“康托展开”直接映射到一个数组元素(后面几种数据结构占用内存较多)。这也是关键之一,我用自己实现的 trie 来做速度加快很多。另外预处理和使用一个变量 pos 来记录数字 0 的位置也很关键。
以下是我的代码:


//  八数码问题 A* 算法
//  by [email protected]
//  2007-12-10

#include 
< cstdio >
#include 
< queue >
#include 
< map >
using   namespace  std;

const   int  ten [ 9 =  { 100000000 10000000 1000000 100000 10000 1000 100 10 1 };
int  dist [ 9 ][ 9 ], diff [ 9 ][ 9 ];
//  dist [i][j] 为位置 i, j 的曼哈顿距离, diff [i][j] 为交换位置 i, j 使状态数改变的量
int  goalPos [ 9 =  { 8 0 1 2 3 4 5 6 7 };
//  goalPos [i] 为目标状态中 i 的位置
int  goalNum  =   123456780 ;     //  目标状态的状态数
int  goalInv  =   0 ;         //  目标状态忽略 0 后的逆序数
int  state [ 9 ];             //  当前状态

struct  STATE{
    
int  num, pos, g, h;     //  状态数,0 的位置,达到此状态的耗费,到达目标状态的启发函数值
    STATE ( int  num,  int  pos,  int  g,  int  h):num(num), pos(pos), g(g), h(h){}
    
bool   operator   <  ( const  STATE &  other)  const      //  状态的评价函数等于耗费加上启发函数值
    {
        
if  (g  +  h  ==  other.g  +  other.h)  return  h  >  other.h;     //  由于查询较少,此句帮助不大,可删掉
         return  g  +  h  >  other.g  +  other.h;
    }
};

void  preprocess ()         //  预处理
{
    
for  ( int  i  =   0 ; i  <   9 ; i  ++ ){
        dist [i][i] 
=   0 ;
        
for  ( int  j  =   0 ; j  <  i; j  ++ ){
            dist [i][j] 
=  dist [j][i]  =  abs (i  /   3   -  j  /   3 +  abs (i  %   3   -  j  %   3 );
            diff [i][j] 
=  diff [j][i]  =  abs (ten [i]  -  ten [j]);
        }
    }
}

bool  noAns ( int  pos)         //  检查开始状态忽略 0 后的逆序数,如果和目标状态的逆序数奇偶性不一致,则没有解
{
    
int  inv  =   0 ;
    
for  ( int  i  =   0 ; i  <   9 ; i  ++ ){
        
for  ( int  j  =   0 ; j  <  i; j  ++ ){
            
if  (state [j]  >  state [i]) inv  ++ ;
        }
    }
    
return  (inv  -  pos  -  goalInv)  %   2   !=   0 ;
}

int  heu ( int  pos)         //  计算启发函数值
{
    
int  h  =   0 ;
    
for  ( int  i  =   0 ; i  <   9 ; i  ++ ){
        
int  j  =  goalPos [state [i]];
        h 
+=  dist [i][j];
    }
    
return  h  -  dist [pos][goalPos [ 0 ]];
}

int  astar ()
{
    
// int cnt = 0;
     int  num  =   0 , pos  =   0 ;
    
for  ( int  i  =   0 ; i  <   9 ; i  ++ ){
        scanf (
" %d " & state [i]);
    }
    
for  ( int  i  =   0 ; i  <   9 ; i  ++ ){        
        num 
=  num  *   10   +  state [i];
    }
    
for  ( int  i  =   0 ; state [i]; i  ++ ){
        pos 
++ ;
    }
    
if  (noAns (pos))  return   - 1 ;     //  检查是否无解
    map  < int int >  ng;         //  CLOSED 表,已扩展的结点(状态数 -> 到达该结点的最少耗费)
    priority_queue  < STATE >  q;     //  OPEN 表,待扩展的结点,但是仍然会存在已扩展的结点的记录
    STATE start(num, pos,  1 , heu (pos));     //  因为 map 对不存在的 key 返回 0,故初始状态的耗费应设为 1
    q.push (start);
    
while  (q.size ()){
        
// cnt ++;
        STATE top  =  q.top ();     //  考察 OPEN 表中评价函数值最小的结点
        q.pop ();
        
int  pos  =  top.pos, num  =  top.num, g  =  top.g, h  =  top.h;
        
if  (num  ==  goalNum){
            
// printf ("%d ", cnt);
             return  g  -   1 ;     //  找到最优解,注意要减去 1
        }
        
if  (ng [num])  continue ;     //  已经扩展过此结点则忽略
        ng [num]  =  g;         //  结点加入 CLOSED 表
        
//  扩展此结点
         if  (pos  >   2 ){         //  move 0 up
             int  p  =  pos  -   3 ;
            
int  i  =  num  /  ten [p]  %   10 , n  =  num  -  i  *  diff [pos][p];
            
int  h2  =  h  -  dist [p][goalPos [i]]  +  dist [pos][goalPos [i]];
            
if  ( ! ng [n]) q.push (STATE (n, p, g  +   1 , h2));
        }
        
if  (pos  <   6 ){         //  move 0 down
             int  p  =  pos  +   3 ;
            
int  i  =  num  /  ten [p]  %   10 , n  =  num  +  i  *  diff [pos][p];
            
int  h2  =  h  -  dist [p][goalPos [i]]  +  dist [pos][goalPos [i]];
            
if  ( ! ng [n]) q.push (STATE (n, p, g  +   1 , h2));
        }
        
if  (pos  %   3 ){         //  move 0 left
             int  p  =  pos  -   1 ;
            
int  i  =  num  /  ten [p]  %   10 , n  =  num  -  i  *  diff [pos][p];
            
int  h2  =  h  -  dist [p][goalPos [i]]  +  dist [pos][goalPos [i]];
            
if  ( ! ng [n]) q.push (STATE (n, p, g  +   1 , h2));
        }
        
if  (pos  %   3   !=   2 ){     //  move 0 right
             int  p  =  pos  +   1 ;
            
int  i  =  num  /  ten [p]  %   10 , n  =  num  +  i  *  diff [pos][p];
            
int  h2  =  h  -  dist [p][goalPos [i]]  +  dist [pos][goalPos [i]];
            
if  ( ! ng [n]) q.push (STATE (n, p, g  +   1 , h2));
        }
    }
    
return   0 ;
}

int  main ()
{
    
// freopen ("input.txt", "r", stdin);
    preprocess ();
    
int  t;
    scanf (
" %d " & t);
    
while  (t  -- ){
        printf (
" %d  " , astar ());
    }
    
return   0 ;
}

/*
Input
第一行是一个整数n,表示一共有多少组测试数据。
下面n行每行9个字符,用空格隔开,代表一个初始状态。
目标状态是 1 2 3 4 5 6 7 8 0。

Output
最小操作步数,无解时输出-1。

sample input
4
1 2 3 4 5 0 7 8 6
1 2 3 4 0 5 6 7 8
8 6 7 2 5 4 3 0 1
6 4 7 8 5 0 3 2 1

sample output
1
14
31
31

注意在 Release 模式下运行此程序,否则可能会很慢!
注意优先队列 priority_queue 是最大堆,因此在定义 STATE 结构的比较函数时要让 f 值较小的元素较大,
而当 f 相等的时候,启发函数 h 较小的状态应该优先考虑,故让该状态较大。
优先队列中仍然会存在一些 CLOSED 表中的状态,因为在生成新的状态时没有查找删除该新状态是否生成过。
*/

你可能感兴趣的:(数据结构,算法,struct,扩展,output,distance)