AT CODE FESTIVAL 2016 Final J 题解

题目

妙妙题!#

简要题意:给定一个 n,有一个 n×n 的网格图。

4n 个方向 U/D/L/R1,2,,n,如下图:

对于每个方向,有个限制:数 x。你可以进行 x 次推棋子,把一个棋子放到当前方向指向的第一格,然后如果原来第一格有棋子,把它放到第二格,如果原来第二格有棋子,把它放到第三格 (就是类似推箱子的过程)

如果最终会推出棋盘,那么这次操作不能进行。

输入 U/D/L/R1,2,,n 表示 4n 个方向的操作数限制。

4n 个方向的 x 总和 x=n2

请构造一个合法的推棋子方案(即按顺序输入方向),或报告无解(输出 NO)。

可以参考原题样例理解。


考虑推棋子这件事对全局的影响太大,转换成把这颗棋子放到当前方向的第一格空位,这样就好做多了。

把每个格子向它上下左右 4 个方向连边(上下左右分别连向它),流量为 1

S4n 个方向连边,流量为这个方向的推次数限制。把 n×n 个格子向 T 连流量为 1 的边。

跑网络流,发现合法方案一定是满流,即流量为 n2

同时我们也可以求出每个格子是由哪个方向放进去的(很显然,实在不会看代码)

发现这是比二分图匹配弱的(感性理解一下),参照二分图匹配来分析复杂度是 O(|E||V|)=O(n2n2)=O(n3)

PS:这一步有不用网络流的做法,但我无法理解,于是放了网络流的做法,大家可以对照那个代码自行研究。

网络流部分代码(只放连边和判断方向,dinic 相信大家都会了):

	scanf("%d",&n);S=0;T=n*n+4*n+1;
	for(int i=1;i<=n;i++) scanf("%d",&U[i]),add(S,n*n+i,U[i]);
	for(int i=1;i<=n;i++) scanf("%d",&D[i]),add(S,n*n+n+i,D[i]);
	for(int i=1;i<=n;i++) scanf("%d",&L[i]),add(S,n*n+2*n+i,L[i]);
	for(int i=1;i<=n;i++) scanf("%d",&R[i]),add(S,n*n+3*n+i,R[i]);
	#define wz(i,j) (i-1)*n+j
	for(int i=1;i<=n;i++) for(int j=1;j<=n;j++)
		add(n*n+2*n+i,wz(i,j),1),add(n*n+3*n+i,wz(i,j),1),add(n*n+j,wz(i,j),1),add(n*n+n+j,wz(i,j),1),add(wz(i,j),T,1);
	//连边
	while(bfs()) memcpy(_head,head,sizeof(head)),ans+=dfs(S,1e9);
	if(ans!=n*n) return 0*puts("NO");
	for(int i=1;i<=n;i++) for(int j=1;j<=n;j++)
		for(int k=head[wz(i,j)];k;k=e[k].nex)
		{
			int to=e[k].to;
			if(to!=T&&e[k].w){a[i][j]=(to-n*n-1)/n;break;}
		}//判断方向

这还没完,我们只求出了每个格子是由哪个方向放进去的,并不知道顺序,下面解决这个问题。


考虑如果放了一个某方向的棋子,则这个棋子沿这个方向之前一定都被填满了。

考虑 dfs,对于某个格子,for 找到这个方向所有未被填的格子,然后 dfs 那个格子(如果没有就不执行),最后把这个格子填上。

伪代码:

void dfs(int x,int y)
{
	for(枚举往前格子) if(!vis[nx][ny]) dfs(nx,ny);
	vis[x][y]=1;cout<<方向<<"\n";
}

伪代码也不一定对,理解意思就行。


上面的方法如果每个格子只被 dfs 一次,那么总复杂度是 O(n2×n)=O(n3)

但是事实上并不一定在这样,会出现这种情况。

就相当于有环,这时候我们不好确定顺序,可以做如下变换:

发现这样依然是符合要求的,而且消去了环。

说一下实现方法,记一个 v 表示一个点输出了没有,记一个 V 表示这个点遍历了一次(无环)还是两次(有环),感性理解一下。然后往前 fordfs 找环,有一些细节可以看代码。

代码:

bool dfs1(int x,int y)
{
	if(V[x][y]) return V[x][y]=0,1;V[x][y]=1;
	int adx=dx[a[x][y]],ady=dy[a[x][y]],nx=x+adx,ny=y+ady,col=a[x][y];//有可能之后会更改颜色所以要先记颜色。
	while(nx>0&&ny>0&&nx<=n&&ny<=n)
	{
		if(!v[nx][ny]&&dfs1(nx,ny))
		{
			a[nx][ny]=col;//如果循环更改不记颜色会寄
			if(V[x][y]) return V[x][y]=0,1;
			return dfs1(x,y);//注意如果更改了颜色就要重新 dfs
		}
		nx+=adx;ny+=ady;
	}v[x][y]=1;//一定要最后才把这个变成1
	return 0*printf("%c%d\n",c[a[x][y]],a[x][y]<2?y:x);
}



for(int i=1;i<=n;i++) for(int j=1;j<=n;j++) dfs1(i,j);//可能还有没做完的,要多次。

势能分析#

这里先给出结论,这样做是 O(n3) 的。

下面的势能分析可能跟别人讲的都不一样,可以参考着理解。

其实势能分析你可以理解为:选取一个恰当的状态,有值 d(看做势能),这个值每减少 x 就会增加 f(x) 的复杂度(可以理解为于势能转化为动能),那么总复杂度就是 f(x)。对它估界,一般 f(x)=poly(n)×polylog(n),于是 f(x)f(x)=f(d)

比如单调队栈每减少一个数就会增加 1 的复杂度,于是线性。线段树合并线段树每少 1 个节点复杂度就会 +1 ,于是复杂度是节点数就是 O(nlogn)

来看这题。定义势能 d 为原来 n2 个格子到它自己边界(比如格子里写 L 就是左边界)的距离和,dn3。举 LD 的例子来说,这次遍历对复杂度的影响是 posLposD 的,同时 L 离自己边界的距离也减少了这个值。于是复杂度就是 d,即 O(n3) 的。

于是做完了,总复杂度 O(n3) 的。

代码:

#include<bits/stdc++.h>
#define LL long long
#define fr(x) freopen(#x".in","r",stdin);freopen(#x".out","w",stdout);
using namespace std;
const int N=305,M=N*N+4*N,dx[4]={-1,1,0,0},dy[4]={0,0,-1,1};
const char c[4]={'U','D','L','R'};
struct edge{int to,nex,w;}e[M*200];
int n,m,S,T,tot=1,head[M],_head[M],d[M],ans,U[N],D[N],L[N],R[N],a[N][N];
bool v[N][N],V[N][N];
inline void add(int u,int v,int w)
{
	e[++tot]={v,head[u],w};head[u]=tot;
	e[++tot]={u,head[v],0};head[v]=tot;
}
inline bool bfs()
{
	memset(d,0,sizeof(d));d[S]=1;queue<int>q;q.push(S);
	while(!q.empty())
	{
		int t=q.front();q.pop();
		for(int i=head[t];i;i=e[i].nex)
		{
			int to=e[i].to;
			if(!d[to]&&e[i].w>0) d[to]=d[t]+1,q.push(to);
		}
	}
	return d[T];
}
int dfs(int x,int F)
{
	if(x==T) return F;
	int tt=F;
	for(int &i=_head[x];i;i=e[i].nex)
	{
		int to=e[i].to;
		if(d[to]==d[x]+1&&e[i].w>0)
		{
			int t=dfs(to,min(tt,e[i].w));
			tt-=t;e[i].w-=t;e[i^1].w+=t;
			if(!tt) break;
		}
	}
	if(tt==F) d[x]=0;
	return F-tt;
}
bool dfs1(int x,int y)
{
	if(V[x][y]) return V[x][y]=0,1;V[x][y]=1;
	int adx=dx[a[x][y]],ady=dy[a[x][y]],nx=x+adx,ny=y+ady,col=a[x][y];
	while(nx>0&&ny>0&&nx<=n&&ny<=n)
	{
		if(!v[nx][ny]&&dfs1(nx,ny))
		{
			a[nx][ny]=col;
			if(V[x][y]) return V[x][y]=0,1;
			return dfs1(x,y);
		}
		nx+=adx;ny+=ady;
	}v[x][y]=1;
	return 0*printf("%c%d\n",c[a[x][y]],a[x][y]<2?y:x);
}
int main()
{
	scanf("%d",&n);S=0;T=n*n+4*n+1;
	for(int i=1;i<=n;i++) scanf("%d",&U[i]),add(S,n*n+i,U[i]);
	for(int i=1;i<=n;i++) scanf("%d",&D[i]),add(S,n*n+n+i,D[i]);
	for(int i=1;i<=n;i++) scanf("%d",&L[i]),add(S,n*n+2*n+i,L[i]);
	for(int i=1;i<=n;i++) scanf("%d",&R[i]),add(S,n*n+3*n+i,R[i]);
	#define wz(i,j) (i-1)*n+j
	for(int i=1;i<=n;i++) for(int j=1;j<=n;j++)
		add(n*n+2*n+i,wz(i,j),1),add(n*n+3*n+i,wz(i,j),1),add(n*n+j,wz(i,j),1),add(n*n+n+j,wz(i,j),1),add(wz(i,j),T,1);
	while(bfs()) memcpy(_head,head,sizeof(head)),ans+=dfs(S,1e9);
	if(ans!=n*n) return 0*puts("NO");
	for(int i=1;i<=n;i++) for(int j=1;j<=n;j++)
		for(int k=head[wz(i,j)];k;k=e[k].nex)
		{
			int to=e[k].to;
			if(to!=T&&e[k].w){a[i][j]=(to-n*n-1)/n;break;}
		}
	for(int i=1;i<=n;i++) for(int j=1;j<=n;j++) dfs1(i,j);
	return 0;
}

很困难,tourist 都没有场切(写了 n=40 的做法,2000 分,满分 2100,所以只有 100 分给 n=300),听说写了个 O(n5) 的做法,很强。

posted @   HaHeHyt  阅读(52)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
点击右上角即可分享
微信分享提示
主题色彩
主题色彩