ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

「NOI2016」网格 题解

2022-08-27 11:30:43  阅读:174  来源: 互联网

标签:障碍物 题解 ll NOI2016 网格 yy vis xx continue


「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 $ 四种情况。我们可以分类讨论。

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

  • 当答案为 $ -1 $ 时:

    • 空地本来就少于两个。

      此时只需要判断 $ n \times m - c $ 是否小于 $ 2 $。

    • 有两个相邻的空地。

      此时在 $ n \times m - c = 2 $ 的基础上,需要记录每行每列的障碍物数量,进一步判断是否相邻。

  • 当答案为 $ 0 $ 时:

    • 有一坨障碍物已经使得空地不连通。

      由于障碍物较少,考虑从障碍物入手,然后找到对计算答案有用的空地,进行统计。

      我们可以证明,若存在这样一坨障碍物,则这坨障碍物必定为一个八连通分量。

      我们可以利用这个性质,依次找出障碍物组成的每个八连通分量,再将所有与当前八连通分量八连通的空地找出,把相邻的空地连边,看是否连通即可。

  • 当答案为 $ 1 $ 时:

    • 网格只有一行或一列。

    • 网格中存在一个点,使得将它变成障碍物后空地不连通。

      可以发现,这跟割点的定义相同,考虑类比上方的思路,找到有用的空地,相邻的空地连边,建图求割点。

      我们想到,可以同样找与障碍物八连通的空地。但是看下面这种情况(X 表示障碍,. 表示空地,1 表示选的点):

      .......
      .111...
      .1X1...
      .11111.
      ...1X1.
      ...111.
      .......
      

      此时会把中间的 1 判为割点,但是它并不是整个网格图的割点。

      所以我们考虑再拓展一层,变成这样:(2 表示第二次拓展的点)

      22222..
      21112..
      21X1222
      2111112
      2221X12
      ..21112
      ..22222
      

      可以证明,此时如果标为 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

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有