2018-2019 ICPC NEERC Northern Eurasia Finals M题 – Minegraphed 【构造】【图论】【强联通分量】【拓扑排序】

比赛链接:http://codeforces.com/contest/1089

题意是这样的:
你需要将一张由 n n n个节点构成的有向图,转化为一个三维的地图。(1 ≤ n ≤ \leq n \leq n 9)
要求对于任意一对节点,在这两个图中的可达性是相同的。
在三维地图中的移动方式有:水平方向移动1格,如果正下方的格子是空的就会自动下落,另外如果头顶的格子是空的还可以爬高一格。

举个例子:
在这里插入图片描述
在上图中,假设有数字的格子都是空的。那么我们从1号格子出发,可以先向左走两格,然后爬高一格到达2号格子,或者下落一层到达4号格子,反过来从2号格子或者4号格子走到1号格子也是可以的。也就是说,1号、2号、4号格子是互相可达的。但是3号格子因为所处的位置太高,只能从高处向下落,但不能从低处向上爬,因此从3号格子出发只能单向到达1号、2号、4号格子。

另外,题目样例给出的情况是这样的:
描述

这个图有点难懂,图中每个数字其实表示的是上方的那个格子,对照输出才发现,4号格子其实是最顶层的,1号格子是中间层的。1号格子和4号格子相邻并且高度差为1,因此可以互相到达。2号格子和3号格子都位于最底层,也可以互相到达。另外由于高度差的关系,只能从4号格子所在层出发掉落到最底层,而最底层无法回到高层,所以这是一个单向到达的关系。

显然,这是一道构造的题目。

那么怎么构造呢?

刚看到这个题的时候,首先就会想到,我们可以用Tarjan求出图中的强联通分量,把能够互相到达的节点缩成一个点,最后只需要把他们紧挨着排列在一起就可以。
然后我们得到了一个有向无环图,可以使用拓扑排序,这样我们只能从拓扑序更靠前的节点,单向到达拓扑序更靠后的节点。这在三维地图中可以通过高度差来实现。

也就是说,我们构造的地图是由多个“层”组成的。按照拓扑排序的结果,把拓扑序靠前的强联通分量放在更高层,每个强联通分量内的节点都放在同一层,相邻的层之间保持超过两格的高度差。构造出来的地图是这样的感觉:
在这里插入图片描述
不过还存在一些问题,比如,从一个层出发能够到达多个层,并且这多个层之间是不可达的情况,应该如何处理。

这个题给的地图尺寸上限挺大的,于是突然想到了这样一种构图方法:
在这里插入图片描述
假设缩点后的强联通分量共有 m m m个,那么3维地图总共就有竖直堆叠的m层构成。每一层的长度都是2 m m m2,宽度都是4,高度都是3。由一个长条状的“站台”, m m m2个“通道”,以及 m m m2个“开关”构成。带数字的格子都位于站台之上,对于任意两层,都有一个专门的通道与之对应。俯视图可能会看的更清楚一些。
大致的思路就是,我们首先要让任意两层都不可到达,然后如果要从层A要到达层B,只能走专门设计的通道。这样的通道总共有 m m m2个。
图1
对于样例来说,我们首先把1号和4号节点放在第一层,2号和3号节点放在第二层。由于强联通分量1可以单向到达2,所以我们把从层1到达层2的“通道1-2”打开。也就是从层1的站台出发,穿过像门一样的开关,走到通道处,可以竖直掉落到层2中对应的位置,但是反向的过程无法实现。

我们可以再看一个层数更多的例子:
图2
如果从层A可以单向到达层C,我们就要打开“通道A-C”,然后把A和B对应位置的通道挖开,这样保证可以竖直掉落到层C。然后开启层A和层C对应通道的大门,但是层B的开关要关闭,否则从层B也可以经过这个通道掉落到层C。
如果层B还可以到达层D之间,那就同样找到“通道B-D”,用上述方法建立通道即可。
不同的通道之间是互不干扰的,这也就是为什么要准备 m m m2个通道,并且通道之间还要保证空隙。
上述方法构建的地图,长度最大为200,宽度为4,高度最大为30,所以格子数的上限是24000,小于题目要求的上限1e6。

另外还有两个坑:
(1)每一层的长度还有下限是10,因为有可能 n n n个点全被缩进同一个强联通分量,如果站台长度只有2是放不下 n n n个点的。
(2)最底层的站台需要垫高,因为从任意一层都可以先掉落到整张地图的最底部,然后向上爬一格回到站台。

这道题在比赛结束前的最后9秒钟顺利AC,真的是紧张刺激。

因为时间来不及了,所以代码写的有点丑:

#include<stdio.h>
#include<string.h>
#include<vector>
#include<algorithm>
using namespace std;
int con[12][12],conj[12][12];
vector<int> node[12];
bool instack[12];
int index,top,stack[12],dfn[12],low[12];
int num,belong[12],arr[12],pst[12],indeg[12];
char ans[32][6][210];
bool vis[12];

void Tarjan(int u){
	int cnt=node[u].size();
	instack[u]=true;
	stack[++top]=u;
	dfn[u]=low[u]=++index;
	for(int i=0;i<cnt;i++){  
		int v=node[u][i];
		if(!dfn[v]){
			Tarjan(v);
			if(low[v]<low[u]) low[u]=low[v];
		}else{
			if(instack[v] && dfn[v]<low[u]) low[u]=dfn[v];
		}
	}
	if(dfn[u]==low[u]){
		num+=1;
		while(true){
			int pst=stack[top--];
			instack[pst]=false;
			belong[pst]=num;
			if(pst==u) break;
		}
	}
	return ;
}

void solve(int n){
	top=num=index=0;
	memset(dfn,0,sizeof(dfn));
	memset(low,0,sizeof(low));
	for(int i=1;i<=n;i++){
		if(!dfn[i]) Tarjan(i); // 强联通分量 
	}
	return ;
}

void Tsort(int m){
	int k=0; // 拓扑排序 
	for(int retry=0;retry<=m;retry++){
		for(int i=1;i<=m;i++){
			if(indeg[i]==0){
				for(int j=1;j<=m;j++){
					if(conj[i][j]==1) indeg[j]-=1;
				}
				pst[i]=k; arr[k++]=i;
				indeg[i]=-1;
			}
		}
		if(k==m) break;
	}
}

void init(int m,int n){
	int len=max(2*m*m,12); // 初始化各层 
	for(int k=0;k<m;k++){
		for(int i=0;i<len;i++){
			for(int j=0;j<=3;j++){
				ans[3*k][j][i]=ans[3*k+1][j][i]=ans[3*k+2][j][i]='.';
			}
			ans[3*k+2][0][i]='#'; // 站台 
			if(i&1) continue;
			ans[3*k][1][i]=ans[3*k+1][1][i]=ans[3*k+2][1][i]='#'; // 开关 
			ans[3*k+2][2][i]=ans[3*k+2][3][i]='#'; // 通道 
		}
		for(int i=1;i<=n;i++){
			if(belong[i]==arr[k]){
				ans[3*k+1][0][i]='0'+i;
			}
		}
	}
	for(int k=3*m;k<3*m+2;k++){
		for(int i=0;i<len;i++){
			for(int j=0;j<=3;j++){
				ans[k][j][i]='.'; // 最底部垫高两层 
			}
		}
	}
}

int main(){
	int n,lift;
	while(scanf("%d",&n)!=EOF){
		memset(conj,0,sizeof(conj));
		for(int i=1;i<=n;i++){
			for(int j=1;j<=n;j++){
				scanf("%d",&con[i][j]);
				if(con[i][j]==1){
					node[i].push_back(j);
				}
			}
		}
		solve(n);
		memset(indeg,0,sizeof(indeg));
		for(int i=1;i<=n;i++){
			for(int j=1;j<=n;j++){
				if(con[i][j]==1 && belong[i]!=belong[j]){
					if(conj[belong[i]][belong[j]]==0) indeg[belong[j]]+=1;
					conj[belong[i]][belong[j]]=1;
				}
			}
		}
		Tsort(num);
		init(num,n);
		for(int i=1;i<=num;i++){
			for(int j=1;j<=num;j++){
				if(conj[i][j]==1){
					lift=2*((i-1)*num+(j-1)); // 打开通道 
					ans[3*pst[i]][1][lift]=ans[3*pst[i]+1][1][lift]='.';
					ans[3*pst[j]][1][lift]=ans[3*pst[j]+1][1][lift]='.';
					for(int k=pst[i];k<pst[j];k++){
						ans[3*k+2][3][lift]='.';
					}
				}
			}
		}
		int len=max(2*num*num,12);
		printf("%d %d %d\n",len,4,3*num+2);
		for(int k=0;k<3*num+2;k++){
			if(k!=0) printf("\n");
			for(int i=0;i<4;i++){
				ans[k][i][len]='\0';
				printf("%s\n",ans[k][i]);
			}
		}
		for(int i=1;i<=n;i++){
			node[i].clear();
		}
	}
	return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值