先看了一篇别人的题解, 虽然这个题解过不了OJ,内存超限, 但是里面用到一种思想挺好, 就是逆向BFS+康托展开,因为终点状态唯一, 所以, 将终点作为第一个状态点 ,放进队列, 然后开始逆向广搜,将每一个没有出现过的状态点都存起来, 然后后面每一次询问的时候,就不用再去搜了, 直接O(1)的查询, 这种思路还是很不错的。!!!
下面是这个MLE代码, 虽然没有过, 但是还是很有借鉴价值的:
//这种思路是挺好的 , 但是内存会超限
#include<iostream>
#include<cstring>
#include<cstdio>
#include<queue>
#define N 9
#define NMAX 362885
using namespace std;
struct Point{
char eightMap[N];
int position;
string path;
};
const int fac[] = {1, 1, 2, 6, 24, 120, 720, 5040, 40320};
const int dir[4][2] = {{1, 0}, {0, 1}, {-1, 0}, {0, -1}};
const char option[4] = {'u', 'l', 'd', 'r'};
bool visited[NMAX];
string inversionPath[NMAX];
inline int cantor(Point p) { //康托展开
int ans = 0;
for(int i = 0; i < N; i++) {
int cnt = 0;
for(int k = i+1; k < N; k++) {
if(p.eightMap[k] < p.eightMap[i]) cnt++;
}
ans += fac[8-i]*cnt;
}
return ans;
}
bool bfs() {
int row, col , cantorValue;
memset(visited, false, sizeof(visited));
Point startPoint;
for(int i = 1; i < N; i++) startPoint.eightMap[i-1] = i+'0';
startPoint.eightMap[N-1] = 'x';
startPoint.position = N-1;
startPoint.path = "";
visited[0] = true;
queue<Point> Q;
Q.push(startPoint);
while(!Q.empty()) {
Point p = Q.front();
Q.pop();
for(int i = 0; i < 4; i ++) {
row = p.position/3+dir[i][0]; //获取x的行列号
col = p.position%3+dir[i][1];
if(row < 0 ||col < 0 || row >= 3 ||col >= 3) continue;
Point newPoint(p); //初始化赋值新点
newPoint.eightMap[newPoint.position] = newPoint.eightMap[col+row*3];
newPoint.position = col+row*3; //x的位置要说清楚
newPoint.eightMap[newPoint.position] = 'x'; //和上上面一行 作用是 x和一个点交换位置
cantorValue = cantor(newPoint); //获取这个新状态的编号
if(visited[cantorValue]) continue; //如果这个状态已经访问过了 不用加入队列
visited[cantorValue] = true; //否则机上已经访问的标记
newPoint.path += option[i]; //新点中加上路径记录
inversionPath[cantorValue] = newPoint.path; //将路径存起来
Q.push(newPoint);//放到队列中
}
//这样逆向的好处是: 不用每一次询问都重新bfs一次, 而是把所有状态到这个终极状态的路径都存起来
//后面在查询的时候, 时间复杂度就是O(1); 66666666666666666真是个好办法!!!
}
return false;
}
int main() {
Point startPoint;
int startCantor;
bfs();
while(cin >> startPoint.eightMap[0]) {
if(startPoint.eightMap[0] == 'x')
startPoint.position = 0;
for(int i = 1; i < 9; i ++) {
cin >> startPoint.eightMap[i];
if(startPoint.eightMap[i] == 'x')
startPoint.position = i;
}
startCantor = cantor(startPoint);
if(visited[startCantor]) {
for(int i= inversionPath[startCantor].size()-1; i >= 0; i --)
cout << inversionPath[startCantor][i];
cout << endl;
}
else
cout << "unsolvable" << endl;
}
return 0;
}
被折磨了三天, 看着大神的题解, 终于搞定了:打表不适用了会MLE, 这里用的是A* 搜索(BFS+优先队列+估价函数) (也可以用双向BFS搞定,我还没试过。。。
#include<iostream>
#include<cstring>
#include<cstdio>
#include<queue>
#include<cmath>
using namespace std;
struct node {
int str[9]; //八数码具体情况
int h, g; //两个估价函数
int x, y; //空格的位置
int Hash; //HASH值
bool operator< (const node n) const {
return (h!=n.h)?(h>n.h):(g>n.g);
}
bool check() { //判断是否合法
if(x>=0&&x<3&&y>=0&&y<3)
return true;
return false;
}
} s, u, v;
int HASH[9]= {1,1,2,6,24,120,720,5040,40320}; //HASH的权值
int aimHash = 46233;
int vis[400000]; //判断状态已经表里, 初始为-1, 否则为到达这步的方向
int pre[400000]; //路径保存
int way[4][2] = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}}; //四个方向 右左上下
int get_hash(node tmp) { //获得HASH值 (康托展开)
int ans = 0;
for(int i = 0; i < 9; i++) {
int cnt = 0;
for(int j = i+1; j < 9; j++)
if(tmp.str[j] < tmp.str[i])
cnt++;
ans += cnt*HASH[8-i];
}
return ans;
}
bool isok(node tmp) { //求出逆序对, 判断是否有解(偶数有解, 奇数无解)
int ans = 0;
for(int i = 0; i < 9; i++) {
for(int j = i+1; j < 9; j++)
if(tmp.str[i] && tmp.str[j] && tmp.str[i] > tmp.str[j]) ans++;
}
return !(ans&1);
}
int get_h(node tmp) {
int ans = 0, x, y;
for(int i = 0; i < 9; i++) {
if(tmp.str[i] != 0) {
x = (tmp.str[i]-1)/3;
y = (tmp.str[i]-1)%3;
} else {
x = 2;
y = 2;
}
ans += abs(i/3-x)+abs(i%3-y);
}
return ans;
}
int ccc = 0;
void Astar() {
priority_queue<node> que;
que.push(s);
while(!que.empty()) {
u = que.top();
que.pop();
for(int i = 0; i < 4; i++) {
v = u;
v.x += way[i][0];
v.y += way[i][1];
if(v.check()) {
swap(v.str[v.x*3+v.y], v.str[u.x*3+u.y]);
v.Hash = get_hash(v);
if(vis[v.Hash] == -1) {
vis[v.Hash] = i;
pre[v.Hash] = u.Hash;
v.g++;
v.h = get_h(v);
que.push(v);
}
if(v.Hash == aimHash) return ;//为什么放在这里就不会超时!!!!!!!!!
}
}
}
}
void Print() {
string ans;
ans.clear();
int nxt = aimHash;
while(pre[nxt]!=-1) { //从终点往起点找路径
switch(vis[nxt]) {
case 0:ans+='r';break;
case 1:ans+='l';break;
case 2:ans+='d';break;
case 3:ans+='u';break;
}
nxt = pre[nxt];
}
for(int i = ans.size()-1; i >= 0; i--)
putchar(ans[i]);
puts("");
}
int main() {
char a[100];
while(gets(a)!=NULL) {
memset(vis, -1, sizeof(vis));
memset(pre, -1, sizeof(pre));
int len = strlen(a), k = 0;
for(int i = 0; i < len; i++) {
if((a[i]<'9' && a[i] >= '0') || a[i] == 'x') {
if(a[i]!='x') s.str[k] = a[i] - '0';
else {
s.str[k] = 0;
s.x = k/3;
s.y = k%3;
}
k++;
}
}
if(!isok(s)) {
printf("unsolvable\n");
continue;
}
s.Hash = get_hash(s);
if(s.Hash == aimHash) {
puts("");
continue;
}
vis[s.Hash] = -2;
s.g = 0;
s.h = get_h(s);
Astar();
Print();
}
return 0;
}
经验小总结:
①:之前的bfs中节点的判断都是写在扩展节点前面的, 之所以过了, 是因为数据比较水, 这一题如果也写在for的前面(向下扩展)就一定会TLE, 因为节点扩展越到后面扩展的
就越多, 假设某一个点已经达到状态了,却不对他进行判断,还将它入队,那么等判断到他的时候, 中间就要判断更多的点了, 所以最合适的判断时机是, 每有一个新点被扩
展出来的时候,就立即判断他是不是目标状态点。
②:A*搜索:A*是一种启发式搜索,g为已花代价(在这题中g即为扩展时已经移动的总步数),h为估计的剩余代价(在这题中h即为每个状态中九个数字当前位置与目标状态时的九个数字应该的位置的曼哈顿距离之和),而A*是根据f=g+h作为估价函数进行排列,也就是优先选择可能最优的节点进行扩展。
③:就是一维数组的元素序数到二维数组的行列坐标的转换。最好的方式就是一维从0开始,那么序数n到二维坐标就直接是(n/3, n%3),同理二维坐标(x,y)到一维的转换变成一维的序数就是x*3+y;这个序数也是从0开始的。
④:我的想法变了, 之前一直以为哈希函数是什么固定的函数,然而事实上就是自己任意构造一个合理的能够自变量因变量一一对应关系的函数,这题中的get_Hash()函数就是一个哈希函数,然而事实上这就是一个康托计数而已。
⑤:选用正确的头文件, 向cmath头文件就可以直接调用, 快速的处理abs这一类对数值进行操作的函数,不用自己写,很方便。
⑥:我一直用的Dev这个IDE, 这个IDE有个不好的地方就是一个窗口中写了多个文件的时候, 编译可能会出问题, 比如不同文件中, 如果有相同的函数名的函数, 会交叉调用(貌似是这样,具体我也说不清, 总之乱七八糟),因为这个原因和前面一个原因在一起导致我被误导了很久。
⑦:阶乘问题:哈希函数(康托计数)中,数据范围比较小,(将可能用到的范围)直接打好表, 不要在程序中每次有输入数据都重新算一次,非常浪费时间, 这一题中的Hash数组就是一个预处理,节省了很多时间,后面康托计数可以直接调用,不用重新去算。
⑧:这一题的答案不是唯一的,只要是可行解都是正确的,以至于我之前看了大神的博客后,将这个aimHash的值写的和题解上的一样(然而我写的hash函数和题解的hash函数的计算并不是相同的过程,这样就是完全错误的), 这样是不对的, hash值应当与哈希函数相对应,如果不对应的话那就毫无意义!(如果照抄别人的hash值,而函数的计算规则不同, 那就毫无任何意义!)
⑨:gets的while输入处理,while(gets(a)!=NULL) 而不是EOF。
⑩:BFS中我之前记录节点路径一直是放在每一个节点中的, 这一题就是在节点外进行路径存储的例子,存在全局数组中。(我之前那些广搜题,都是记录节点的二维坐标,我都是存在节点里面的,然后开个全局结构体数组,每个数组结构体元素中保存父结点的坐标,和当前节点坐标, 最后DFS打印路径即可)。这一题中,八数码的不重复的状态锁对应的HASH值都是唯一的, 所以可以直接全局数组记录路径。
参考博客:http://blog.csdn.net/acm_cxlove/article/details/7745323