Conclusion---GMOJ.3980推箱子(图模型转化,tarjan)

Conclusion—GMOJ.3980推箱子

Description

有一个“推箱子”游戏:给出一个n * m的地图,有的位置是障碍—用“#”表示;有的位置为空地—“.”;以及一个目标位置—“X”。地图中会有一个人和一个箱子。人可以推着箱子前进。问有多少种安排 人和箱子初始位置 的方案可以使箱子被推到目标位置。
n , m ≤ 1000 n,m \leq 1000 n,m1000

Solution

考虑如何搜索:
发现终点状态已经被确定,所以直接从终点往回拉,看能到哪里。
但如果人的位置状态 x,y 也要记下的话,状态数就太多了。

可以发现人的移动只有两种情况—

  1. 推动箱子
  2. 走一段空地后绕到箱子的另一方向

其中2的目的还是走到箱子旁边。于是可以发现只有记箱子的位置及人在箱子的哪个方向是有意义的。
现在状态数S较小了。考虑转移—

  1. 转移显然
  2. 考虑把每个位置当作点,相邻位置连边。人能从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

  1. 这样地图的搜索一般可以先把位置抽象为点,利用图的算法解决问题。
  2. 仔细分析游戏的本质,拆分为不同模块,往往能很好地简化问题,帮助解题。不要怕难。
  3. 后面的博客会更重视 ‘Postscript’ 部分,少放代码,少注意blog美观度。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值