经典的八数码问题,有人说不做此题人生不完整,哈哈。
状态总数是9! = 362880 种,不算太多,可以满足广搜和A*对于空间的需求。
状态可以每次都动态生成,也可以生成一次存储起来,我用的动态生成,《组合数学》书上有一种生成排列的方法叫做"序数法",我看了一会书,把由排列到序数,和由序数到排列的两个函数写了出来,就是代码中的int order(const char *s, int n) 和void get_node(int num, node &tmp)两个函数。
启发函数,用的是除空格外的八个数字到正确位置的网格距离。
几种方法的比较:广搜,效率最低,500ms;A*,32ms,已经比较高效了;IDA*, 0ms,空间也减少许多。A*为判重付出了巨大代价,时间 and 空间,uva上还有一个15数码的题,用A*肯定会爆空间的。IDA*不记录已经走过的路径,所以省去了空间,也省去了判断重复的步骤,但是会出现重复计算。
广搜:
// BFS
#include < iostream >
#include < cstdio >
#include < queue >
using namespace std;
/* 把1..n的排列映射为数字 0..(n!-1) */
int fac[] = { 1 , 1 , 2 , 6 , 24 , 120 , 720 , 5040 , 40320 , 362880 }; // ...
int order( const char * s, int n) {
int i, j, temp, num;
num = 0 ;
for (i = 0 ; i < n - 1 ; i ++ ) {
temp = 0 ;
for (j = i + 1 ; j < n; j ++ ) {
if (s[j] < s[i])
temp ++ ;
}
num += fac[s[i] - 1 ] * temp;
}
return num;
}
bool is_equal( const char * b1, const char * b2){
for ( int i = 0 ; i < 9 ; i ++ )
if (b1[i] != b2[i])
return false ;
return true ;
}
// hash
struct node{
char board[ 9 ];
char space; // 空格所在位置
};
const int TABLE_SIZE = 362880 ;
int hash( const char * cur){
return order(cur, 9 );
}
/* 整数映射成排列 */
void get_node( int num, node & tmp) {
int n = 9 ;
int a[ 9 ]; // 求逆序数
for ( int i = 2 ; i <= n; ++ i) {
a[i - 1 ] = num % i;
num = num / i;
tmp.board[i - 1 ] = 0 ; // 初始化
}
tmp.board[ 0 ] = 0 ;
int rn, i;
for ( int k = n; k >= 2 ; k -- ) {
rn = 0 ;
for (i = n - 1 ; i >= 0 ; -- i) {
if (tmp.board[i] != 0 )
continue ;
if (rn == a[k - 1 ])
break ;
++ rn;
}
tmp.board[i] = k;
}
for (i = 0 ; i < n; ++ i)
if (tmp.board[i] == 0 ) {
tmp.board[i] = 1 ;
break ;
}
tmp.space = n - a[n - 1 ] - 1 ;
}
char visited[TABLE_SIZE];
int parent[TABLE_SIZE];
char move[TABLE_SIZE];
int step[ 4 ][ 2 ] = {{ - 1 , 0 },{ 1 , 0 }, { 0 , - 1 }, { 0 , 1 }}; // u, d, l, r
void BFS( const node & start){
int x, y, k, a, b;
int u, v;
for (k = 0 ; k < TABLE_SIZE; ++ k)
visited[k] = 0 ;
u = hash(start.board);
parent[u] = - 1 ;
visited[u] = 1 ;
queue < int > que;
que.push(u);
node tmp, cur;
while ( ! que.empty()){
u = que.front();
que.pop();
get_node(u, cur);
k = cur.space;
x = k / 3 ;
y = k % 3 ;
for ( int i = 0 ; i < 4 ; ++ i){
a = x + step[i][ 0 ];
b = y + step[i][ 1 ];
if ( 0 <= a && a <= 2 && 0 <= b && b <= 2 ){
tmp = cur;
tmp.space = a * 3 + b;
swap(tmp.board[k], tmp.board[tmp.space]);
v = hash(tmp.board);
if (visited[v] != 1 ){
move[v] = i;
visited[v] = 1 ;
parent[v] = u;
if (v == 0 ) // 目标结点hash值为0
return ;
que.push(v);
}
}
}
}
}
void print_path(){
int n, u;
char path[ 1000 ];
n = 1 ;
path[ 0 ] = move[ 0 ];
u = parent[ 0 ];
while (parent[u] != - 1 ){
path[n] = move[u];
++ n;
u = parent[u];
}
for ( int i = n - 1 ; i >= 0 ; -- i){
if (path[i] == 0 )
printf( " u " );
else if (path[i] == 1 )
printf( " d " );
else if (path[i] == 2 )
printf( " l " );
else
printf( " r " );
}
}
int main(){
freopen( " in " , " r " , stdin);
node start;
char c;
for ( int i = 0 ; i < 9 ; ++ i){
cin >> c;
if (c == ' x ' ){
start.board[i] = 9 ;
start.space = i;
}
else
start.board[i] = c - ' 0 ' ;
}
BFS(start);
if (visited[ 0 ] == 1 )
print_path();
else
printf( " unsolvable " );
return 0 ;
}
A*算法:
代码中对priority_queue<>模板的使用还是很有技巧性的,通过push一个最小的,再把它pop出来就解决了由于更改造成不一致性。前面这种做法是错误的,多谢一位朋友的提醒,这种方式确实不能保持堆的性质。不过我们可以采用冗余的办法,直接插入新值,这就不会破坏堆的性质了。
修改过的代码:
// A*
#include < iostream >
#include < cstdio >
#include < cstring >
#include < cstdlib >
#include < queue >
using namespace std;
/* 把1..n的排列映射为数字 0..(n!-1) */
int fac[] = { 1 , 1 , 2 , 6 , 24 , 120 , 720 , 5040 , 40320 , 362880 }; // ...
int order( const char * s, int n) {
int i, j, temp, num;
num = 0 ;
for (i = 0 ; i < n - 1 ; i ++ ) {
temp = 0 ;
for (j = i + 1 ; j < n; j ++ ) {
if (s[j] < s[i])
temp ++ ;
}
num += fac[s[i] - 1 ] * temp;
}
return num;
}
bool is_equal( const char * b1, const char * b2){
for ( int i = 0 ; i < 9 ; i ++ )
if (b1[i] != b2[i])
return false ;
return true ;
}
// hash
struct node{
char board[ 9 ];
char space; // 空格所在位置
};
const int TABLE_SIZE = 362880 ;
int hash( const char * cur){
return order(cur, 9 );
}
/* 整数映射成排列 */
void get_node( int num, node & tmp) {
int n = 9 ;
int a[ 9 ]; // 求逆序数
for ( int i = 2 ; i <= n; ++ i) {
a[i - 1 ] = num % i;
num = num / i;
tmp.board[i - 1 ] = 0 ; // 初始化
}
tmp.board[ 0 ] = 0 ;
int rn, i;
for ( int k = n; k >= 2 ; k -- ) {
rn = 0 ;
for (i = n - 1 ; i >= 0 ; -- i) {
if (tmp.board[i] != 0 )
continue ;
if (rn == a[k - 1 ])
break ;
++ rn;
}
tmp.board[i] = k;
}
for (i = 0 ; i < n; ++ i)
if (tmp.board[i] == 0 ) {
tmp.board[i] = 1 ;
break ;
}
tmp.space = n - a[n - 1 ] - 1 ;
}
// 启发函数: 除去x之外到目标的网格距离和
int goal_state[ 9 ][ 2 ] = {{ 0 , 0 }, { 0 , 1 }, { 0 , 2 },
{ 1 , 0 }, { 1 , 1 }, { 1 , 2 }, { 2 , 0 }, { 2 , 1 }, { 2 , 2 }};
int h( const char * board){
int k;
int hv = 0 ;
for ( int i = 0 ; i < 3 ; ++ i)
for ( int j = 0 ; j < 3 ; ++ j){
k = i * 3 + j;
if (board[k] != 9 ){
hv += abs(i - goal_state[board[k] - 1 ][ 0 ]) +
abs(j - goal_state[board[k] - 1 ][ 1 ]);
}
}
return hv;
}
int f[TABLE_SIZE], d[TABLE_SIZE]; // 估计函数和深度
// 优先队列的比较对象
struct cmp{
bool operator () ( int u, int v){
return f[u] > f[v];
}
};
char color[TABLE_SIZE]; // 0, 未访问;1, 在队列中,2, closed
int parent[TABLE_SIZE];
char move[TABLE_SIZE];
int step[ 4 ][ 2 ] = {{ - 1 , 0 },{ 1 , 0 }, { 0 , - 1 }, { 0 , 1 }}; // u, d, l, r
void A_star( const node & start){
int x, y, k, a, b;
int u, v;
priority_queue < int , vector < int > , cmp > open;
memset(color, 0 , sizeof ( char ) * TABLE_SIZE);
u = hash(start.board);
parent[u] = - 1 ;
d[u] = 0 ;
f[u] = h(start.board);
open.push(u);
color[u] = 1 ;
node tmp, cur;
while ( ! open.empty()){
u = open.top();
if (u == 0 )
return ;
open.pop();
get_node(u, cur);
k = cur.space;
x = k / 3 ;
y = k % 3 ;
for ( int i = 0 ; i < 4 ; ++ i){
a = x + step[i][ 0 ];
b = y + step[i][ 1 ];
if ( 0 <= a && a <= 2 && 0 <= b && b <= 2 ){
tmp = cur;
tmp.space = a * 3 + b;
swap(tmp.board[k], tmp.board[tmp.space]);
v = hash(tmp.board);
if (color[v] == 1 && (d[u] + 1 ) < d[v]){ // v in open
move[v] = i;
f[v] = f[v] - d[v] + d[u] + 1 ; // h[v]已经求过
d[v] = d[u] + 1 ;
parent[v] = u;
// 直接插入新值, 有冗余,但不会错
open.push(v);
}
else if (color[v] == 2 && (d[u] + 1 ) < d[v]){ // v in closed
move[v] = i;
f[v] = f[v] - d[v] + d[u] + 1 ; // h[v]已经求过
d[v] = d[u] + 1 ;
parent[v] = u;
open.push(v);
color[v] = 1 ;
}
else if (color[v] == 0 ){
move[v] = i;
d[v] = d[u] + 1 ;
f[v] = d[v] + h(tmp.board);
parent[v] = u;
open.push(v);
color[v] = 1 ;
}
}
}
color[u] = 2 ; //
}
}
void print_path(){
int n, u;
char path[ 1000 ];
n = 1 ;
path[ 0 ] = move[ 0 ];
u = parent[ 0 ];
while (parent[u] != - 1 ){
path[n] = move[u];
++ n;
u = parent[u];
}
for ( int i = n - 1 ; i >= 0 ; -- i){
if (path[i] == 0 )
printf( " u " );
else if (path[i] == 1 )
printf( " d " );
else if (path[i] == 2 )
printf( " l " );
else
printf( " r " );
}
}
int main(){
// freopen("in", "r", stdin);
node start;
char c;
for ( int i = 0 ; i < 9 ; ++ i){
cin >> c;
if (c == ' x ' ){
start.board[i] = 9 ;
start.space = i;
}
else
start.board[i] = c - ' 0 ' ;
}
A_star(start);
if (color[ 0 ] != 0 )
print_path();
else
printf( " unsolvable " );
return 0 ;
}
// IDA*
#include < iostream >
#include < cstdio >
#include < cstdlib >
using namespace std;
#define SIZE 3
char board[SIZE][SIZE];
// 启发函数: 除去x之外到目标的网格距离和
int goal_state[ 9 ][ 2 ] = {{ 0 , 0 }, { 0 , 1 }, { 0 , 2 },
{ 1 , 0 }, { 1 , 1 }, { 1 , 2 }, { 2 , 0 }, { 2 , 1 }, { 2 , 2 }};
int h( char board[][SIZE]){
int cost = 0 ;
for ( int i = 0 ; i < SIZE; ++ i)
for ( int j = 0 ; j < SIZE; ++ j){
if (board[i][j] != SIZE * SIZE){
cost += abs(i - goal_state[board[i][j] - 1 ][ 0 ]) +
abs(j - goal_state[board[i][j] - 1 ][ 1 ]);
}
}
return cost;
}
int step[ 4 ][ 2 ] = {{ - 1 , 0 }, { 0 , - 1 }, { 0 , 1 }, { 1 , 0 }}; // u, l, r, d
char op[ 4 ] = { ' u ' , ' l ' , ' r ' , ' d ' };
char solution[ 1000 ];
int bound; // 上界
bool ans; // 是否找到答案
int DFS( int x, int y, int dv, char pre_move){ // 返回next_bound
int hv = h(board);
if (hv + dv > bound)
return dv + hv;
if (hv == 0 ){
ans = true ;
return dv;
}
int next_bound = 1e9;
for ( int i = 0 ; i < 4 ; ++ i){
if (i + pre_move == 3 ) // 与上一步相反的移动
continue ;
int nx = x + step[i][ 0 ];
int ny = y + step[i][ 1 ];
if ( 0 <= nx && nx < SIZE && 0 <= ny && ny < SIZE){
solution[dv] = i;
swap(board[x][y], board[nx][ny]);
int new_bound = DFS(nx, ny, dv + 1 , i);
if (ans)
return new_bound;
next_bound = min(next_bound, new_bound);
swap(board[x][y], board[nx][ny]);
}
}
return next_bound;
}
void IDA_star( int sx, int sy){
ans = false ;
bound = h(board); // 初始代价
while ( ! ans && bound <= 100 ) // 上限
bound = DFS(sx, sy, 0 , - 10 );
}
int main(){
freopen( " in " , " r " , stdin);
int sx, sy; // 起始位置
char c;
for ( int i = 0 ; i < SIZE; ++ i)
for ( int j = 0 ; j < SIZE; ++ j){
cin >> c;
if (c == ' x ' ){
board[i][j] = SIZE * SIZE;
sx = i;
sy = j;
}
else
board[i][j] = c - ' 0 ' ;
}
IDA_star(sx, sy);
if (ans){
for ( int i = 0 ; i < bound; ++ i)
cout << op[solution[i]];
}
else
cout << " unsolvable " ;
return 0 ;
}