题目:编号为1~8的8个正方形滑块被摆成3行3列(有一个格子留空)。把一种状态变成另一种状态最少移动多少步,到达不了,就输出-1。
2 | 6 | 4 |
1 | 3 | 7 |
5 | 8 |
8 | 1 | 5 |
7 | 3 | 6 |
4 | 2 |
分析:题目就是一个bfs最短路径问题,关键是如何判断当前状态是寻找过的。作者给了3种方法,我总结一下。
第一种:把0~8的所有可能的排列组合和0~362879(全排列的数目)对应起来,说起来这种方法真是精妙,看你怎么编码,最能激发想象力。作者编码如下:
int vis[362880],fact[9];
void init_lookup_table(){
fact[0]=1;
for(int I=1;i<9;i++)fact[I]=fact[I-1]*I;
}
fact[1]=1*1!;
fact[2]=2*fact[1]=2*1*1;
fact[3]=3*fact[2]=3*2*1;
fact[4]=4*fact[3]=4*3*2*1;
...
fact[8]=8!;
int try_to_insert(int s){
int code=0;
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;
}
这里利用了排列组合每次变换后的每个位置上的值的后面所有值小于它本身的有多少个。假设当前排列是8 7 6 5 4 3 2 1 0。然后第一个后面所有元素小于它有8个,fact[8-0]=8!,8*8!,9!=9*8!,还剩下8!。再看7,fact[8-1]=7!,7*7!,8!=8*7!,还剩下7!。同样6还剩下6!,5还剩下5!,4还剩下4!,3还剩下3!,2还剩下2!。1就是0,还剩下1个。一共就是362879就是最大下标。
每一种排列都是唯一的,因为即使cnt有可能一样,但是fact的值是不一样的。数学真是有趣,这都能推出来,美!
这种排序方法也有缺陷,一是数量大就不适用,二是比较难想。
第二种:
哈希编码,映射,先声明这题没有冲突,是完美的哈希编码。
以 12 8 4 6 23 45为例,共6个可以开长度为6的数组。映射到表中12%6=0;8%6=2;4%6=4;6%6=0;23%6=5;45%6=3;
0 | 1 | 2 | 3 | 4 | 5 |
12 | 6 | 8 | 45 | 4 | 23 |
发现有冲突向后一位,位数放入偏移数组,但是这题没有冲突,作者神仙。 解题思路就是将排列组合的9个数组合起来变成一个数,一共有362880个组合,就有362880个数,开长度为1000003的数组。
代码部分:
const int hashsize = 1000003;
int head[hashsize], nexts[maxstate];
void init_lookup_table() {
memset(head, 0, sizeof(head));
}
int hashs(State& s) {
int v = 0;
for (int i = 0; i < 9; i++)
v = v * 10 + s[i];//将组合变成一个9位数
return v % hashsize;
}
int try_to_insert(int s) {
int h = hashs(st[s]);
int u = head[h];
while (u) {
if (memcmp(st[u], st[s], sizeof(st[s])) == 0)return 0;
u = nexts[u];
}
nexts[s] = head[h];
head[h] = s;
return 1;
}
第三种:
终于到了不用动脑子的方法了。
建立集合用集合判重,具体看代码。
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;
}
最后一种虽然简单,但是效率相对前面两种来说就显得低很多。
最后
附上回归正题,八数码代码:
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <algorithm>
#include<iostream>
using namespace std;
const int maxstate = 1000000;
typedef int State[9];
State st[100000],goal;
int dist[maxstate];//距离数组
//如果需要打印,可在这里加一个“父亲编号”数组 int fa[maxstate]
const int dx[] = { -1,1,0,0 };
const int dy[] = { 0,0,-1,1 };
const int hashsize = 1000003;
int head[hashsize], nexts[maxstate];
void init_lookup_table() {
memset(head, 0, sizeof(head));
}
int hashs(State& s) {
int v = 0;
for (int i = 0; i < 9; i++)
v = v * 10 + s[i];//将组合变成一个9位数
return v % hashsize;
}
int try_to_insert(int s) {
int h = hashs(st[s]);
int u = head[h];
while (u) {
if (memcmp(st[u], st[s], sizeof(st[s])) == 0)return 0;
u = nexts[u];
}
nexts[s] = head[h];
head[h] = s;
return 1;
}
int bfs() {
init_lookup_table();
int front = 1, rear = 2;
while (front < rear) {
State& s = st[front];
if (memcmp(s, goal, 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 i = 0; i < 4; i++) {
int newx = x + dx[i];
int newy = y + dy[i];
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;//失败
}
int main() {
for (int i = 0; i < 9; i++)scanf("%d", &st[1][i]);
for (int i = 0; i < 9; i++)scanf("%d", &goal[i]);
int ans = bfs();
if (ans > 0)printf("%d\n", dist[ans]);
else printf("-1\n");
system("pause");
return 0;
}