原题:
Eight
Time Limit: 1000MS Memory Limit: 65536K
Total Submissions: 43121 Accepted: 17543 Special Judge
Description
The 15-puzzle has been around for over 100 years; even if you don’t know it by that name, you’ve seen it. It is constructed with 15 sliding tiles, each with a number from 1 to 15 on it, and all packed into a 4 by 4 frame with one tile missing. Let’s call the missing tile ‘x’; the object of the puzzle is to arrange the tiles so that they are ordered as:
1 2 3 4
5 6 7 8
9 10 11 12
13 14 15 x
where the only legal operation is to exchange ‘x’ with one of the tiles with which it shares an edge. As an example, the following sequence of moves solves a slightly scrambled puzzle:
1 2 3 4 1 2 3 4 1 2 3 4 1 2 3 4
5 6 7 8 5 6 7 8 5 6 7 8 5 6 7 8
9 x 10 12 9 10 x 12 9 10 11 12 9 10 11 12
13 14 11 15 13 14 11 15 13 14 x 15 13 14 15 x
r-> d-> r->
The letters in the previous row indicate which neighbor of the ‘x’ tile is swapped with the ‘x’ tile at each step; legal values are ‘r’,‘l’,‘u’ and ‘d’, for right, left, up, and down, respectively.
Not all puzzles can be solved; in 1870, a man named Sam Loyd was famous for distributing an unsolvable version of the puzzle, and
frustrating many people. In fact, all you have to do to make a regular puzzle into an unsolvable one is to swap two tiles (not counting the missing ‘x’ tile, of course).
In this problem, you will write a program for solving the less well-known 8-puzzle, composed of tiles on a three by three
arrangement.
Input
You will receive a description of a configuration of the 8 puzzle. The description is just a list of the tiles in their initial positions, with the rows listed from top to bottom, and the tiles listed from left to right within a row, where the tiles are represented by numbers 1 to 8, plus ‘x’. For example, this puzzle
1 2 3
x 4 6
7 5 8
is described by this list:
1 2 3 x 4 6 7 5 8
Output
You will print to standard output either the word ``unsolvable’’, if the puzzle has no solution, or a string consisting entirely of the letters ‘r’, ‘l’, ‘u’ and ‘d’ that describes a series of moves that produce a solution. The string should include no spaces and start at the beginning of the line.
Sample Input
2 3 4 1 5 x 7 6 8
Sample Output
ullddrurdllurdruldr
Source
South Central USA 1998
中文:
八数码问题
代码1:
使用康托展开 + 奇偶剪枝 + 状态打表
poj的g++程序不让使用stdc++.h头文件,比较恶心
此外,该题目输入为读入行,建议使用gets 而不要用getline
//#include<bits/stdc++.h>
#include <cstdio>
#include <queue>
#include <string>
#include <cstring>
#include <algorithm>
using namespace std;
const int maxn = 3e6 + 1;
const int inf = 9999999;
typedef pair<int, int> pii;
typedef long long ll;
struct node
{
int x;
int y;
int has;
char num[10];
};
char eig[100];
int dir[4][2] = { {0,1},{0,-1},{-1,0},{1,0} };
bool vis[maxn];
int Pow[10],pre[maxn];
char path[maxn];
void getPow()
{
Pow[0] = Pow[1] = 1;
for (int i = 2; i < 10; i++)
{
Pow[i] = Pow[i - 1] * i;
}
}
int contour(char* s)
{
int num = 0, tot = 0;
for (int i = 0; i < 8; i++)
{
num = 0;
for (int j = i + 1; j < 9; j++)
{
if (s[i] > s[j])
{
num ++;
}
tot += Pow[9 - i - 1] * num;
}
}
return tot + 1;
}
bool judge(int x, int y)
{
if (x < 0 || y < 0 || x>2 || y>2)
{
return false;
}
else
{
return true;
}
}
int getPos(int x, int y)
{
return x * 3 + y;
}
void bfs() {
getPow();
queue<node> Q;
memset(vis, 0, sizeof(vis));
memset(pre, 0, sizeof(pre));
node st;
for (int i = 0; i < 8; i++)
{
st.num[i]= '1' + i;
}
st.num[8]= '0';
st.num[9] = '\0';
int has = contour(st.num);
vis[has] = 1;
path[has] = -1;
st.x = st.y = 2;
st.has = has;
Q.push(st);
while (!Q.empty())
{
node head = Q.front();
Q.pop();
node tmp;
for (int i = 0; i < 4; i++)
{
tmp.x = head.x + dir[i][0];
tmp.y = head.y + dir[i][1];
if (!judge(tmp.x, tmp.y))
{
continue;
}
int pre_pos = getPos(head.x, head.y);
int now_pos = getPos(tmp.x, tmp.y);
strcpy(tmp.num, head.num);
swap(tmp.num[pre_pos], tmp.num[now_pos]);
tmp.has = contour(tmp.num);
if (vis[tmp.has])
{
continue;
}
vis[tmp.has] = 1;
pre[tmp.has] = head.has;
if (i == 0)
{
path[tmp.has] = 'l';
}
else if (i == 1)
{
path[tmp.has] = 'r';
}
else if (i == 2)
{
path[tmp.has] = 'd';
}
else
{
path[tmp.has] = 'u';
}
Q.push(tmp);
}
}
}
void getPath(int state)
{
int i = state;
while(path[i]!=-1)
{
printf("%c",path[i]);
i = pre[i];
}
printf("\n");
}
bool prim(char* s)
{
int cnt = 0;
int len =strlen(s);
for (int i=0;i<len ; i++)
{
for (int j=0;j<i;j++)
{
if (s[i]=='0' || s[j] == '0')
{
continue;
}
if (s[j] > s[i])
{
cnt++;
}
}
}
return cnt % 2 == 0;
}
vector<char> ans;
int main()
{
//ios::sync_with_stdio(false);
bfs();
char s[100];
while (gets(s))
{
int len = strlen(s);
int cnt = 0;
for (int i=0;i<len;i++)
{
if (s[i]=='x')
{
eig[cnt++] = '0';
}
if (s[i]>='1' && s[i] <='8')
{
eig[cnt++]=s[i];
}
}
eig[cnt] = '\0';
int state = contour(eig);
if (prim(eig) && vis[state])
{
getPath(state);
}
else
{
printf("unsolvable\n");
//cout << "unsolvable" << endl;
}
}
return 0;
}
/*
2 3 4 1 5 x 7 6 8
*/
解答1:
印象中,自己在大二刚刚参加acm的时候做过这道题目,现在已经过去好多年了。自己当年学过的东西忘记了实在可惜,有机会一定要全部重新捡起来。
先说粗暴的解决办法,搜索嘛。无论是dfs还是bfs,都是暴力寻找解的过程,这里使用bfs枚举每一个数码的状态,可以使用map或者set记录搜索过的状态,寻找最优解即可,但是这样做会tle。
打表,直接把目标状态,即12345678x作为搜索的起始状态,搜索该状态全部可达到的状态,搜索完成后。每次输出状态直接在表中查询即可。poj这招貌似没啥大用=_=
再说康托展开,如果使用STL(红黑树)记录状态,那么查找的时间复杂度是log(n)级别的,而且查找的key值是一个字符串,更加耗时,这里利用康托展开对状态进行编码,其实康拓展开就是一个hash映射,可以把一个全排列的映射成一个整数hash,此外。康托展开是一个双射函数,也就是说,如果我有hash值,也可以通过hash得到唯一的全排列。
那么,用康托展开的hash用来记录每个状态是否可达,此外,在代码中没有使用hash值反向得到的状态的方法,而是新开了个path数组,记录路径,这样速度会比解hash的方式更加快速一些。
奇偶剪枝,奇偶剪枝实际上在遍历迷宫问题的时候比较常见,尤其是度量方式为曼哈顿距离的时候。
先说结论,如果起始状态与目标状态之间逆序数的奇偶性不同,那么肯定不能达到目标状态。
原因,首先考虑,数码中状态0(即空格不考虑)的位置不影响逆序数,如果横向的移动状态0,奇偶性不发生改变。
例如,有如下情况1
1 2 3
4 5 6
7 0 8
当前逆序数个数为0,如果,交换0和5,那么数码的顺序变成123406758,逆序数个数为2
情况2
1 2 3
4 8 6
7 0 5
逆序数个数为5,如果交换0和8的位置,顺序变为123406785,逆序数个数为3
情况3
1 2 3
4 6 7
8 0 5
逆序数个数为3,交换0和6,顺序变成123407865,逆序数个数为5
可见,更改上下顺序,每次必然会引起2的倍数的奇偶性的变换,所以起始状态和目的状态的奇偶性必然要在相同的情况下才能达到,否则不可能达到。
代码2:
//#include<bits/stdc++.h>
#include <cstdio>
#include <queue>
#include <string>
#include <cstring>
#include <algorithm>
using namespace std;
const int maxn = 3e6 + 1;
const int inf = 9999999;
typedef pair<int, int> pii;
typedef long long ll;
struct node
{
int x;
int y;
int has;
int h;
int g;
char num[10];
bool operator<(const node& rhs) const
{
if (h!=rhs.h)
{
return h > rhs.h;
}
else
{
return h > rhs.g;
}
}
};
char eig[100];
int dir[4][2] = { {0,1},{0,-1},{-1,0},{1,0} };
bool vis[maxn];
int Pow[10],pre[maxn];
char path[maxn];
void getPow()
{
Pow[0] = Pow[1] = 1;
for (int i = 2; i < 10; i++)
{
Pow[i] = Pow[i - 1] * i;
}
}
int getH(node& tmp)
{
int ans = 0;
for (int i=0;i<9;i++)
{
int x = i / 3;
int y = i % 3;
if (tmp.num[i]!='0')
ans += abs(x - (tmp.num[i] - '0' - 1) / 3) + abs(y - (tmp.num[i] - '0' - 1) % 3);
}
return ans;
}
int contour(char* s)
{
int num = 0, tot = 0;
for (int i = 0; i < 9; i++)
{
num = 0;
for (int j = i + 1; j < 9; j++)
{
if (s[i] > s[j])
{
num ++;
}
tot += Pow[9 - i - 1] * num;
}
}
return tot + 1;
}
bool judge(int x, int y)
{
if (x < 0 || y < 0 || x>2 || y>2)
{
return false;
}
else
{
return true;
}
}
int getPos(int x, int y)
{
return x * 3 + y;
}
void bfs() {
getPow();
// queue<node> Q;
priority_queue<node> Q;
memset(vis, 0, sizeof(vis));
memset(pre, 0, sizeof(pre));
node st;
for (int i = 0; i < 8; i++)
{
st.num[i]= '1' + i;
}
st.num[8]= '0';
st.num[9] = '\0';
int has = contour(st.num);
vis[has] = 1;
path[has] = -1;
st.x = st.y = 2;
st.has = has;
st.g= 0;
st.h = getH(st);
Q.push(st);
while (!Q.empty())
{
node head = Q.top();
// node head = Q.front();
Q.pop();
node tmp;
for (int i = 0; i < 4; i++)
{
tmp.x = head.x + dir[i][0];
tmp.y = head.y + dir[i][1];
if (!judge(tmp.x, tmp.y))
{
continue;
}
int pre_pos = getPos(head.x, head.y);
int now_pos = getPos(tmp.x, tmp.y);
strcpy(tmp.num, head.num);
swap(tmp.num[pre_pos], tmp.num[now_pos]);
tmp.has = contour(tmp.num);
tmp.g = head.g + 1;
tmp.h = getH(tmp);
if (vis[tmp.has])
{
continue;
}
vis[tmp.has] = 1;
pre[tmp.has] = head.has;
if (i == 0)
{
path[tmp.has] = 'l';
}
else if (i == 1)
{
path[tmp.has] = 'r';
}
else if (i == 2)
{
path[tmp.has] = 'd';
}
else
{
path[tmp.has] = 'u';
}
Q.push(tmp);
}
}
}
void getPath(int state)
{
int i = state;
while(path[i]!=-1)
{
printf("%c",path[i]);
i = pre[i];
}
printf("\n");
}
bool prim(char* s)
{
int cnt = 0;
int len =strlen(s);
for (int i=0;i<len ; i++)
{
for (int j=0;j<i;j++)
{
if (s[i]=='0' || s[j] == '0')
{
continue;
}
if (s[j] > s[i])
{
cnt++;
}
}
}
return cnt % 2 == 0;
}
vector<char> ans;
int main()
{
//ios::sync_with_stdio(false);
bfs();
char s[100];
while (gets(s))
{
int len = strlen(s);
int cnt = 0;
for (int i=0;i<len;i++)
{
if (s[i]=='x')
{
eig[cnt++] = '0';
}
if (s[i]>='1' && s[i] <='8')
{
eig[cnt++]=s[i];
}
}
eig[cnt] = '\0';
int state = contour(eig);
if (prim(eig) && vis[state])
{
getPath(state);
}
else
{
printf("unsolvable\n");
//cout << "unsolvable" << endl;
}
}
return 0;
}
/*
2 3 4 1 5 x 7 6 8
*/
解答2:
第二份代码里面用了a star算法,既然是寻解问题,肯定可以搞点启发式策略。
A star算法对比普通的寻解策略在于它使用了启发式规则f(i) = h(i) + g(i)。
h(i)是在状态i时估计启发值,g(i)是在状态i时确定的启发值。
在eight 问题里面, g(i)记为当前走了多少步,h(i)记录为当前数码状态与目标数码状态之间的曼哈顿距离
队列改为使用优先队列,每次寻找f(i)最小的节点优先出队,这样有助于更快的找到目的状态。
注意!不过在上面的代码里面依然使用了打表的计算方式枚举了所有状态。
hdoj的判定比较严格,另外,这里使用ida实现
#include<bits/stdc++.h>
using namespace std;
int dx[] = {-1, 0, 0, 1};
int dy[] = {0, -1, 1, 0};
char op[] = {'u', 'l', 'r', 'd'};
int goal_state[10][2] = {{0,0}, {1,1}, {1,2}, {1,3}, {2,1}, {2,2}, {2,3}, {3,1}, {3,2}, {3,3}};
int depth;
char a[10];
char path[1000];
struct node
{
char board[4][4];
int x, y;
};
int get_h(char board[][4])
{
int cost = 0;
for(int i = 1; i <= 3; i++)
for(int j = 1; j <= 3; j++)
if(board[i][j])
cost += abs(i - goal_state[board[i][j]][0]) + abs(j - goal_state[board[i][j]][1]);
return cost;
}
bool prim() {//0
int cnt = 0;
for(int i = 1; i <= 9; i++)
for(int j = 1; j < i; j++)
if(a[j] < a[i] && a[j])
cnt++;
if(cnt & 1)
return false;
return true;
}
bool check(int x, int y) {
if(x >= 1 && x <= 3 && y >= 1 && y <= 3)
return true;
return false;
}
bool dfs(node now, int cur, int pre)
{
if(get_h(now.board) == 0)
{
path[cur] = 0;
return true;
}
for(int i = 0; i < 4; i++)
{
if(i + pre == 3)//1
continue;
node next = now;
next.x += dx[i];
next.y += dy[i];
if(check(next.x, next.y))
{
path[cur] = op[i];
swap(next.board[next.x][next.y], next.board[now.x][now.y]);
if(get_h(next.board) + cur >= depth)
continue;
if(dfs(next, cur + 1, i))
return true;
}
}
return false;
}
void solve(node st)
{
depth = get_h(st.board);
while(true)
{
if (dfs(st,0,100))
{
puts(path);
break;
}
depth++;
}
}
int main()
{
int sx, sy;
char c[2];
while(1)
{
node st;
for(int i = 1; i <= 3; i++)
{
for(int j = 1; j <= 3; j++)
{
if(scanf("%s", c) == EOF)
return 0;
if(c[0] == 'x')
{
st.board[i][j] = 0;
st.x = i, st.y = j;
}
else
st.board[i][j] = c[0] - '0';
a[(i - 1) * 3 + j] = st.board[i][j];
}
}
if(prim())
solve(st);
else
printf("unsolvable\n");
}
return 0;
}
上面的代码参考了别人的(自己写的怎么都不过=_=)
ida每次遍历迭代的深度信息,并判断当前遍历的深度与当前状态距离目标状态的曼哈顿距离作为启发函数f,如果f大于等于枚举的depth,就不用搜索了。
另外,还可以在dfs时,把和上一步相反的操作的过滤掉,能介绍一些重复的搜索