P4082 [USACO17DEC] Push a Box P 题解

蒟蒻过的第一道黑体耶(

题意

一个人能推动地图中的箱子,当且仅当他在箱子旁边,且箱子在按照人向箱子移动的方向移动一格后不会卡墙里或卡进后室卡出地图外。

给定箱子、人的初始坐标和地图和若干次形如 ( 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 # # . # #

其中 ABessieB 为箱子。如果 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)

此时,如果我们把每个不是墙的坐标点看成点,相邻的点连一条双向边,则:如果 AB 相邻,则想绕到 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 能到箱子的四周的哪里时,DfsBfs 都可以,注意不能经过箱子。

最后 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;
}

恭喜你解决了一道黑题😃

  • 15
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值