把博客园图标替换成自己的图标
把博客园图标替换成自己的图标end

P5372 [SNOI2019] 积木 题目分析

题目链接

分析

很有意思,赛时没有想到更加简洁的做法。

我们只用关注空白方块,然后我们可以把矩形看作边

定义:

  • < 当前位置的边向右一格。
  • > 向左。
  • n 向下。
  • u 向上。
  • o 没有定义。

这样我们一个矩形所占的两个位置的有向边是相对的(于是这两者之间连的边可以看作无向边)。

我们对目标的地图搞一下这个,对初始地图搞一下这个。

把那些没有用的格子(指初始和目标完全相同的),然后对剩下的格子(点)连边,就可以发现:

  • 若目标地图与初始地图的空白部分相同,那么两个空白部分的度数为 \(1\),其他的均为 \(2\),那么这个图是一个链+一些环。
  • 若目标地图与出事地图的空白部分不相同,那么两个空白部分的度数为 \(1\),其他的均为 \(2\),那么这个图还是一个链+一些环。

因此这个图一定是一条链+一些环。

而正好我们需要从初始的空白部分走到目标的空白部分,因此只需走完这个路径就可以了。

那么怎么走呢?

对于:

S(开始)-A-B-C-D-B-E-F-T(结束)

\(ta_{i,j}\) 表示初始图 \((i,j)\) 的方向,\(tb_{i,j}\) 表示 \((i,j)\) 的方向。

因为一遍可以搞定这一次循环,而且当你按照 \(tb_{i,j}\) 这个方向操作的时候,你可以发现操作后他能够贴合目标图,并且还能够行走到下下个点。

然后我们可以先修改这个链,再递归遍历整个图修改剩下的环。

代码

时间复杂度 \(\mathcal{O}(nm)\)

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <stdlib.h>
#include <vector>
//#define int long long
#define N 2005
using namespace std;
int n,m,fxy[4][2] = {{0,-1},{-1,0},{0,1},{1,0}};//LURD
char dir[4] = {'L','U','R','D'};
char a[N][N],b[N][N];
int ta[N][N],tb[N][N],sx,sy,ex,ey;
string ans;
bool vis[N][N];
void read_map(char map[N][N],int target[N][N],int &x,int &y) {
	for (int i = 1;i <= n;i ++)
		for (int j = 1;j <= m;j ++) {
			char c = map[i][j];
			if (c == 'o') x = i,y = j,target[i][j] = -1;
			else if (c == '>') target[i][j] = 0;
			else if (c == '<') target[i][j] = 2;
			else if (c == 'n') target[i][j] = 3;
			else target[i][j] = 1;
		}
}
void goone(int k) {//走一步带来的改变
	ans.push_back(dir[k]);
	int xi = sx + fxy[k][0],yi = sy + fxy[k][1];
	int xj = xi + fxy[ta[xi][yi]][0],yj = yi + fxy[ta[xi][yi]][1];
	ta[xj][yj] = -1,ta[xi][yi] = (k + 2) % 4,ta[sx][sy] = k;
	sx = xj,sy = yj;
}
void walk(int x,int y) {
	while(sx != x || sy != y) goone(tb[sx][sy]);
}
void dfs(int x,int y) {
	if (vis[x][y]) return; 
	vis[x][y] = 1;
	for (int i = 0;i < 4;i ++) {
        int xx = x + fxy[i][0],yy = y + fxy[i][1];//一端
        if (xx < 1 || xx > n || yy < 1 || yy > m || vis[xx][yy]) continue;
        int mx = xx + fxy[tb[xx][yy]][0],my = yy + fxy[tb[xx][yy]][1];//另一端
        if (ta[xx][yy] != tb[xx][yy]) {//是环上面的
            goone(i);//进入环
            walk(mx,my);//把环修改,到原来目标(xx,yy)的另一端
            goone(tb[mx][my]);//闭环,处理这一个矩形
        }
        goone(i);//有无异常都继续
        vis[xx][yy] = true;
        dfs(mx, my);//常规
        goone(tb[mx][my]);//回溯:从另一端回来。
    }
}
signed main(){
	cin >> n >> m;
	for (int i = 1;i <= n;i ++) scanf("%s",a[i] + 1);
	for (int i = 1;i <= n;i ++) scanf("%s",b[i] + 1);
	read_map(a,ta,sx,sy),read_map(b,tb,ex,ey);
	walk(ex,ey);//先修改链,防止出现dfs被链上的点干扰
	dfs(ex,ey);
	cout << ans;
	return 0;
}

对于 dfs 部分举个例子。

第一步:搭好舞台(4个点的环)

假设环上有 A、B、C、D 四个格子,空格在环外的 X

  • 当前状态(ta:积木是 A—BC—D。空格在 X。
  • 目标状态(tb:要求改成 A—CB—D

dfs 现在在环外的 X 点,它发现邻居 A 的边是坏的(ta[A]=Btb[A]=C)。


第二步:执行“三步走”代码(这就是你 AC 代码里的逻辑)

if (ta[xx][yy] != tb[xx][yy]) {  // 发现 A 是坏边
    goone(i);              // ① 踏入坏边
    walk(mx, my);          // ② 沿目标滑行(mx,my 是 A 的目标搭档 C)
    goone(tb[mx][my]);     // ③ 再走一步,闭环
}

我们一步步拆开看物理世界发生了什么:

① 执行 goone(i)(空格踏入 A)

  • 当前空格:X。
  • 动作:把 A—B 这块积木推进空格 X。
  • 结果
    • XA 连成了新积木(ta[X]=A, ta[A]=X)。
    • B 被挤出来,变成新的空格。
  • 此时空格位置B
  • 此时地图:X—A(新边),C—D(旧边还在),空格在 B。

② 执行 walk(mx, my)(沿目标走到 C)

  • 当前空格:B。
  • walk 的逻辑是:查 tb[B]tb[B] 指向 D(因为目标是 B—D)。
  • walk 内部执行 goone(tb[B]):即把 D 旁边的积木推进空格 B。
    • B 旁边是 D,D 现在和 C 在一起(积木 C—D)。
    • 移动这块积木,结果:
      • BD 连成新积木(ta[B]=D, ta[D]=B)—— 目标边 B—D 被修好了!
      • C 被挤出来,变成新的空格。
  • 此时空格位置C
  • walk 的目标就是走到 C,循环结束。
  • 此时地图:X—A(旧边),B—D(已修正),空格在 C。

③ 执行 goone(tb[mx][my])(再走一步,闭环)

  • 注意这里的参数:tb[mx][my],即 tb[C]
  • 查目标:tb[C] 指向 A(因为目标是 A—C)。
  • 动作:把 A 旁边的积木推进空格 C。
    • C 旁边是 A,A 现在和 X 在一起(积木 X—A)。
    • 移动这块积木,结果:
      • CA 连成新积木(ta[C]=A, ta[A]=C)—— 目标边 A—C 被修好了!
      • X 被挤出来,变成新的空格。
  • 此时空格位置X(回到了最初的起点!)。

第三步:看完整的三步走成果

执行步骤 代码行 空格移动轨迹 修正了哪条边 拆掉了哪条旧边
goone(i) X → B(经过A) 无(只是拆旧边) A—B
walk(mx, my) B → C(经过D) B—D C—D
goone(tb[mx][my]) C → X(经过A) A—C X—A(临时边)

最终状态

  • 新积木:A—C ✅,B—D ✅。
  • 旧积木 A—B 和 C—D 已被彻底替换。
  • 空格回到了 X(环外的安全位置)。

场景重现:dfs 发现 A-B 是条坏边

此时 dfs 开始了它的“三步走 + 秋千”操作:

1. 第一次“发射”(if 里的三步走)

  • 动作dfs 把坏边 A-B 扯断,用三步走把它改成了 A-C
  • 结果:环上修好了一段(A 连上了 C),但 dfs 发现自己被弹回了 S 点(起点)。
  • 此时状态:虽然修好了 A-C,但 dfs 还没检查 C 点呢!它不能卡在起点不动。

2. 第二次“荡秋千”—— goone(i)(进入)

为了去检查 C 点,dfs 执行了 goone(i)(向原来 A 的方向走)。

  • 物理发生了啥? 因为当前空格在 S,S 旁边是 A,而 A 刚刚连上了 C。goone(i) 会把这块 A-C 积木推进 S。
  • 结果:A 和 S 连在了一起,空格被弹射到了 C 点
  • 关键点:这一步暂时破坏了刚刚修好的 A-C(因为 A 现在连 S 了,C 变成了空格)。但这没关系,dfs 只是“借用”这条边当秋千,把自己荡到 C 点去。

3. 深入检查(dfs(mx, my)

  • 空格在 C 点,dfs 开始检查 C 点的边(比如 C-D 是不是坏的)。
  • 如果坏,dfs 就会在 C 点这里再次执行“三步走”,把 C-D 修成 B-D,并把空格弹到 B 点……以此类推。
  • 就这样,dfs 像一只爬虫,通过一次次“荡秋千”,在环上前进,把所有坏边都修好了。

4. 第三次“荡回来”—— goone(tb[mx][my])(回溯)

dfs 把环后面那半圈(C-D 那部分)彻底修好后,它发现自己身处某个位置,为了回到当初的“起点” A 点,它执行回溯(goone(tb[mx][my]))。

  • 因为上一步 dfs 在检查 B-D 时,把空格留在了某个位置。这个回溯操作会反向荡秋千,把空格从当前位置弹回 A 点(或者 S 点)。
  • 更妙的是:当空格弹回来时,它把原来借用的那条边 A-C 重新“缝”上了!此时 A 重新连回 C,并且空格回到了 S 点。
posted @ 2026-07-16 21:16  high_skyy  阅读(3)  评论(0)    收藏  举报
动态线条
动态线条end
浏览器标题切换
浏览器标题切换end
💬 加载中……