「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 $ 四种情况。我们可以分类讨论。
注意,这里的分类讨论是有顺序的,只有当前面的条件不满足才会判断后面的条件。
-
当答案为 $ -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