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—B,C—D。空格在 X。 - 目标状态(
tb):要求改成 A—C,B—D。
dfs 现在在环外的 X 点,它发现邻居 A 的边是坏的(ta[A]=B 但 tb[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。
- 结果:
- X 和 A 连成了新积木(
ta[X]=A, ta[A]=X)。 - B 被挤出来,变成新的空格。
- X 和 A 连成了新积木(
- 此时空格位置: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)。
- 移动这块积木,结果:
- B 和 D 连成新积木(
ta[B]=D, ta[D]=B)—— 目标边 B—D 被修好了! - C 被挤出来,变成新的空格。
- B 和 D 连成新积木(
- 此时空格位置: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)。
- 移动这块积木,结果:
- C 和 A 连成新积木(
ta[C]=A, ta[A]=C)—— 目标边 A—C 被修好了! - X 被挤出来,变成新的空格。
- C 和 A 连成新积木(
- 此时空格位置: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 点。

浙公网安备 33010602011771号