P3438 - ZAB-Frogs 题解

首先容易想到二分答案,然后对平方个格子,算出它有没有被至少一个圆心为坏点、半径为当前答案的圆覆盖,然后将相邻的没有被覆盖的格子对连起来,用 bfs 或并查集判断起终点是否连通(注意一个特殊情形:起点等于终点,这时候无论如何都连通,此时还需要判断该点有没有覆盖)。

那么难点就在于怎么快速(平方复杂度)算出每个格子有没有被覆盖。这有两种思路,分别是 ycx(蒟蒻)和 wjz(神仙)的方法。

ycx 的方法

考虑在二分之前将每个点与坏点的最小距离预处理出来,然后二分的时候就异常简单了。而且预处理的时间复杂度上限放宽了一个 log,可以到平方对数。

对于一个格子,考虑分成以它为原点的四个象限分别计算(坐标轴就算两遍呗,反正 min 可叠加)。以第二象限为例,就对每一行递推,在当前点往右移动的过程中希望做到对数或均摊对数的复杂度来算出每个点在第二象限中的答案。

这是欧式距离,比较棘手。首先讲一个没啥用的 observation:每列最多只有一个决策代表(取最矮的)。然后从左往右,每列的决策代表的高度应该递增。

但此时还是不能确定,这个高度关于横坐标单调升的决策序列里哪个是最近的。下面使用单调队列、单调栈、斜率优化的精髓——及时 pop 无用决策:找一找有什么特征的决策在某个时间点后永远无法成为最优决策。

考虑当前单调序列里相邻的两个点 \((x_1,y_1),(x_2,y_2)(x_1<x_2)\),当前在 \((x,y)\),那么 \((x_1,y_1)\) 更优当且仅当 \((x_1-x)^2+(y_1-y)^2<(x_2-x)^2+(y_2-y)^2\),即 \(f=2(x_2-x_1)x+\left(x_1^2-x_2^2\right)+2(y_2-y_1)y+\left(y_1^2-y_2^2\right)<0\)。然后考虑 \((x,y)\) 往右移一格变成 \((x+1,y)\) 的时候,考察 \(f\) 做何变化:后面三项显然都不动,所以 \(f\) 增加了 \(2(x_2-x_1)\)。由于 \(x_2>x_1\),任意相邻两点对应的 \(f\) 会随 \((x,y)\) 的右移一直增加,所以即使现在是左边那个决策优一点,等到 \(f\geq0\) 了左边的决策就再无翻身之日了。

针对刚刚的 observation,现在可以采取类似单调队列、单调栈、斜率优化的行动:当某个相邻对的 \(f\geq0\) 的时候,毅然决然地将左边的决策删掉。那么任意时刻维护的决策序列有了每个相邻对都有 \(f<0\) 的性质,此时显然序列头是最优的。

那么考虑怎么维护这个序列使得任意处 \(f<0\) 的性质总满足。那就每当有 \(f\geq 0\) 就立刻删左边。但每个时刻怎么知道此时有哪些相邻对刚刚达到 \(f\geq 0\) 的状态呢?可以在每次序列内元素变更的时候(无论是 pop 还是 push),都将新产生的相邻对预言一下(容易预言吧)什么时候会需要删除左边元素,放进对应时刻的待操作序列(是个 vector),然后每个时刻执行就可以了。至于有的相邻对还没被操作,就被破坏了,这没关系,多 check 几遍总是好的,怕的是漏 check 对吧。

那这样用链表(std::list)维护决策序列即可,甚至不带 log,是平方的。然后算完这玩意甚至可以不用二分,就将点们按照距离该点最近的坏点的距离排序,从大到小扫一遍实时用并查集维护,复杂度 \(\mathrm O(nm\log)\) 或者 \(\mathrm O(nm\alpha)\)

code(有点难写、难调,毕竟用到了 list)
#include<bits/stdc++.h>
using namespace std;
#define pb push_back
#define mp make_pair
#define X first
#define Y second
int sq(int x){return x*x;}
int dis(int x1,int y1,int x2,int y2){return sq(x1-x2)+sq(y1-y2);}
const int N=1010,M=1010;
int n,m,s;
int sx,sy,ex,ey;
bool hav[N][M];
int up[N][M],dwn[N][M];
list<int> ls,laugh;
list<int>::iterator pt[M];
vector<int> op[M];
int tim(int dif,int x1,int x2){
	int v=2*abs(x1-x2);
	return max(0,(dif+v-1)/v);
}
int mnd[N][M];
bool ok(int x){return 1<=x&&x<=m;}
void deal(int (*ud)[M],int ord){//remember to clear
	int d1,d2,t;
	for(int i=1;i<=n;i++){
		ls.clear();for(int j=1;j<=m;j++)pt[j]=laugh.begin(),op[j].clear();
		for(int j=ord==1?1:m;(ord==1?(j<=m):j);j+=ord){
			if(ud[i][j]){
				ls.pb(j),pt[j]=--ls.end();
				if(ls.size()>1&&
				   ok(t=j+ord*tim(dis(ud[i][j],j,i,j)-dis(ud[i][*-- --ls.end()],*-- --ls.end(),i,j),*-- --ls.end(),j)))
					op[t].pb(*-- --ls.end());
			}
			for(int k=0;k<op[j].size();k++){
				int x=op[j][k];
				if(*pt[x]!=x)continue;
				list<int>::iterator p=pt[x],q=p++;
				swap(p,q);
				if(q==ls.end())continue;
				if(dis(ud[i][*p],*p,i,j)>=(d2=dis(ud[i][*q],*q,i,j))){
					if(p==ls.begin())pt[*p]=laugh.begin(),ls.erase(p);
					else{
						pt[*p]=laugh.begin(),ls.erase(p--);
						if(ok(t=j+ord*tim(d2-dis(ud[i][*p],*p,i,j),*p,*q)))op[t].pb(*p);
					}
				}
			}
			if(ls.size())mnd[i][j]=min(mnd[i][j],dis(ud[i][ls.front()],ls.front(),i,j));
		}
	}
}
struct ufset{
	int fa[N*M];
	void init(){memset(fa,0,sizeof(fa));}
	int root(int x){return fa[x]?fa[x]=root(fa[x]):x;}
	void mrg(int x,int y){
		x=root(x),y=root(y);
		if(x!=y)fa[x]=y;
	}
}ufs;
int id(int x,int y){return (x-1)*m+y;}
pair<int,int> ord[N*M];
bool cmp(pair<int,int> x,pair<int,int> y){return mnd[x.X][x.Y]>mnd[y.X][y.Y];}
bool vis[N][M];
int main(){
//	freopen("C:\\Users\\chenx\\Downloads\\1514\\22.in","r",stdin);
	laugh.pb(0);
	cin>>n>>m>>sx>>sy>>ex>>ey>>s;
	for(int i=1;i<=s;i++){
		int x,y;
		scanf("%d%d",&x,&y);
		hav[x][y]=true;
	}
	for(int i=1;i<=n;i++)for(int j=1;j<=m;j++){
		up[i][j]=up[i-1][j];
		if(hav[i][j])up[i][j]=i;
	}
	for(int i=n;i;i--)for(int j=1;j<=m;j++){
		dwn[i][j]=dwn[i+1][j];
		if(hav[i][j])dwn[i][j]=i;
	}
	memset(mnd,0x3f,sizeof(mnd));
	deal(up,-1),deal(dwn,1),deal(dwn,-1),deal(up,1);
//	for(int i=1;i<=n;i++)for(int j=1;j<=m;j++)cout<<mnd[i][j]<<" \n"[j==m];
	if(sx==ex&&sy==ey)return cout<<mnd[sx][sy],0;
	int now=0;
	for(int i=1;i<=n;i++)for(int j=1;j<=m;j++)ord[++now]=mp(i,j);
	sort(ord+1,ord+now+1,cmp);
	ufs.init();
	for(int i=1;i<=now;i++){
		int x=ord[i].X,y=ord[i].Y;
		vis[x][y]=true;
		if(vis[x+1][y])ufs.mrg(id(x,y),id(x+1,y));
		if(vis[x-1][y])ufs.mrg(id(x,y),id(x-1,y));
		if(vis[x][y+1])ufs.mrg(id(x,y),id(x,y+1));
		if(vis[x][y-1])ufs.mrg(id(x,y),id(x,y-1));
		if(ufs.root(id(sx,sy))==ufs.root(id(ex,ey)))return cout<<mnd[x][y],0;
	}
	puts("0");
	return 0;
}

wjz 的方法

wjz 就很神仙了,想到了一个 ddstd 的方法。考虑直接在二分的时候考虑哪些点被覆盖。

枚举点、枚举圆,这样最坏是四方的。先枚举圆的话可以聪明点,每行覆盖的是一个区间,可以用差分去掉一个 \(n\),剩下三方的复杂度,不行。

但这个差分的方法有启发性。我们针对一个圆在某一行上覆盖的区间,可能会发现它包含或包含于其它圆在该行上的覆盖区间。那这时候取那个最大的,包含于它的都可以不管了。接下来是一个很 nb 的针对有哪些特殊的圆在该行上的覆盖区间不用算的 observation:对于某点 \((x,y)\),考察它上下最近的两个坏点,那么该列比这两个坏点更要远的坏点代表的圆在 \((x,y)\) 所在行的覆盖区间一定被最近的两个坏点的圆覆盖,结合所有圆的半径相等易证正确性。

那这就很爽了呀,对每个点都只有两个圆会对该点所在行有作用,那平方差分就做到了呀。

判连通用 bfs 或并查集,但是用并查集会带 \(\log^2\) 或者 \(\alpha\log\),但我懒癌晚期,还是并查集了(

code(相比之下就比 ycx 的好写多了,wjz dd ycx)
#include<bits/stdc++.h>
using namespace std;
const int N=1010,M=N;
int n,m,sx,sy,ex,ey,s;
bool hav[N][M];
int up[N][M],dwn[N][M];
int dlt[N][M];
struct ufset{
	int fa[N*M];
	void init(){memset(fa,0,sizeof(fa));}
	int root(int x){return fa[x]?fa[x]=root(fa[x]):x;}
	void mrg(int x,int y){
		x=root(x),y=root(y);
		if(x!=y)fa[x]=y;
	}
}ufs;
int id(int x,int y){return (x-1)*m+y;}
bool chk(int lim){//remember to make it first
//	cout<<lim<<":\n";
	memset(dlt,0,sizeof(dlt));
	for(int i=1;i<=n;i++){
		for(int j=1;j<=m;j++){
			int mn=2*n;
			if(up[i][j])mn=min(mn,i-up[i][j]);
			if(dwn[i][j])mn=min(mn,dwn[i][j]-i);
			if(lim-1<mn*mn)continue;
			int r=floor(sqrt(lim-1-mn*mn))+.5;
			dlt[i][max(1,j-r)]++,dlt[i][min(m+1,j+r+1)]--;
		}
		for(int j=1;j<=m;j++)dlt[i][j]+=dlt[i][j-1];
	}
//	for(int i=1;i<=n;i++)for(int j=1;j<=m;j++)cout<<!!dlt[i][j]<<" \n"[j==m];
	ufs.init();
	for(int i=1;i<=n;i++)for(int j=1;j<=m;j++)if(!dlt[i][j]){
		if(i<n&&!dlt[i+1][j])ufs.mrg(id(i+1,j),id(i,j));
		if(j<m&&!dlt[i][j+1])ufs.mrg(id(i,j+1),id(i,j));
	}
	return !dlt[sx][sy]&&!dlt[ex][ey]&&ufs.root(id(sx,sy))==ufs.root(id(ex,ey));
}
int main(){
	cin>>n>>m>>sx>>sy>>ex>>ey>>s;
	while(s--){
		int x,y;
		scanf("%d%d",&x,&y);
		hav[x][y]=true;
	}
	for(int j=1;j<=m;j++)for(int i=1;i<=n;i++){
		up[i][j]=up[i-1][j];
		if(hav[i][j])up[i][j]=i;
	}
	for(int j=1;j<=m;j++)for(int i=n;i;i--){
		dwn[i][j]=dwn[i+1][j];
		if(hav[i][j])dwn[i][j]=i;
	}
	int ans=0;
	for(int i=29;~i;i--)if(ans+(1<<i)<=2*n*n&&chk(ans+(1<<i)))ans+=1<<i;
	cout<<ans;
	return 0;
}
posted @ 2021-07-08 11:49  ycx060617  阅读(86)  评论(0编辑  收藏  举报