1 条题解

  • 0
    @ 2026-5-7 23:34:52

    蒟蒻过的第一道黑体耶(

    题意

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

    给定箱子、人的初始坐标和地图和若干次形如 (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) 且不影响箱子。显然当前图中存在这条路径。

    但如果地图长这样:

    0 1 2 3 4 5
    1 # . . # #
    2 # . . # #
    3 . . A B .
    4 # # . # #
    5 # # . # #
    

    如果 A 想从 B 的右边把箱子往左推,则没有不经过箱子的路径使 A 到达 (5,3)(5,3)

    此时,如果我们把每个不是墙的坐标点看成点,相邻的点连一条双向边,则:如果 AB 相邻,则想绕到 B 的另一面去推动箱子而不使箱子移动,当且仅当这两个点属于同一个点双中

    很好理解:对于第一张地图,使 A 到达 (3,1)(3,1) 的路径中有一条经过箱子的路径:

    (3,3)(3,2)(3,1)(3,3)\to(3,2)\to(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)(3,3)(3,1)(3,1) 中有至少两条不经过重复边的路径,所以 (3,3)(3,3)(3,1)(3,1) 属于同一个点双。此时即使有箱子阻挡(即一条路径被阻断),Bessie 也能够绕到 (3,1)(3,1) 上(找到另一条路径)把箱子往下推。这满足点双的定义。

    所以我们就可以在原图上求出每个点所属的点双。注意割点可能会被多个点双包含。

    考虑到我们不关心 Bessie 的具体移动,而只有 Bessie 在靠近到箱子的四周时才会对箱子的位置造成影响,所以判断状态是否已经入过队,我们只需记录箱子的坐标和 Bessie 在箱子的哪一面即可。对于开始时 Bessie 不在箱子旁边,可以预处理出 Bessie 在不经过箱子的情况下,能到箱子上下左右的哪里。

    实现

    一开始求点双的过程使用 Tarjan,注意因为割点可能会被多个点双包含,用一个 vector 记录点双编号即可。

    预处理 Bessie 能到箱子的四周的哪里时,DfsBfs 都可以,注意不能经过箱子。

    最后 Bfs 的时候,状态 {x,y,way}\{x,y,way\} 表示箱子在 (x,y)(x,y) 上,Bessie 在箱子的第 wayway 面旁边(比如说 way=0way=0 就是上面,way=1way=1 就是左边,但 0,20,21,31,3 的方向要相反,后面会说)。

    假设现在箱子在 (ubx,uby)(ubx,uby) 上,Bessie(ucx,ucy)(ucx,ucy) 上,尝试让箱子移动到相邻的格子 (vbx,vby)(vbx,vby),则此时 Bessie 应移到 (vbx,vby)(vbx,vby) 的对面 (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;
    }
    

    恭喜你解决了一道黑题:)

    Update

    修了一下描述中的错误,感谢管理审核:)

    • 1

    信息

    ID
    6831
    时间
    1000ms
    内存
    512MiB
    难度
    10
    标签
    递交数
    1
    已通过
    1
    上传者