路勁尋找-八數碼問題(判重)
題目:編號為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;
}