其他分享
首页 > 其他分享> > 「NOI2016」网格 题解

「NOI2016」网格 题解

作者:互联网

「NOI2016」网格 题解

前言

感谢 zqm 学长提供调代码服务!

本文中,所有没有特殊说明的连通都是指四连通,相邻都是指上下左右相邻。

题目大意

有一个 $ n \times m $ 的网格,上面有 $ c $ 个障碍物,求至少还需要多少个障碍物才能使空地不连通。

输入

第一行有一个整数 $ T $,表示数据组数。

每组数据的第一行有三个整数 $ n,m,c $。

接下来 $ c $,每行两个整数 $ x,y $,表示第 $ x $ 行第 $ y $ 列有一个障碍物。保证每个障碍物不会被多次描述。

输出

一个整数,表示答案。

数据范围

$ 1 \le T \le 20 $

$ n,m \le 10^9 $

$ \sum c \le 10^5 $

思路

分析题目发现,当一个空地在角上的时候,答案最大为 $ 2 $,而如果角上是障碍物,又会形成一个新的角,所以本题答案只有 $ -1,0,1,2 $ 四种情况。我们可以分类讨论。

注意,这里的分类讨论是有顺序的,只有当前面的条件不满足才会判断后面的条件。

别看代码很长,实际上不难。代码中有注释,非常简洁易懂。

对了,这道题需要手写 hash,但是我太弱了,所以就使用 卡常 + Ofast 之力 卡到了 1950ms(逃

代码实现

#include<bits/stdc++.h>
#define ll int
const ll N=4e7+10;
using namespace std;

ll nt[8][2]={0,1,1,0,0,-1,-1,0,1,1,1,-1,-1,1,-1,-1};//判断连通 
ll T,n,m,c,x[N],y[N];//题目输入的变量 
ll sx[N],sy[N];//判断无解,统计有空地的行和列 
map<pair<ll,ll>,ll>a;//记录障碍 
map<pair<ll,ll>,ll>vis;//记录有用的点,辅助建图 
ll cnt,v[N],fir[N],nxt[N];//邻接表 
ll visit[N];//判断答案为 0,标记是否被访问 
ll vvv[N];//判断答案为 0,bfs2,标记数组 
ll flag;//判断答案为 1,标记是否有割点 
ll num,dfn[N],low[N];//判断答案为 1,tarjan 
ll tags[N];//判断答案为 1,标记第一层八连通空地 

ll read(){//快读 
	ll s=0,f=1;
	char ch=getchar();
	while(ch<'0'||ch>'9')f=(ch=='-'?-1:1),ch=getchar();
	while(ch>='0'&&ch<='9')s=s*10+(ch^48),ch=getchar();
	return f*s;
}

void add(ll x,ll y){//连边 
	v[++cnt]=y;
	nxt[cnt]=fir[x];
	fir[x]=cnt;
}

ll bfs2(ll st){//普通的 bfs 
	ll tot=1;
	queue<ll>q;
	q.push(st);
	vvv[st]=1;
	while(!q.empty()){
		ll x=q.front();
		q.pop();
		for(ll i=fir[x];i;i=nxt[i]){
			ll y=v[i];
			if(vvv[y])continue;
			vvv[y]=1;
			q.push(y);
			++tot;
		}
	}
	return tot;
}

bool bfs1(ll st){//找八连通障碍物并建图 
	//初始化 
	vis.clear();
	cnt=0;
	queue<ll>tmp;//记录八连通障碍物 
	queue<ll>q;
	tmp.push(st);//记录 
	q.push(st);
	visit[st]=1;
	while(!q.empty()){
		ll i=q.front();
		q.pop();
		for(ll j=0;j<8;++j){//这里是八连通 
			ll xx=x[i]+nt[j][0];
			ll yy=y[i]+nt[j][1];
			if(xx<1||xx>n||yy<1||yy>m)continue;//出界 
			if(a.find({xx,yy})==a.end())continue;//不是障碍物 
			if(visit[a[{xx,yy}]])continue;//访问过 
			visit[a[{xx,yy}]]=1;//标记 
			tmp.push(a[{xx,yy}]);//记录 
			q.push(a[{xx,yy}]);
		}
	}
	ll tot=0;
	while(!tmp.empty()){
		auto i=tmp.front();
		tmp.pop();
		for(ll j=0;j<8;++j){//这里是八连通 
			ll xx=x[i]+nt[j][0];
			ll yy=y[i]+nt[j][1];
			if(xx<1||xx>n||yy<1||yy>m)continue;//出界 
			if(vis.find({xx,yy})!=vis.end())continue;//已经标记过 
			if(a.find({xx,yy})!=a.end())continue;//是障碍物 
			vis[{xx,yy}]=++tot;//标记点号 
		}
	}
	for(ll i=1;i<=tot;++i)fir[i]=0;//初始化 
	//给有用的点连边 
	for(auto i:vis){
		ll x=i.first.first;
		ll y=i.first.second;
		ll t=i.second;
		for(ll i=0;i<4;++i){//这里是四连通 
			ll xx=x+nt[i][0];
			ll yy=y+nt[i][1];
			if(xx<1||xx>n||yy<1||yy>m)continue;//出界 
			if(vis.find({xx,yy})==vis.end())continue;//没有标记过 
			//连边 
			add(t,vis[{xx,yy}]);
		}
	}
	for(ll i=1;i<=tot;++i)vvv[i]=0;//初始化 
	ll cnt=bfs2(1);
	if(cnt!=tot)return true;
	return false;
}

void tarjan(ll x,ll fa){//tarjan 求割点 
	dfn[x]=low[x]=++num;
	ll tot=0;
	for(ll i=fir[x];i;i=nxt[i]){
		ll y=v[i];
		if(!dfn[y]){
			tarjan(y,x);
			low[x]=min(low[x],low[y]);
			if(low[y]>=dfn[x]){
				tot++;
				if(fa||tot>1){
					if(tags[x])flag=1;//当求出来的割点为第一层空地时才是真正的割点 
				}
			}
		}else{
			low[x]=min(low[x],dfn[y]);
		}
	}
}

bool judge_no_solution_1(){//无解:少于两个空地 
	return (long long)n*m-c<2;
}

bool judge_no_solution_2(){//无解:有两个四连通相邻的空地 
	if((long long)n*m-c!=2)return false;
	//初始化 
	for(ll i=1;i<=n;++i)sx[i]=0;
	for(ll i=1;i<=m;++i)sy[i]=0;
	//统计每行每列障碍物总数 
	for(ll i=1;i<=c;++i){
		sx[x[i]]++;
		sy[y[i]]++;
	}
	//找有空地的行 
	ll x1=0,x2=0;
	for(ll i=1;i<=n;++i){
		if(sx[i]<m){
			if(!x1)x1=i;
			else x2=i;
		}
	}
	//找有空地的列 
	ll y1=0,y2=0;
	for(ll i=1;i<=m;++i){
		if(sy[i]<n){
			if(!y1)y1=i;
			else y2=i;
		}
	}
	if((abs(x1-x2)==1&&y1&&!y2)||(x1&&!x2&&abs(y1-y2)==1)){//判断是否连通 
		return true;
	}
	return false;
}

bool judge_one_1(){//只有一行或一列 
	return n==1||m==1;
}

bool judge_zero(){
	//初始化 
	a.clear();
	for(ll i=1;i<=c;++i){
		visit[i]=0;
	}
	//标记障碍物 
	for(ll i=1;i<=c;++i){
		a[{x[i],y[i]}]=i;
	}
	for(ll i=1;i<=c;++i){
		if(visit[i])continue;//访问过 
		if(bfs1(i))return true;
	}
	return false;
}

bool judge_one_2(){//有割点 
	//初始化 
	flag=0;
	num=0;
	a.clear();
	vis.clear();
	cnt=0;
	//标记障碍物 
	for(ll i=1;i<=c;++i){
		a[{x[i],y[i]}]=1;
	}
	//找有用的点 
	ll tot=0;
	for(ll i=1;i<=c;++i){
		for(ll j=0;j<8;++j){//这里是八连通 
			ll xx=x[i]+nt[j][0];
			ll yy=y[i]+nt[j][1];
			if(xx<1||xx>n||yy<1||yy>m)continue;//出界 
			if(vis.find({xx,yy})!=vis.end())continue;//已经标记过 
			if(a.find({xx,yy})!=a.end())continue;//是障碍物 
			vis[{xx,yy}]=++tot;//标记点号 
		}
	}
	map<pair<ll,ll>,ll>VIS;//临时存储第二层点,为了防止遍历 vis 时爆炸 
	ll ___=tot;
	//再拓展一层 
	for(auto i:vis){
		ll x=i.first.first;
		ll y=i.first.second;
		ll t=i.second;
		for(ll i=0;i<8;++i){//这里是八连通 
			ll xx=x+nt[i][0];
			ll yy=y+nt[i][1];
			if(xx<1||xx>n||yy<1||yy>m)continue;//出界 
			if(vis.find({xx,yy})!=vis.end()||VIS.find({xx,yy})!=VIS.end())continue;//标记过 
			if(a.find({xx,yy})!=a.end())continue;//是障碍物 
			VIS[{xx,yy}]=++tot;
		}
	}
	//初始化 
	for(ll i=1;i<=___;++i)tags[i]=1;
	for(ll i=___+1;i<=tot;++i)tags[i]=0;
	//转移至 vis 
	for(auto i:VIS){
		ll x=i.first.first;
		ll y=i.first.second;
		ll t=i.second;
		vis[{x,y}]=t;
	}
	for(ll i=1;i<=tot;++i)fir[i]=0;
	//给有用的点连边 
	for(auto i:vis){
		ll x=i.first.first;
		ll y=i.first.second;
		ll t=i.second;
		for(ll i=0;i<4;++i){//这里是四连通 
			ll xx=x+nt[i][0];
			ll yy=y+nt[i][1];
			if(xx<1||xx>n||yy<1||yy>m)continue;//出界 
			if(vis.find({xx,yy})==vis.end())continue;//没有标记过 
			//连边 
			add(t,vis[{xx,yy}]);
		}
	}
	for(ll i=1;i<=tot;++i)dfn[i]=low[i]=0;//初始化 
	for(ll i=1;i<=tot;++i){
		if(!dfn[i]){
			tarjan(i,0);
		}
	}
	if(flag)return true;
	return false;
}

int main(){
	
	T=read();
	while(T--){
		n=read();
		m=read();
		c=read();
		for(ll i=1;i<=c;++i){
			x[i]=read();
			y[i]=read();
		}
		//无解 
		if(judge_no_solution_1()){
			printf("-1\n");
			continue;
		}
		if(judge_no_solution_2()){
			printf("-1\n");
			continue;
		}
		//答案为 0 
		if(judge_zero()){
			printf("0\n");
			continue;
		}
		//答案为 1 
		if(judge_one_1()){
			printf("1\n");
			continue;
		}
		if(judge_one_2()){
			printf("1\n");
			continue;
		}
		//答案为 2 
		printf("2\n");
	}
	
	return 0;
}

总结

在代码改动的时候,一定要注意将相关的其它函数都看一眼,否则可能会造成不必要的损失。请记住,板子不是一成不变的,要知道每个位置什么意思。(我就是因为改 bfs1 的实现方法时没改该死的 tarjan 判割点的条件,挂了好久)

一般来说,多组测试卡常效果最好的还是将 memset 改成 for 暴力赋值。虽然 memset 快一些,但它会将整个数组都更改,我们用不到的也被更改了。如果我们只更改有用的,效率可能更高。

尾声

如果你发现了问题,你可以直接回复这篇题解

如果你有更好的想法,也可以直接回复!

标签:障碍物,题解,ll,NOI2016,网格,yy,vis,xx,continue
来源: https://www.cnblogs.com/zsc985246/p/16621315.html