题意
八数码问题,不必赘述。
思路
BFS 在 POJ 过,然而 HDU T成狗。
正解为 A* ,加上 康托展开 压缩状态。
A*算法
A* 算法的核心是公式 f = g + h。其中,g 起始状态到当前状态的距离,h 当前状态到目标状态的估计距离。
BFS 是 A* 的一种特殊状况, h 恒等于 0。
h 的估计
如果h(n)< d(n)到目标状态的实际距离,这种情况下,搜索的点数多,搜索范围大,效率低。但能得到最优解。
如果h(n)=d(n),即距离估计h(n)等于最短距离,那么搜索将严格沿着最短路径进行, 此时的搜索效率是最高的。
如果 h(n)>d(n),搜索的点数少,搜索范围小,效率高,但不能保证得到最优解。
实现起来与 BFS 很相似,要用到优先队列。
A* 算法的缺点是占用内存随问题的规模指数增长。据说 IDA* 能弥补这一点,以后再学。
可能是因为这个原因,有一个迷之 MLE 的地方。具体见 AC 代码中注释的注意点把。
本题中把状态中每个数字到正确位置的欧氏距离和作为估计值。
康托展开
就是用从小到大的次序序号代表长度为 n 的排列,可以把状态数压缩到 n!。
求每个排列的序号可以求小于它的排列的个数,用数位DP的思想。枚举相等的位,以下的位便不受限制。特别的,第 t 位 (n - 1 - t)! * k ,其中 k 是 t 位之后的小于第 t 为的数字的个数。
题目链接
http://acm.hdu.edu.cn/showproblem.php?pid=1043
AC代码
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<queue>
#include<cstdlib>
#include<cstring>
using namespace std;
const int maxn = 4e5 + 10;
const int mx[4] = {-1, 0, 1, 0};
const int my[4] = {0, 1, 0, -1};
const char op[7] = "urdl";
struct node //保存 A* 状态的结构体
{
int f, g, h; //f = g + h
int has; //康托展开后的哈希值
int m[3][3]; //保存当前状态图
int x, y; //空白格的位置
bool operator < (node b) const {return f == b.f ? g > b.g : f > b.f;} //重载 < 号,优先队列
};
bool vis[maxn]; //A* 的访问标记
int pre[maxn]; //保存路径中的前缀
char chr[maxn]; //保存路径中的操作
int get_h(int m[][3]) //状态中每个数字到正确位置的欧氏距离和作为估计值
{
int res = 0;
for(int i= 0; i< 3; i++)
for(int j= 0; j< 3; j ++)
res += abs((m[i][j] - 1) / 3 - i) + abs((m[i][j] - 1) % 3 - j);
return res;
}
const int can[9] = {1,1,2,6,24,120,720,5040,40320}; //9个数的康托展开
int Cantor(int m[][3])
{
int res = 0, k = 0;
int temp[10]; //保存到一维数组中便于计算
for(int i= 0; i< 3; i++)
for(int j= 0; j< 3; j++)
temp[k ++] = m[i][j];
for(int i= 0; i< 9; i++) //类比数位DP的原理
{
k = 0;
for(int j= i + 1; j< 9; j++)
if(temp[j] < temp[i]) k ++;
res += can[9 - 1 - i] * k;
}
return res;
}
void print(int x) //输出路径
{
if(pre[x] == -1) return;
print(pre[x]);
printf("%c", chr[x]);
}
void Astar(node s) //A* 搜索
{
if(s.has == 0) //起始状态就是目标状态
{
printf("\n");
return;
}
priority_queue<node> qu; //从初始状态出发
memset(vis, false, sizeof vis);
vis[s.has] = true;
qu.push(s);
pre[s.has] = -1;
while(qu.size())
{
node v = qu.top();
qu.pop();
for(int i= 0; i< 4; i++)
{
int xx = v.x + mx[i], yy = v.y + my[i];
if(xx < 0 || yy < 0 || xx >= 3 || yy >= 3) continue;
node u = v;
swap(u.m[u.x][u.y], u.m[xx][yy]);
u.has = Cantor(u.m);
if(vis[u.has]) continue; //判断当前状态是否访问过
vis[u.has] = true;//注意,在这里做访问标记,不是从队列中取出的时候,借助dijkstra理解
pre[u.has] = v.has, chr[u.has] = op[i]; //记录路径
if(u.has == 0) //到达最终状态,注意点
{
print(0);
printf("\n");
return;
}
u.g ++; //填写状态信息
u.h = get_h(u.m);
u.f = u.g + u.h;
u.x = xx, u.y = yy;
qu.push(u); //加入队列
}
}
}
int main()
{
char str[30];
node s;
while(gets(str))
{
for(int i= 0, j = 0; i< 9 && str[j] != '\n'; j++) //建立起点的状态
{
if(str[j] == ' ') continue;
else if(str[j] == 'x') s.m[i / 3][i % 3] = 9, s.x = i / 3, s.y = i % 3;
else s.m[i / 3][i % 3] = str[j] - '0';
i ++;
}
s.g = 0;
s.h = get_h(s.m);
s.f = s.g + s.h;
s.has = Cantor(s.m);
int k = 0; //通过逆序数的奇偶判断是否可解
for(int i= 0; i< 9; i++)
for(int j= 0; j< i; j++)
if(s.m[j / 3][j % 3] != 9 && s.m[i / 3][i % 3] < s.m[j / 3][j % 3]) k ++;
if(k & 1) printf("unsolvable\n");
else Astar(s); //注意点
}
return 0;
}