版权声明:Powered By Fighter https://blog.csdn.net/qq_30115697/article/details/89392369
AK 提高试炼场祭
题解
这是一道神仙题
首先发现爆搜有70分!!!要是在考场上那肯定直接暴力啊!
然而还是得写正解。
首先,爆搜慢是因为有很多无用状态(比如空格在乱跑但指定棋子并没有移动)。所以我们要剪掉这些状态。
我们考虑只搜有用的状态。也就是说我们强制让空格处于棋子的四周,这个时候就会出现两种移动的情况:
- 空格绕棋子四周走。
- 空格与棋子互换位置。
对于第一种情况,我们发现可以通过bfs求出空格到达下一个位置的时间(前提是不能经过棋子所在的位置)。第二种情况就更简单一些,时间为1。
再说一下状态的组成:状态=当前棋子位置+空格对于棋子的相对方向(表示为 )。
于是我们发现状态之间是可以通过上面两种方式进行转移,并且如果我们把得到的时间作为权值,并在状态之间连边,就会形成一张无向图!!于是可以愉快地跑最短路!!而且我们发现要求的终点位置是确定的,我们只需要枚举空格位于哪个方向就可以得到终点状态最小值。
还有一些细节问题:
- 如何表示状态:我们把棋盘上的每一个位置先顺序标号,再加4个方向编号0~3,表示空格位于当前位置的哪个方向。于是可以得到一个状态 的编号为
- 如何判断固定死的格子:这个很简单,我们预处理 表示状态 是否可以存在(先不考虑能否到达,仅判断越界和固定)。然后建图的时候判断即可
- 空格不一定在起始点周围:这一部分显然可以暴力求。通过一次bfs,求出空格到达起点四周所需的时间,并把这四个状态扔到spfa的队列中作为初始状态,然后在跑spfa的时候就会自动枚举。
- 对于输入的特判:如果起点与终点重合,输出0;如果起点和终点至少有一个被固定,输出-1。最后如果最短路无法到达终点状态,输出-1。
代码
这可能是我写过除了树剖以外码量数一数二的题了
#include <bits/stdc++.h>
#define MAX 5500
#define clear(x) (memset(x, 0, sizeof(x)))
#define INF 0x3f3f3f3f
using namespace std;
const int mx[] = {-1,1,0,0}, my[] = {0,0,-1,1};
int cnt, head[MAX*10], Next[MAX*100], vet[MAX*100], cost[MAX*100];
int n, m, a[55][55];
int sx, sy, tx, ty, ex, ey;
inline bool chk(int x, int y) { //检查是否能到达(x,y)
if(x<=0 || x>n || y<=0 || y>m) return false;
return a[x][y];
}
inline int get(int x, int y, int t) { //获取状态编号
return ((x-1)*m+y-1)*4+t;
}
struct node {
int x, y, step;
};
bool mark[55][55];
int bfs(int sx, int sy, int tx, int ty, int dx, int dy) { //从(sx,sy)->(tx,ty)且不经过(dx,dy)
queue<node> q;
clear(mark);
mark[sx][sy] = true;
q.push((node) {
sx, sy, 0
});
while (!q.empty()) {
node t = q.front();
q.pop();
if(t.x == tx && t.y == ty) {
return t.step;
}
for (int i = 0; i < 4; ++i) {
int u = t.x+mx[i], v = t.y+my[i];
if(!chk(u, v) || (u==dx && v==dy) || mark[u][v]) continue;
mark[u][v] = true;
q.push((node) {
u, v, t.step+1
});
}
}
return INF;
}
bool ok[55][55][5]; //(i,j)四周是否可能存在空格
void add(int x, int y, int w) {
cnt++;
Next[cnt] = head[x];
head[x] = cnt;
vet[cnt] = y;
cost[cnt] = w;
}
void init() {
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= m; ++j) {
if(!a[i][j]) continue;
for (int k = 0; k < 4; ++k) {
if(chk(i+mx[k], j+my[k])) {
ok[i][j][k] = true;
}
}
}
}
//空格绕棋子四周转
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= m; ++j) {
for (int k = 0; k < 4; ++k) {
for (int l = k+1; l < 4; ++l) {
if(!ok[i][j][k] || !ok[i][j][l]) continue;
int x = get(i, j, k), y = get(i, j, l);
int w = bfs(i+mx[k], j+my[k], i+mx[l], j+my[l], i, j);
if(w == INF) continue;
add(x, y, w), add(y, x, w);
}
}
}
}
//空格和指定棋子互换
for (int i = 1; i <= n; ++i) { //左右互换
for (int j = 1; j < m; ++j) {
if(ok[i][j][3] && ok[i][j+1][2]) {
int x = get(i,j,3), y = get(i,j+1,2);
add(x, y, 1), add(y, x, 1);
}
}
}
for (int i = 1; i < n; ++i) { //上下互换
for (int j = 1; j <= m; ++j) {
if(ok[i][j][1] && ok[i+1][j][0]) {
int x = get(i,j,1), y = get(i+1,j,0);
add(x, y, 1), add(y, x, 1);
}
}
}
}
int dis[MAX*100], vis[MAX*100];
queue<int> q;
void spfa() {
memset(dis, 0x3f, sizeof(dis));
memset(vis, 0, sizeof(vis));
//先让空格走到指定位置四周
for (int i = 0; i < 4; ++i) {
int u = sx+mx[i], v = sy+my[i];
if(!chk(u, v)) continue;
int w = bfs(ex, ey, u, v, sx, sy);
if(w == INF) continue;
int id = get(sx, sy, i);
dis[id] = w;
q.push(id);
vis[id] = true;
}
//spfa
while(!q.empty()) {
int t = q.front();
q.pop();
vis[t] = false;
for (int i = head[t]; i; i = Next[i]) {
int v = vet[i];
if(dis[v] > dis[t]+cost[i]) {
dis[v] = dis[t]+cost[i];
if(!vis[v]) {
vis[v] = true;
q.push(v);
}
}
}
}
int ans = INF;
for (int i = 0; i < 4; ++i) {
ans = min(ans, dis[get(tx, ty, i)]);
}
if(ans == INF) puts("-1");
else printf("%d\n", ans);
}
int main() {
int q;
cin >> n >> m >> q;
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= m; ++j) {
scanf("%d", &a[i][j]);
}
}
init();
while (q--) {
scanf("%d%d%d%d%d%d", &ex, &ey, &sx, &sy, &tx, &ty);
if(sx == tx && sy == ty) puts("0");
else if(!a[tx][ty] || !a[sx][sy]) puts("-1");
else {
spfa();
}
}
return 0;
}
总结
这道题的难点在于他别出心裁SB的把状态抽出来建图,这种做法十分的罕见。
文章内有彩蛋哦