蒟蒻过的第一道黑体耶(
题意
一个人能推动地图中的箱子,当且仅当他在箱子旁边,且箱子在按照人向箱子移动的方向移动一格后不会卡墙里或
卡进后室卡出地图外。给定箱子、人的初始坐标和地图和若干次形如 ( x , y ) (x,y) (x,y) 的询问,求箱子能否被移动到 ( x , y ) (x,y) (x,y) 这个坐标上。
询问与询问互不影响。
解法
很显然重点落在箱子而不是人的移动上,但箱子的移动依赖于人的移动,人的移动又受到箱子的阻挡。分析一下箱子对 Bessie
移动的影响。
假设地图长这样:
0 1 2 3 4 5
1 # . . # #
2 # . B # #
3 . . A . .
4 # # . # #
5 # # . # #
其中 A
为 Bessie
,B
为箱子。如果 A
想从 B
的上面把箱子往下推,则必须有一条不经过箱子的路径使得 A
到达
(
3
,
1
)
(3,1)
(3,1) 且不影响箱子。显然当前图中存在这条路径。
但如果地图长这样:
0 1 2 3 4 5
1 # . . # #
2 # . . # #
3 . . A B .
4 # # . # #
5 # # . # #
如果 A
想从 B
的左边把箱子往右推,则没有不经过箱子的路径使 A
到达
(
5
,
3
)
(5,3)
(5,3)。
此时,如果我们把每个不是墙的坐标点看成点,相邻的点连一条双向边,则:如果 A
与 B
相邻,则想绕到 B
的另一面去推动箱子而不使箱子移动,当且仅当这两个点属于同一个点双中。
很好理解:对于第一张地图,使 A
到达
(
3
,
1
)
(3,1)
(3,1) 的路径中有一条经过箱子的路径:
(
3
,
3
)
→
(
3
,
2
)
→
(
3
,
1
)
(3,3)\to(3,2)\to(3,1)
(3,3)→(3,2)→(3,1)
也有一条不经过箱子的路径:
(
3
,
3
)
→
(
2
,
3
)
→
(
2
,
2
)
→
(
2
,
1
)
→
(
3
,
1
)
(3,3)\to(2,3)\to(2,2)\to(2,1)\to(3,1)
(3,3)→(2,3)→(2,2)→(2,1)→(3,1)
显然由于
(
3
,
3
)
(3,3)
(3,3) 和
(
3
,
1
)
(3,1)
(3,1) 中有至少两条不经过重复边的路径,所以
(
3
,
3
)
(3,3)
(3,3) 和
(
3
,
1
)
(3,1)
(3,1) 属于同一个点双。此时即使有箱子阻挡(即一条路径被阻断),Bessie
也能够绕到
(
3
,
1
)
(3,1)
(3,1) 上(找到另一条路径)把箱子往下推。这满足点双的定义。
所以我们就可以在原图上求出每个点所属的点双。注意割点可能会被多个点双包含。
考虑到我们不关心 Bessie
的具体移动,而只有 Bessie
在靠近到箱子的四周时才会对箱子的位置造成影响,所以判断状态是否已经入过队,我们只需记录箱子的坐标和 Bessie
在箱子的哪一面即可。对于开始时 Bessie
不在箱子旁边,可以预处理出 Bessie
在不经过箱子的情况下,能到箱子上下左右的哪里。
实现
一开始求点双的过程使用 Tarjan
,注意因为割点可能会被多个点双包含,用一个 vector
记录点双编号即可。
预处理 Bessie
能到箱子的四周的哪里时,Dfs
和 Bfs
都可以,注意不能经过箱子。
最后 Bfs
的时候,状态
{
x
,
y
,
w
a
y
}
\{x,y,way\}
{x,y,way} 表示箱子在
(
x
,
y
)
(x,y)
(x,y) 上,Bessie
在箱子的第
w
a
y
way
way 面旁边(比如说
w
a
y
=
0
way=0
way=0 就是上面,
w
a
y
=
1
way=1
way=1 就是左边,但
0
,
2
0,2
0,2 和
1
,
3
1,3
1,3 的方向要相反,后面会说)。
假设现在箱子在
(
u
b
x
,
u
b
y
)
(ubx,uby)
(ubx,uby) 上,Bessie
在
(
u
c
x
,
u
c
y
)
(ucx,ucy)
(ucx,ucy) 上,尝试让箱子移动到相邻的格子
(
v
b
x
,
v
b
y
)
(vbx,vby)
(vbx,vby),则此时 Bessie
应移到
(
v
b
x
,
v
b
y
)
(vbx,vby)
(vbx,vby) 的对面
(
v
c
x
,
v
c
y
)
(vcx,vcy)
(vcx,vcy) 上(比如目标点在当前箱子上面,那 Bessie
需要移到箱子的下面去)。判断两者是否被同一个点双包含即可。当然也有 Bessie
无需移动的情况,判断两个坐标是否相同即可。
因为码量比较大,代码中会有详细的注释。
代码
// 先Tarjan求出点双
// 然后dfs判断人能到哪里
// 最后bfs求解
#include<bits/stdc++.h>
#define mk make_pair
using namespace std;
const int maxn = 1505;
// 行列上移动的方向,注意要和后面的 dfs 和 bfs 对应。
const int WayX[] = {-1,0,1,0};
const int WayY[] = {0,-1,0,1};
int n,m;
char MP[maxn][maxn]; // 原图
int dfn[maxn][maxn],low[maxn][maxn],clo;
vector<int> b[maxn][maxn]; // 每个点被哪些点双包含
int tot; // 存当前点双的编号
pair<int,int> st[maxn * maxn + maxn]; // 注意栈要开 n*n 大小
int top;
int Cx,Cy,Bx,By; // (Cx,Cy) 为 Bessie 的坐标,(Bx,By) 为箱子的坐标。
bool check(int x,int y) { // 判断是否越界/卡墙里
return x >= 1 && x <= n && y >= 1 && y <= m && MP[x][y] != '#';
}
// 暴力判断两点是否被同一点双包含
bool checkSame(int ux,int uy,int vx,int vy) {
for (auto x : b[ux][uy])
for (auto y : b[vx][vy])
if (x == y) return true;
return false;
}
// 求点双
void Tarjan(int ux,int uy,int fx,int fy) {
dfn[ux][uy] = low[ux][uy] = ++ clo;
st[++ top] = mk(ux,uy);
for (int w = 0;w < 4;w ++) {
int vx = ux + WayX[w], vy = uy + WayY[w]; // 相邻点
if (!check(vx,vy)) continue;
if (!dfn[vx][vy]) {
Tarjan(vx,vy,ux,uy);
low[ux][uy] = min(low[ux][uy],low[vx][vy]);
if (low[vx][vy] >= dfn[ux][uy]) { // 发现割点 (ux,uy)
b[ux][uy].push_back(++ tot);
do {
int x = st[top].first, y = st[top].second;
b[x][y].push_back(tot);
} while (st[top --] != mk(vx,vy));
}
} else if (vx != fx || vy != fy)
low[ux][uy] = min(low[ux][uy],dfn[vx][vy]);
}
}
bool CanMove[4],DfsVis[maxn][maxn];
// CanMove 分别对应一开始的 WayX 和 WayY
void dfs(int ux,int uy) { // 尝试让 Bessie 去靠近箱子的四周
if (DfsVis[ux][uy] || (ux == Bx && uy == By)) return ;
// 更新 CanMove
CanMove[0] |= (ux == Bx - 1 && uy == By);
CanMove[1] |= (ux == Bx && uy == By - 1);
CanMove[2] |= (ux == Bx + 1 && uy == By);
CanMove[3] |= (ux == Bx && uy == By + 1);
DfsVis[ux][uy] = true;
for (int w = 0;w < 4;w ++) {
int vx = ux + WayX[w], vy = uy + WayY[w];
if (check(vx,vy)) dfs(vx,vy);
}
}
int Q;
bool BfsVis[maxn][maxn][4]; // 存状态,上文中提到过。
bool Ans[maxn][maxn]; // 最终答案
// Bfs 队列,前一个 pair 存 Bessie 的坐标,后一个 pair 存箱子的坐标。
queue<pair<pair<int,int>,pair<int,int> > >Que;
void Bfs() {
pair<int,int> Box = mk(Bx,By);
// 对处理出的结果往队列里扔初始的状态
for (int w = 0;w < 4;w ++) // 也要和 WayX,WayY 对应
if (BfsVis[Bx][By][w] = CanMove[w]) // 记录状态已入过队
Que.push(mk(mk(Bx + WayX[w],By + WayY[w]),Box));
// for (int w = 0;w < 4;w ++)
// cout << CanMove[w] << '\n';
while (!Que.empty()) {
auto Top = Que.front(); Que.pop();
int ucx = Top.first.first, ucy = Top.first.second; // Bessie 的坐标
int ubx = Top.second.first, uby = Top.second.second; // 箱子的坐标
for (int w = 0;w < 4;w ++) {
int vbx = ubx + WayX[w], vby = uby + WayY[w]; // 箱子的目标位置
int vcx = ubx + WayX[w ^ 2], vcy = uby + WayY[w ^ 2]; // Bessie 的目标位置
// 上文中提到 way 和 way + 2 要对应就是因为这个,可以通过与二异或求出对面方向。
if (!check(vbx,vby) || BfsVis[vbx][vby][w ^ 2]) continue; // 状态已被访问或不合法
if (vcx == ucx && vcy == ucy || checkSame(vcx,vcy,ucx,ucy)) { // 可以满足
// 此时 Bessie 由于推了箱子而到了箱子之前的位置 (ubx,uby)。
// 箱子被推到了 (vbx,vby) 上。
BfsVis[vbx][vby][w ^ 2] = true;
Que.push(mk(mk(ubx,uby),mk(vbx,vby)));
}
}
}
for (int i = 1;i <= n;i ++)
for (int j = 1;j <= m;j ++) {
Ans[i][j] = false;
for (int k = 0;k < 4;k ++)
Ans[i][j] |= BfsVis[i][j][k]; // 统计答案
}
}
int main() {
scanf("%d%d%d",&n,&m,&Q);
for (int i = 1;i <= n;i ++) {
scanf("%s",MP[i] + 1);
for (int j = 1;j <= m;j ++) {
if (MP[i][j] == 'A') Cx = i, Cy = j;
if (MP[i][j] == 'B') Bx = i, By = j;
}
}
Tarjan(Cx,Cy,0,0);
// cout << tot << '\n';
if (!dfn[Bx][By]) { // 本身 Bessie 就摸不到箱子,特判掉。
for (int i = 1,x,y;i <= Q;i ++) {
scanf("%d%d",&x,&y);
if (x == Bx && y == By) puts("YES");
else puts("NO");
}
return 0;
}
dfs(Cx,Cy); Bfs();
for (int i = 1,x,y;i <= Q;i ++) {
scanf("%d%d",&x,&y);
if (Ans[x][y]) puts("YES");
else puts("NO");
}
return 0;
}
恭喜你解决了一道黑题😃