poi2006 Task Frogs (zab)

poi2006 Task Frogs (zab) #


有一个 \(h\times w\) 的矩形田地,有 \(n\) 个稻草人,第 \(i\) 个位置在 \((x_i,y_i)\).

青蛙在 \((a,b)\) 上距离稻草人的距离为 \(\min\ \sqrt{(x_i-a)^2+(y_i-b)^2}\) .

青蛙要从点 \((sx,sy)\) 走到点 \((tx,ty)\) , 一个路径距离稻草人的距离为路径上所有点距离稻草人的距离最小值.

求从青蛙的所有路径中距离稻草人的最大距离的平方.

\(1\leq h,w\leq 1000,\ 1\leq n\leq h\times w\)

首先,一定要求得数组 \(g(i,j)\) 表示青蛙在 \((i,j)\) 点上距离稻草人的距离的平方.

然而,这个不好求.

首先,要设 \(f(i,j)\) 代表在第 \(i\) 行上的稻草人距离第 \(j\) 列的最小距离.

\(f(i,j)\) 是可以 \(O(hw)\) 求得的.

接下来,求 \(g\) 的式子是

\[\begin{align} g(i,j) &=\min\limits_{k} (i-k)^2+f(k,j)^2\\ &=\min\limits_{k} i^2-2ik+k^2+f(k,j)^2\\ &=i^2+\min\limits_{k} -2ik+k^2+f(k,j)^2\\ \end{align} \]

写成这个形式之后,发现如果枚举 \(j\) ,变量是 \(i,k\) 时,这是一个斜率优化.

斜率优化做法1

\(y=kx+b\longrightarrow y=-2ki+k^2+f(k,j)^2\)

其中 \(k=-k,b=k^2+f(k,j)^2,x=i\).

对于每一列,维护一个凸包. 此时,发现由于 \(i\) 是递增的,所以可以用双端队列维护凸包,时间复杂度为 \(O(hw)\).

接着,求值时 \(i\) 同样递增,继续双端队列. 时间复杂度是 \(O(hw)\) 的.

考虑维护 \(k,b\) ,定义一个 class 叫 line.

判断是否需要删除线段,即队列倒数第二个直线与当前直线的交点横坐标小于倒数第一个直线与当前直线的焦点横坐标.

求值时由于单调性,如果队首直线没有队首第二个直线来得优,那么就弹出.

时间复杂度 \(O(hw)\)

空间复杂度 \(O(hw)\)

my code

for(int j=0;j<w;j++){
	for(int i=0;i<h;i++){
		line L=(line){-i,i*i+f[i][j]*f[i][j]};
		while(q.size()>=2){
			line L1=q[q.size()-1];
			line L2=q[q.size()-2];
			if(check(L,L1,L2))q.pop_back();
			else break;
		}
		q.push_back(L);
	}
	for(int i=0;i<h;i++){
		while(q.size()>=2&&dis(q[0],2*i)>dis(q[1],2*i))q.pop_front();
		if(!q.empty())g[i][j]=dis(q[0],2*i)+i*i;
	}
	while(!q.empty())q.pop_back();
}

对于此做法,如果动态加入和删除需要用到 set,因为斜率单调增或单调减,所以按照斜率排序,找到 set 中见的位置,向两边删. 我还没有写过.

斜率优化做法2

听说上面做法比较难写. 于是,将式子转化成 \(b=-kx+y\). 考虑 \(b\) 是因变量,\(x,y\) 是定值,\(k\) 是自变量.

\(b=-kx+y\longrightarrow y=-i\cdot 2k+k^2+f(k,j)^2\)

\(x=2k,y=k^2+f(k,j)^2,k=i\)

考虑 \(b=-kx+y\) 的含义,是平面上有一些点,从下自伤平移一个斜率为 \(k\) 的直线,碰到第一个点时与 \(y\) 轴的交点即为最小值.

同样,能取到最小值的 \((x,y)\) 相邻点之间的连线是一个凸包,最小值是下凸包,最大值是上凸包.

现在是最小值,对于维护下凸包,加入一个点之后判断是否要弹出,可以用到交点,也可以用向量内积.

向量内积

对于向量 \(\vec{v1}=(x_1,y_1),\vec{v2}=(x_2,y_2)\).

\(|\vec{v1}\times \vec{v2}|=|x_1y_2-x_2y_1|\).

如果 \(\vec{v1}\) 逆时针旋转 \(\theta\)\(0\leq \theta\leq 180\) 可以与 \(\vec{v2}\) 相交. 则内积为非负.

当前是维护下凸包,所以,以倒数第二个点为坐标原点,倒数第一个点为 \(\vec{v2}\) ,当前点为 \(\vec{v1}\) .

如果 \(\vec{v1}\times \vec{v2}\geq 0\),则需要弹出倒数第二个点.

当求值时,当前直线相交的第一个点 \(P_i\)\(P_i\)\(P_{i-1}\) 之间组成的线段斜率小于等于当前直线的斜率, \(P_i\)\(P_{i+1}\) 组成的直线大于当前直线的斜率. 那么,可以考虑三分交点或者二分直线.

不会写三分,那么二分直线更靠谱一些.

对于动态维护. 考虑按 \(x\) 的大小从大排序,用 set 维护,插入时向前 pop ,向后 pop . 没写过+1.

但是,对于此题,由于 \(i\) 从小到大, 所以可以用双端队列来维护凸包和查询操作.

时间复杂度是 \(O(hw)\) .

我觉得用向量的确比用交点方便一些.

时间复杂度 \(O(hw)\)

空间复杂度 \(O(hw)\)

my code

for(int j=0;j<w;j++){
	int ub=0;
	for(int i=0;i<h;i++){
		while(ub>1&&check(stk[ub-2].first,stk[ub-2].second,
		stk[ub-1].first,stk[ub-1].second,2*i,i*i+f[i][j]*f[i][j]))ub--;
		stk[ub++]=make_pair(2*i,i*i+f[i][j]*f[i][j]);
	}
	int lb=0;
	for(int i=0;i<h;i++){
		while(lb+1<ub&&
		slope(stk[lb].first,stk[lb].second,stk[lb+1].first,stk[lb+1].second,i))
			lb++;
		g[i][j]=i*i-i*stk[lb].first+stk[lb].second;
	}
}
图中求值做法1

因为路径最小值的最大值,这个东西看上去就很像二分可以解决的东西.

于是,二分得到最大值 \(mid\) .

对于 \(g(x,y)\geq mid\) 的点,可以通过.

对于 \(g(x,y)<mid\) 的点,不可以通过.

bfs 检验是否可以从 \((sx,sy)\) 走到 \((tx,ty)\) .

时间复杂度 \(O(hw\ log(h^2+w^2))\) .

空间复杂度 \(O(hw)\)

如果 bfs 的是手写队列,是可以通过的.

#include<bits/stdc++.h>
using namespace std;
class line{public:int a,b;};
int h,w,n,stx,sty,edx,edy;
int sc[1010][1010];
int f[1010][1010],g[1010][1010];
deque<line>q;
inline void chkmin(int &x,int y){if(y<x)x=y;}
inline int dis(line L,int x){return L.a*x+L.b;}
inline bool check(line L,line L1,line L2){
	return (L.b-L1.b)*(L2.a-L.a)<=(L.b-L2.b)*(L1.a-L.a);
}
const int dx[]={1,0,-1,0};
const int dy[]={0,1,0,-1};
int Q[1010*1010][2],st=0,ed=0;
bool vis[1010][1010];
bool check(int md){
	memset(vis,false,sizeof(vis));
	st=0;ed=0;
	if(g[stx][sty]>=md){
		Q[ed][0]=stx;Q[ed++][1]=sty;
		vis[stx][sty]=true;
	}
	while(st<ed){
		int x=Q[st][0],y=Q[st++][1];
		for(int i=0;i<4;i++){
			int nx=x+dx[i],ny=y+dy[i];
			if(nx<0||nx>=h||ny<0||ny>=w||g[nx][ny]<md||vis[nx][ny])continue;
			vis[nx][ny]=true;
			Q[ed][0]=nx;Q[ed++][1]=ny;
		}
	}
	return vis[edx][edy];
}
int main(){
	ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
	cin>>w>>h>>sty>>stx>>edy>>edx; 
	stx--;sty--;edx--;edy--;
	cin>>n;
	memset(sc,-1,sizeof(sc));
	for(int i=0;i<n;i++){
		int x,y;
		cin>>y>>x;
		x--;y--;
		sc[x][y]=i;
	}
	for(int i=0;i<h;i++)for(int j=0;j<w;j++)f[i][j]=1010;
	for(int i=0;i<h;i++){
		int pos=1010;
		for(int j=0;j<w;j++){
			if(sc[i][j]!=-1)pos=0;
			else pos++;
			chkmin(f[i][j],pos);
		}
	}
	for(int i=0;i<h;i++){
		int pos=1010;
		for(int j=w-1;j>=0;j--){
			if(sc[i][j]!=-1)pos=0;
			else pos++;
			chkmin(f[i][j],pos);
		}
	}
	for(int j=0;j<w;j++){
		for(int i=0;i<h;i++){
			line L=(line){-i,i*i+f[i][j]*f[i][j]};
			while(q.size()>=2){
				line L1=q[q.size()-1];
				line L2=q[q.size()-2];
				if(check(L,L1,L2))q.pop_back();
				else break;
			}
			q.push_back(L);
		}
		for(int i=0;i<h;i++){
			while(q.size()>=2&&dis(q[0],2*i)>dis(q[1],2*i))q.pop_front();
			if(!q.empty())g[i][j]=dis(q[0],2*i)+i*i;
		}
		while(!q.empty())q.pop_back();
	}
	int low=0,high=h*h+w*w+1,ans=0;
	while(low<high){
		int mid=(low+high)>>1;
		if(check(mid)){
			low=mid+1;
			ans=max(ans,mid);
		}
		else high=mid;
	} 
	cout<<ans<<endl;
	return 0;
}
/*inline? ll or int? size? min max?*/
图中求值做法2

\(dp(x,y)\) 表示从起点出发,到点 \((x,y)\) 最少经过路程.

可以用 dijkstra 求得.

如果用spfa的话,反而会tle一个点.

时间复杂度: \(O(hw\ log(hw))\)

空间复杂度: \(O(hw)\)

my code

#include<bits/stdc++.h>
using namespace std;
class line{public:int a,b;};
int h,w,n,stx,sty,edx,edy;
int sc[1010][1010];
int f[1010][1010],g[1010][1010];
deque<line>q;
inline void chkmin(int &x,int y){if(y<x)x=y;}
inline int dis(line L,int x){return L.a*x+L.b;}
inline bool check(line L,line L1,line L2){
	return (L.b-L1.b)*(L2.a-L.a)<=(L.b-L2.b)*(L1.a-L.a);
}
const int dx[]={1,0,-1,0};
const int dy[]={0,1,0,-1};
vector<pair<int,int> >v[1010*1010*2];
queue<pair<int,int> >Q;
bool vis[1010][1010];
int main(){
	ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
	cin>>w>>h>>sty>>stx>>edy>>edx; 
	stx--;sty--;edx--;edy--;
	cin>>n;
	memset(sc,-1,sizeof(sc));
	for(int i=0;i<n;i++){
		int x,y;
		cin>>y>>x;
		x--;y--;
		sc[x][y]=i;
	}
	for(int i=0;i<h;i++)for(int j=0;j<w;j++)f[i][j]=1010;
	for(int i=0;i<h;i++){
		int pos=1010;
		for(int j=0;j<w;j++){
			if(sc[i][j]!=-1)pos=0;
			else pos++;
			chkmin(f[i][j],pos);
		}
	}
	for(int i=0;i<h;i++){
		int pos=1010;
		for(int j=w-1;j>=0;j--){
			if(sc[i][j]!=-1)pos=0;
			else pos++;
			chkmin(f[i][j],pos);
		}
	}
	for(int j=0;j<w;j++){
		for(int i=0;i<h;i++){
			line L=(line){-i,i*i+f[i][j]*f[i][j]};
			while(q.size()>=2){
				line L1=q[q.size()-1];
				line L2=q[q.size()-2];
				if(check(L,L1,L2))q.pop_back();
				else break;
			}
			q.push_back(L);
		}
		for(int i=0;i<h;i++){
			while(q.size()>=2&&dis(q[0],2*i)>dis(q[1],2*i))q.pop_front();
			if(!q.empty())g[i][j]=dis(q[0],2*i)+i*i;
		}
		while(!q.empty())q.pop_back();
	}
	vis[stx][sty]=true;
	v[g[stx][sty]].push_back(make_pair(stx,sty));
	for(int i=h*h+w*w;i>=0;i--){
		while(!Q.empty())Q.pop();
		for(int j=0;j<(int)v[i].size();j++)Q.push(v[i][j]);
		while(!Q.empty()){
			int x=Q.front().first,y=Q.front().second;Q.pop();
			if(x==edx&&y==edy){
				cout<<i<<endl;
				return 0;
			}
			for(int d=0;d<4;d++){
				int nx=x+dx[d],ny=y+dy[d];
				if(nx<0||nx>=h||ny<0||ny>=w||vis[nx][ny])continue;
				vis[nx][ny]=true;		
				if(g[nx][ny]>=i)Q.push(make_pair(nx,ny));
				else v[g[nx][ny]].push_back(make_pair(nx,ny));
			}
		}
	}
	return 0;
}
/*inline? ll or int? size? min max?*/
图中求值做法3

用链表求.

对于每一个距离,用一个 vector \(v(i)\) 记录 \(g\) 值为 \(i\) 且存在一条路径使得路径上每一个点的 \(g\) 值都比 \(i\) 大的点.

考虑从大到小枚举答案 \(i\)

\(v(i)\) 中的点放入队列中.

访问到一个未访问的点 \((x,y)\) 时首先将此点打上访问过的标记,接着分为两种情况.

\(g(x,y)\geq i\) ,加入队列.

\(g(x,y)<i\) ,加入对应的 \(v(g(x,y))\) 中.

时间复杂度:\(O(hw)\)

空间复杂度: \(O(hw)\)

my code

#include<bits/stdc++.h>
using namespace std;
class line{public:int a,b;};
int h,w,n,stx,sty,edx,edy;
int sc[1010][1010];
int f[1010][1010],g[1010][1010];
deque<line>q;
inline void chkmin(int &x,int y){if(y<x)x=y;}
inline int dis(line L,int x){return L.a*x+L.b;}
inline bool check(line L,line L1,line L2){
	return (L.b-L1.b)*(L2.a-L.a)<=(L.b-L2.b)*(L1.a-L.a);
}
const int dx[]={1,0,-1,0};
const int dy[]={0,1,0,-1};
vector<pair<int,int> >v[1010*1010*2];
queue<pair<int,int> >Q;
bool ok[1010][1010];
int main(){
	ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
	cin>>w>>h>>sty>>stx>>edy>>edx; 
	stx--;sty--;edx--;edy--;
	cin>>n;
	memset(sc,-1,sizeof(sc));
	for(int i=0;i<n;i++){
		int x,y;
		cin>>y>>x;
		x--;y--;
		sc[x][y]=i;
	}
	for(int i=0;i<h;i++)for(int j=0;j<w;j++)f[i][j]=1010;
	for(int i=0;i<h;i++){
		int pos=1010;
		for(int j=0;j<w;j++){
			if(sc[i][j]!=-1)pos=0;
			else pos++;
			chkmin(f[i][j],pos);
		}
	}
	for(int i=0;i<h;i++){
		int pos=1010;
		for(int j=w-1;j>=0;j--){
			if(sc[i][j]!=-1)pos=0;
			else pos++;
			chkmin(f[i][j],pos);
		}
	}
	for(int j=0;j<w;j++){
		for(int i=0;i<h;i++){
			line L=(line){-i,i*i+f[i][j]*f[i][j]};
			while(q.size()>=2){
				line L1=q[q.size()-1];
				line L2=q[q.size()-2];
				if(check(L,L1,L2))q.pop_back();
				else break;
			}
			q.push_back(L);
		}
		for(int i=0;i<h;i++){
			while(q.size()>=2&&dis(q[0],2*i)>dis(q[1],2*i))q.pop_front();
			if(!q.empty())g[i][j]=dis(q[0],2*i)+i*i;
		}
		while(!q.empty())q.pop_back();
	}
	ok[stx][sty]=true;
	v[g[stx][sty]].push_back(make_pair(stx,sty));
	for(int i=h*h+w*w;i>=0;i--){
		while(!Q.empty())Q.pop();
		for(int j=0;j<(int)v[i].size();j++)Q.push(v[i][j]);
		while(!Q.empty()){
			int x=Q.front().first,y=Q.front().second;
			if(x==edx&&y==edy){
				cout<<i<<endl;
				return 0;
			}
			Q.pop();
			for(int d=0;d<4;d++){
				int nx=x+dx[d],ny=y+dy[d];
				if(nx<0||nx>=h||ny<0||ny>=w||ok[nx][ny])continue;
				ok[nx][ny]=true;	
				if(g[nx][ny]>=i)Q.push(make_pair(nx,ny));				
				else v[g[nx][ny]].push_back(make_pair(nx,ny));
			}
		}
	}
	return 0;
}
/*inline? ll or int? size? min max?*/
乱搞1

from sjc

考虑用 dijkstra 的做法. 考虑从周围 4 个转移而来,此时会 wa 5个点. 如果周围 8 个,就会 wa 1 个点.

如果周围 8 个点,每个保留 3 个转移点,此时就可以ac.

乱搞2

from tzc

考虑双端队列. pop_back()时忘了怎么左……. 每次转移从前30个点转移过来. 就可以ac.

小结

这是一道非常好的题目,从 \(f\) \(g\) 的设定到推出斜率优化的式子都非常优美.

如果是独立思考的话,考虑枚举行数,用 \(O(1)\)\(O(log(w))\) 地求出对于一行稻草人的最小值.

这是我第一道独立写出的斜率优化题,今后几天也会练习一些斜率优化的题目.

其他

参考

other people's 斜率优化

#include<iostream>

typedef long long lld;
const int N=1011,inf=N*3;
int n,m,pedge,f[N][N],g[N][N],input[N],output[N],head[N*N*2+1];
bool data[N][N],used[N][N];
struct point{
	int x,y;
	point operator-(const point&a)const{
		return(point){x-a.x,y-a.y};
	}
	point operator+(const point&a)const{
		return(point){x+a.x,y+a.y};
	}
	lld operator*(const point&a)const{
		return lld(x)*a.y-lld(y)*a.x;
	}
}S,T,que[N*N];//向量 
struct Edge{
	point ver;
	int next;
}edge[N*N];
const point dir[]={(point){0,-1},(point){0,1},(point){1,0},(point){-1,0}};

void init(){
	int p,x,y;
	scanf("%d%d%d%d%d%d%d",&n,&m,&S.x,&S.y,&T.x,&T.y,&p);
	for(int i=1;i<=p;i++){
		scanf("%d%d",&x,&y);
		data[x][y]=true;
	}
}

int sqr(int a){
	return a*a;
}

void minit(int&a,int b){
	if(a>b)a=b;
}

void calc_f(){
	int i,j,t;
	for(i=1;i<=n;i++){
		t=-inf;
		for(j=1;j<=m;j++){
			if(data[i][j])
				t=j;
			f[i][j]=sqr(j-t);
		}
		t=inf;
		for(j=m;j>=1;j--){
			if(data[i][j])
				t=j;
			minit(f[i][j],sqr(j-t));
		}
	}//用两次扫描的方法求出f 
}

void solve(){
	int cl=1,op=0;
	point cur;
	for(int i=1;i<=n;i++){
		cur=(point){i,input[i]+sqr(i)};
		while(cl<op&&(que[op]-que[op-1])*(cur-que[op])<=0)
			op--;
		que[++op]=cur;
		while(cl<op&&que[cl].y-(i<<1)*que[cl].x>que[cl+1].y-(i<<1)*que[cl+1].x)
			cl++;
		output[i]=que[cl].y-(i<<1)*que[cl].x+i*i;
	}
}

void calc_g(){//用凸包的方法求出g 
	int i,j;
	for(i=1;i<=m;i++){
		for(j=1;j<=n;j++)
			input[j]=f[j][i];
		solve();
		for(j=1;j<=n;j++)
			g[j][i]=output[j];
		for(j=1;j<=n;j++)
			input[n-j+1]=f[j][i];
		solve();
		for(j=1;j<=n;j++)
			minit(g[j][i],output[n-j+1]);
	}
}

bool can(const point&a){
	bool flag=used[a.x][a.y];
	used[a.x][a.y]=true;
	return a.x>0&&a.y>0&&a.x<=n&&a.y<=m&&!flag;
}

void ins(const point&a){
	edge[++pedge]=(Edge){a,head[g[a.x][a.y]]};
	head[g[a.x][a.y]]=pedge;
}

int bfs(){
	int cl,op;
	point cur;
	used[S.x][S.y]=true;
	ins(S);
	for(int i=n*n+m*m;;i--){//从大到小枚举答案 
		op=0;
		for(int j=head[i];j;j=edge[j].next)
			que[++op]=edge[j].ver;//将该答案的链表中串联的数取出到队列中 
		for(cl=1;cl<=op;cl++){//进行广搜 
			if(que[cl].x==T.x&&que[cl].y==T.y) 
				return i;
			for(int j=0;j<4;j++)
				if(can(cur=que[cl]+dir[j]))
					if(g[cur.x][cur.y]>=i)//不小于答案则直接加入队列 
						que[++op]=cur;
					else//否则放入链表 
						ins(cur);
		}
	}
	return 0;
}

int main(){
	init();
	calc_f();//求出题目中的f 
	calc_g();//求出题目中的g 
	printf("%d\n",bfs());//广搜 
	fclose(stdout);return 0;
}
posted @ 2021-07-02 23:38  xyangh  阅读(21)  评论(0)    收藏  举报