Conclusion—GMOJ.3980推箱子
Description
有一个“推箱子”游戏:给出一个n * m的地图,有的位置是障碍—用“#”表示;有的位置为空地—“.”;以及一个目标位置—“X”。地图中会有一个人和一个箱子。人可以推着箱子前进。问有多少种安排 人和箱子初始位置 的方案可以使箱子被推到目标位置。
n , m ≤ 1000 n,m \leq 1000 n,m≤1000
Solution
考虑如何搜索:
发现终点状态已经被确定,所以直接从终点往回拉,看能到哪里。
但如果人的位置状态 x,y 也要记下的话,状态数就太多了。
可以发现人的移动只有两种情况—
- 推动箱子
- 走一段空地后绕到箱子的另一方向
其中2的目的还是走到箱子旁边。于是可以发现只有记箱子的位置及人在箱子的哪个方向是有意义的。
现在状态数S较小了。考虑转移—
- 转移显然
- 考虑把每个位置当作点,相邻位置连边。人能从A走到B(A,B与当前箱子相邻),当且仅当A,B在一个点双里—因为必有一条 A->箱子->B 的路径,且箱子是不能走的。
现在求出了S个状态(人紧挨箱子)是否可行。但我们的初始状态中 人是可以不挨着箱子的,所以,对于每个状态,还要计算有多少位置 可以不经过箱子 到达这个合法位置。
继续利用我们求出的点双(建出圆方树)—若人和箱子不在一个点双,直接加上人所在子树的siz;否则,加上 割掉箱子与方点边后 人所在联通块大小(也用siz算)。
//最丑代码
//多处可常数优化,勿作挑剔
#include<cstdio>
#define N 1005
#define M N*N*2
#define ll long long
using namespace std;
char s[N];
int t[4][2];
int n,m,sx,sy,nm;
int a[N][N];
int key(int x,int y){return (x-1)*m+y;}
int lk[M],fa[M],rt,siz[M],dep[M];
struct edge{
int to,nx;
}e[M*8];int tot;
void add(int u,int v)
{
e[++tot].to=v;e[tot].nx=lk[u];lk[u]=tot;
}
bool vis[M];
int cnt,dfn[M],low[M],od[M];
int q[M],T;
int top[M];
void dfs(int x)
{
dep[x]=dep[fa[x]]+1;
vis[x]=1;
low[x]=dfn[x]=++cnt;
top[x]=q[++T]=x;
for (int i=lk[x];i;i=e[i].nx)
if (!vis[e[i].to])
{
fa[e[i].to]=x;
dfs(e[i].to);
if (low[e[i].to]<low[x])
low[x]=low[e[i].to];
if (low[e[i].to]==dfn[x])
{
while (q[T]!=e[i].to)top[q[T--]]=x;
--T;
top[e[i].to]=x;
}else if (low[e[i].to]>dfn[x])
{while (q[T]!=e[i].to)--T;--T;}
}else if (e[i].to!=fa[x]&&dfn[e[i].to]<low[x])low[x]=dfn[e[i].to];
}
bool vs[N][N][4],ab[N][N][4][4];
struct que{
int x,y,t;
}B[M*5];
int hd=1,tl=0;
inline void push(int x,int y,int t)
{
vs[x][y][t]=1;
++tl;
B[tl].x=x;
B[tl].y=y;
B[tl].t=t;
}
void bfs()
{
for (int i=0;i<4;++i)
if (!a[sx+t[i][0]][sy+t[i][1]])push(sx,sy,i);
while (hd<=tl)
{
int tt=B[hd].t;
int xx=B[hd].x+t[tt][0],
yy=B[hd].y+t[tt][1];
if (!a[xx+t[tt][0]][yy+t[tt][1]]
&&!vs[xx][yy][tt])
push(xx,yy,tt);
for (int i=0;i<4;++i)
if (ab[B[hd].x][B[hd].y][tt][i]&&
!vs[B[hd].x][B[hd].y][i])
push(B[hd].x,B[hd].y,i);
++hd;
}
}
void init()
{
t[0][0]=1;
t[0][1]=0;
t[1][0]=-1;
t[1][1]=0;
t[2][0]=0;
t[2][1]=1;
t[3][0]=0;
t[3][1]=-1;
scanf("%d%d\n",&n,&m);nm=n*m;
for (int i=1;i<=n;++i)a[i][0]=a[i][m+1]=1;
for (int i=1;i<=m;++i)a[0][i]=a[n+1][i]=1;
for (int i=1;i<=n;++i)
{
scanf("%s\n",s+1);
for (int j=1;j<=m;++j)
if (s[j]=='#')a[i][j]=1;
else if (s[j]=='X')sx=i,sy=j;
}
rt=key(sx,sy);
for (int i=1;i<=n;++i)
for (int j=1;j<=m;++j)
if (!a[i][j])
for (int k=0;k<4;++k)
if (!a[i+t[k][0]][j+t[k][1]])
add(key(i,j),key(i+t[k][0],j+t[k][1]));
}
int qq[10];
void dfs2(int x)
{
dep[x]=dep[fa[x]]+1;
if (x<=nm)siz[x]=1;
for (int i=lk[x];i;i=e[i].nx)
{
fa[e[i].to]=x;
dfs2(e[i].to);
siz[x]+=siz[e[i].to];
}
}
int main()
{
init();
dfs(rt);
for (int i=1;i<=n;++i)
for (int j=1;j<=m;++j)
if (vis[key(i,j)])
{
for (int k=0;k<3;++k)
for (int l=k+1;l<4;++l)
{
int A=key(i+t[k][0],j+t[k][1])
,B=key(i+t[l][0],j+t[l][1]);
if (top[A]==top[B]||
top[A]==B||
top[B]==A)
ab[i][j][k][l]=
ab[i][j][l][k]=1;
}
}
bfs();
for (int i=1;i<=nm;++i)
if (vis[i])lk[i]=0;
tot=0;
int sum=0;
for (int i=1;i<=nm;++i)
if (vis[i])
{
++sum;
add(i,i+nm);
if (top[i]!=i)add(top[i]+nm,i);
else add(fa[i],i);
}
dfs2(rt);
ll ans=0;
for (int i=1;i<=n;++i)
for (int j=1,p;j<=m;++j)
if (vis[key(i,j)]&&(i!=sx||j!=sy))
{
p=key(i,j);
bool bz=0,bz2=0;
for (int k=0,x;k<4;++k)
if (vs[i][j][k])
{
x=key(i+t[k][0],j+t[k][1]);
if (fa[x]==p)ans+=siz[x];
else if (dep[x]>dep[p])
{
if (!bz)
{
ans+=siz[p+nm];
bz=1;
}
}else
{
if (fa[p]==x)
{
ans+=sum-1-siz[p];
}else
{
if (!bz2)
{
ans+=sum-1-siz[p];
bz2=1;
}
}
}
}
}
printf("%lld\n",ans);
return 0;
}
Postscript
- 这样地图的搜索一般可以先把位置抽象为点,利用图的算法解决问题。
- 仔细分析游戏的本质,拆分为不同模块,往往能很好地简化问题,帮助解题。不要怕难。
- 后面的博客会更重视 ‘Postscript’ 部分,少放代码,少注意blog美观度。