当年我省只有vfk一人AC此题(其实是AK),学长记载的时候猜测他一定用了很高级的算法。做完之后发现特定算法方面它只用到了BFS和SPFA,不禁想起自己去年NOIP,三个小时没做出一道DFS(得了10分)。
读完题后,判断它是搜索。棋子的移动等价于空格的移动。要不要使用启发式搜索?怎样表示一个状态?我发现只要标记出空格的位置和指定棋子的位置。这样一来,状态只有不到810000个,60分应该有了。500次查询是怎么回事?4亿多的计算量……启发式搜索能让有解的情况快一点,无解还是得搜索整个状态空间。这是一个上界,也许实际情况达不到这么坏?然而,吃过几次亏了,对数据没有明确说明的特性不要乱假设……
和WZH学长交流了一下,结果他看题解去了(嘿嘿)……看完题解的WZH学长,每句话都带着强烈的启发性。虽然不是很明白他在说什么,但他强调了四连通。
先回到自己的思路。如果使用启发式搜索,启发函数我会选择指定棋子到终点的曼哈顿距离。这样不能体现空格和棋子之间的联系,移动的是空格,然而醉翁之意不在酒,棋子才是我的目标。如果开局的时候空格离指定棋子很远,会扩展出很多无用的状态,因为是让空格走,这个不影响启发函数。也许我应该把空格和棋子的距离加入启发函数?这样仍然不能避免无解时搜索整个状态空间。我也考虑过让空格先走到棋子附近,再搜索,把空格处于四连通或八连通位置作为初始状态。W学长的话又促使我思考,发现四连通就够了。但是还是禁不住巨大的搜索量。
我得从根本上加以改进,减少状态数。先前想的那些策略,都是为了让空格尽量接近棋子,不走冤枉路,不喧宾夺主。能不能让棋子自己主导搜索?它想往旁边移一格,怎么办?借助于空格。空格先到那里,再和它交换一下位置。必须是这样。是否可以规定状态中棋子和空格是相邻的?这样使状态数减少到3600个。怎么扩展呢?每次BFS?很可能会搜索整个棋盘,不优。联想到题目中一个有点怪的约定:每组数据中棋盘是固定的,变的只是开局时空格的位置和游戏的指定棋子起始、终止的位置。方便了预处理。
然后写代码。一开始有点不专心,造成了之后调试的困难。考虑不周……我写着写着才发现要用SPFA。还有其他错误:字母打错、不必要的限制条件、改了一处错误却没改配套的地方、SPFA使用了struct Node里的dist而不是更新后的、在外面加点忘记置inq为true、初始化数组的位置不对、莫名地在每次询问后把预处理出来的cost数组清零了(是说大数据怎么一堆-1……)。手里有数据,我是对着数据调的,作弊啦。练习了一下GDB的使用。一开始,学长推荐GDB,我是拒绝的。最近发现还是挺方便,可以让程序在恰当的地方停下来,查看一些变量。要综合百家之长啊。
对于我的代码做一些说明。两个idx函数用于把多个维度压缩成一维,因为状态数比较少,就没有用哈希表。namespace pre有两个用处:一是用于预处理,修改dist数组,在main里面用这些信息计算cost;二是用于让空格走到棋子附近。发现它们的需求是相似的,就共用了一段代码。namespace search用于针对每一局来search,状态之间转移的代价来自cost。在W学长的提醒下加上了对起点终点是否重合的判断。
#include <cstdio>
#include <queue>
#include <cmath>
#include <cstring>
#include <algorithm>
using namespace std;
const int dr[] = {-1, 1, 0, 0}, dc[] = {0, 0, -1, 1}, INF = 0x3f3f3f3f;
int n, m, cost[1<<14], dist[32][32];
bool M[32][32];
// 棋子在(i, j),空格在k方向,棋子转移到p方向
inline int idx(int i, int j, int k, int p)
{
return i<<9 | j<<4 | k<<2 | p;
}
// 棋子在(i, j),空格在k方向
inline int idx(int i, int j, int k)
{
return i<<7 | j<<2 | k;
}
namespace pre {
struct Node {
int r, c;
};
// 从(r, c)到(r1, c1)周围四连通的位置,且不过(r1, c1);至多有cnt个目标点
void cal(int r, int c, int r1, int c1, int cnt)
{
memset(dist, -1, sizeof(dist));
M[r1][c1] = false;
queue<Node> Q;
Q.push((Node){r, c});
dist[r][c] = 0;
Node u;
while (!Q.empty()) {
u = Q.front();
Q.pop();
if (abs(u.r-r1) == 1 && abs(u.c-c1) == 1) {
if (--cnt == 0)
break;
}
for (int i = 0; i < 4; ++i) {
Node v = (Node){u.r+dr[i], u.c+dc[i]};
if (M[v.r][v.c] && dist[v.r][v.c] == -1) {
dist[v.r][v.c] = dist[u.r][u.c] + 1;
Q.push(v);
}
}
}
M[r1][c1] = true;
}
}
namespace search {
struct Node {
int r, c, dir;
};
int d[1<<12];
bool inq[1<<12];
int spfa(int ti, int tj, queue<Node>& Q)
{
Node u;
while (!Q.empty()) {
u = Q.front();
Q.pop();
int hu = idx(u.r, u.c, u.dir);
inq[hu] = false;
for (int k = 0; k < 4; ++k) {
Node v = (Node){u.r+dr[k], u.c+dc[k], k^1};
int c = cost[idx(u.r, u.c, u.dir, k)], hv = idx(v.r, v.c, v.dir);
if (M[v.r][v.c] && c && d[hv] > d[hu] + c) {
d[hv] = d[hu] + c;
if (!inq[hv]) {
Q.push(v);
inq[hv] = true;
}
}
}
}
int ans = INF;
for (int k = 0; k < 4; ++k)
ans = min(ans, d[idx(ti, tj, k)]);
return ans == INF ? -1 : ans;
}
}
int main()
{
int q;
scanf("%d %d %d", &n, &m, &q);
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= m; ++j) {
int x;
scanf("%d", &x);
M[i][j] = x;
}
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= m; ++j)
if (M[i][j]) // blank
for (int k = 0; k < 4; ++k) {
int i1 = i+dr[k], j1 = j+dc[k]; // 棋子
if (M[i1][j1]) {
pre::cal(i, j, i1, j1, 4);
for (int p = 0; p < 4; ++p) {
int i2 = i1+dr[p], j2 = j1+dc[p]; // 目标
if (dist[i2][j2] != -1) {
cost[idx(i1, j1, k^1, p)] = dist[i2][j2] + 1;
}
}
}
}
int ei, ej, si, sj, ti, tj;
while (q--) {
using namespace search;
scanf("%d %d %d %d %d %d", &ei, &ej, &si, &sj, &ti, &tj);
if (si == ti && sj == tj) {
puts("0");
continue;
}
pre::cal(ei, ej, si, sj, 4);
memset(d, 0x3f, sizeof(d));
queue<Node> Q;
for (int k = 0; k < 4; ++k) {
int i = si+dr[k], j = sj+dc[k];
if (dist[i][j] != -1) {
Q.push((Node){si, sj, k});
inq[idx(si, sj, k)] = true;
d[idx(si, sj, k)] = dist[i][j];
}
}
printf("%d\n", spfa(ti, tj, Q));
}
return 0;
}
体会:大胆构思,仔细实现。