编号为1-8的8个正方形滑块被摆成三行三列(有一个格子留空),如图所示:
每次可以把与空格相邻的滑块移到空格中,而它原来的位置就成了新的空格。给定初始局面和目标局面,你的任务是计算出最少的移动步数,如果无法达到目标局面,输出-1。
2 | 6 | 4 |
1 | 3 | 7 |
5 | 8 |
8 | 1 | 5 |
7 | 3 | 6 |
4 | 2 |
样例输入:2 6 4 1 3 7 0 5 8
8 1 5 7 3 6 4 0 2
样例输出:31
不难把八数码问题归结为图上的最短路问题。其中每个状态就是9个格子中的滑块编号(从上到下,从左到右地把它们放到一个包含9个元素的数组中)。具体程序如下:
#include <iostream> #include <cstring> using namespace std; typedef int State[9]; /**定义状态类型*/ const int MAXSTATE=1000000; State st[MAXSTATE],goal; /**状态数组,所有状态都保存在这里**/ int dist[MAXSTATE]; /**距离数组*/ int fa[MAXSTATE]; /**父亲编号数组,用于打印*/ int vis[36288],fact[9]; void init_lookup_table(); int try_to_insert(int s); const int dx[]={-1,1,0,0}; const int dy[]={0,0,-1,1}; /**BFS返回目标状态 在st数组下标**/ int bfs() { init_lookup_table(); /*初始化查找表*/ int front=1,rear=2; while (front < rear) { State& s =st[front]; if ( memcmp( goal, s,sizeof (s) )==0 ) return front; int z; for (z=0; z<9 ; z++) if (!s[z]) break; int x=z/3,y=z%3; for (int d=0;d<4;d++) { int newx=x+dx[d]; int newy=y+dy[d]; int newz=newx*3+newy; if (newx>=0 && newx<3 && newy>=0 && newy<3 ) { State& t = st[rear]; memcpy(&t,&s,sizeof(s)); t[newz]=s[z]; t[z]=s[newz]; dist[rear] = dist[front]+1 ; if (try_to_insert(rear)) rear++; } } front++; } return 0; }
注意,此处用到了 cstring 的 memcmp 和 memcpy 来完成整块内存的比较和复制,比用循环比较和循环赋值要快。主程序很容易实现:
int main() { for (int i=0;i<9;i++) cin>>st[1][i]; /*起始状态*/ for (int i=0;i<9;i++) cin>>goal[i]; /*目标状态*/ int ans=bfs(); /*返回目标状态的下标*/ if (ans>0) cout<<dist[ans]<<endl; else cout<<"-1"<<endl; return 0; }
注意,应该在调用bfs函数之前设置好st[1]和goal。上面的代码几乎是完整的,唯一没有涉及的是init_lookup_taible()和try_to_insert(rear)的实现。为什么会有这个东西呢?还记得bfs中的vis数组吗?我们用它来进行bfs中的判重。这里的查找表和它的功能类似,也是避免我们将同一个节点结构访问多次。树的bfs不需要判重,因为根本不会重复,但对于图来说,如果不判重,时间和空间都将产生极大的浪费。
如何判重呢?难道要声明一个9维数组vis,然后 if(vis[s[0]][s[1]][s[2]][s[3]]...[s[8]]) ? 无论程序好不好看,9维数组的每维都要包含9个元素,一共有9^9=387420489项,太多了,数组开不下。实际的节点数并没有这么多。(0-8的排列总共只有9!=362880个),为什么9维数组开不下呢?原因在于,数组中有很多项都没有用到,但却占据了空间。
下面讨论3种常见的方法来解决这个问题,同时也将其用到八数码问题中。
第一种方法是:把排列变成整数,然后只开一个一维数组,也就是说,我们设计一套排列的编码和解码函数,把0-8的全排列和0-362879的整数一一对应起来。
int vis[36288],fact[9]; void init_lookup_table() { fact[0]=1; for (int i=1;i<9;i++) fact[i]=fact[i-1] * i ; } int try_to_insert(int s) { int code =0; /*把st[s]映射到整数code*/ for (int i=0 ; i<9 ; i++) { int cnt=0; for (int j=i+1;j<9;j++) if (st[s][j] < st[s][i]) cnt++; code += fact[8-i] * cnt; } if (vis[code]) return 0; return vis[code]=1; }
尽管原理巧妙,时间效率也非常高,但编码解码法的适用范围并不大。如果隐式图的总节点数非常大,编码也将会很大,数组还是开不下。
第二种方法是适用哈希技术。简单地说,就是把节点变成整数,但不必一一对应,换句话说,只要设计一个所谓的哈希函数h(x),然后将任意节点x映射到某个给定范围[0,M-1]的整数即可。其中M是程序员根据可用内存的大小自选的。在理想情况下,只需开一个大小为M的数组就能完成判重,但此时往往有不同节点的哈希值相同,因此需要把哈希值相同的状态组织成链表,代码如下:
const int MAXHASHSIZE = 1000007; int head[MAXHASHSIZE],next[MAXHASHSIZE]; void init_lookup_table() { memset(head,0,sizeof(head) ); } int hash (State &s) { int v = 0; for (int i = 0; i < 9; i++) v= v * 10 + s[i]; /**可以任意计算,例如,把九个数字组合成9位数*/ return v &MAXHASHSIZE; /**确保hash函数值是不超过hash表的大小的非负整数*/ } int try_to_insert(int s) { int h = hash(st[s]); int u = head[h]; while(u) { if ( memcmp(st[u],st[s],sizeof (st[s]))==0 ) return 0; /**找到了,插入失败**/ u=next[u]; /**那就顺着链表再找下一个**/ } next[s] = head[h]; head[h] = s; return 1; }
哈希表的执行效率很高,适用范围也很广。除了BFS中的结点判重外,你还可以把它用到其他需要快速查找的地方。不过需要注意的是:在哈希表中,对效率起到关键作用的是哈希函数。如果哈希函数选取得当,几乎不会有结点的哈希值相同,且此时链表查找的速度也较快。但如果冲突严重,整个哈希表会退化成少数几条长长的链表,查找速度将非常缓慢。有趣的是,前面的编码函数可以看做是一个完美的哈希函数,不需要解决冲突。不过,如果你事先并不知道它是完美的,也就不敢像前面一样只开一个vis数组。哈希技术还有很多值得探讨的地方。
第三种方法是使用STL中的集合。如果你用过STL的栈和队列,就可以理解下面的定义:set<State> vis 。它声明了一个类型为 state 的集合vis 。这样,只需用 if(vis.count(s)) 来判断 s 是否在集合vis中,并用vis.insert(s)加入集合,用vis.remove(s)从集合中移除s。但问题在于,并不是所有类型的State都可以作为set中的元素类型。STL要求set的元素类型必须定义"<"运算符,如int,string,但C语言原生的数组(包括字符数组)却不行。下面是一种使用int的方法:
#include <set> set<int> vis; void init_lookup_table(){ vis.clear();} int try_to_insert(int s) { int v=0; for(int i = 0 ; i < 9 ; i++ ) v = v * 10 + st[s][i]; if (vis.count(v)) return 0; vis.insert(v); return 1; }
但在很多其他场合中,数组是没有办法简单地转化成整数的。只能声明一个结构体,并重载"括号运算"来比较两个状态。这种实现方法,只能用两个整数读出两个状态在状态数组st中的下标,在比较时直接使用memcpy来比较整个内存块。
这种实现方法明显比刚才的要慢很多。因为调用memcmp直接比较两个整数要慢得多。事实上,在刚才的三种实现中,使用STL集合的代码最简单,但时间效率也最低。建议在做题时,仅仅把STL作为“跳板”——先写一个STL版的程序,确保主算法正确,然后把set替换成自己写的哈希表。
研读了这么多的方法,虽然脑子还有点乱,但是感觉越来越有趣了!一定要好好研究!