凸包学习笔记

凸包,是指能包含点集中所有点的最小凸多边形(三维就是凸多面体)。显然,凸包的所有顶点都是点集中的点。凸包通常有两种出题套路,分别是计算几何(直接求有关凸包的信息)和决策单调性(类似于斜率优化 dp)。由于凸包本身并不好维护且不一定全都用得上,在信息学竞赛中,通常维护上凸壳或下凸壳。凸包可以有这两个凸壳拼接而得。下图是一只可爱的凸包:

该图中,红线围起的多边形为凸包,蓝色点与其连线为上凸壳,绿色点与其连线为下凸壳。

一、凸包构建方法

注:该版块内容仅讨论上凸壳。

1.静态构建

首先,我们按照 \(x\) 轴坐标进行排序,然后按顺序一个一个插入点。

我们假设一条线段也是凸壳,那么我们先将前两个点加入上凸壳,然后考虑拓展这个上凸壳:

  1. 斜率减小
    下图就是一个斜率减小的例子:

    此时,若采用绿色线段的模式,将第二个蓝色点扔出上凸壳,显然不符合凸包包含所有点的要求,所以直接加入黑点即可。同时,根据这一事实,我们也可以推断出:上凸壳每条边斜率递减。相对应的,下凸壳每条边斜率递增。
  2. 斜率增大
    下图就是一个斜率增大的例子:

    我们发现,假如我们采取红色折线的模式,那么这个凸包就不是一个凸多边形了,所以要将第二个橘色点扔出上凸壳。同时要注意的是,假如仍然保留第一个橘色点,这个凸包也不是一个多边形,所以第一个橘色点也要舍去。说明在舍去一个点后,对于此时凸包最右侧的点是否保留,需要继续递归处理,直至上凸壳只剩两个点。

根据上述讨论,我们总结出了上凸壳的构建方式:

  1. 对所有点根据 \(x\) 坐标值进行升序排序。
  2. 直接加入前两个点。
  3. 将当前第二靠右的点 \(a\),第一靠右的点 \(b\) 和当前枚举点 \(c\) 进行比较:
    1. 若当前上凸壳中点数为 \(1\),跳出循环;
    2. 当线段 \((a,b)\) 的斜率大于线段 \((b,c)\) 的斜率时,直接加入点 \(c\),跳出循环;
    3. 否则弹出 \(b\),继续循环。

这和单调栈如出一辙。时间复杂度瓶颈为排序,因此时间复杂度为 \(O(n\log n)\),单调栈部分则为 \(O(n)\)。假如要求时间复杂度非均摊,我们可以用二分找出进行完 \(3\) 操作后的栈顶,单次时间复杂度为 \(O(\log n)\)(这类题有 [NOI2014] 购票)。

cpp

   //单调栈法

   #define dx(x,y) (xc[x]-xc[y])

   #define dy(x,y) (yc[x]-yc[y])

   int n,xc[N],yc[N],id[N],st[N],tp;

   int check(int x,int y,int z){

   	return dx(z,y)*dy(y,x)<=dy(z,y)*dx(y,x)

   }int cmp(int x,int y){

   	return xc[x]<xc[y];

   }int main(){

   	……………………………………

   	for(int i=1;i<=n;i++) id[i]=i;

   	sort(id+1,id+n+1,cmp);

   	for(int i=1;i<=n;st[++tp]=id[i++])

   		while(tp>1&&check(st[tp-1],st[tp],id[i])) tp--;

   }
cpp

   //二分法(摘自本人 [NOI2014] 购票 代码)

   //实际上,不能均摊的情况基本上只有要求可撤销的时候。

   struct mstack{

   	int tp=-1;vector<int>st;

   	db sp(int x,int y){

   		return 1.0*(f[x]-f[y])/(d[x]-d[y]);

   	}int cmp(int x,int y,int z){

   		return sp(x,y)>=sp(y,z);

   	}void add(int x,int id){

   		int l=1,r=tp,ans=tp+1;

   		while(l<=r){

   			int mid=(l+r)/2;

   			if(cmp(st[mid-1],st[mid],x))

   				ans=mid,r=mid-1;

   			else l=mid+1;

   		}if(ans>tp) st.push_back(0);

   		sk[++ft]={id,tp,st[ans]},st[tp=ans]=x; 

   	}//插入

   	int ans(int x){

   		if(tp<0) return (int)9e18;

   		int l=0,r=tp-1,ans=st[tp];

   		while(l<=r){

   			int mid=(l+r)/2;

   			if(sp(st[mid],st[mid+1])>x)

   				ans=st[mid],r=mid-1;

   			else l=mid+1;

   		}return f[ans]-d[ans]*x;

   	}//查询

   };

2.动态加点

这个时候我们就不能按照刚才的方式事先排序了。因此我们需要一个能快速找到前驱后继,支持加点和删除的数据结构。视题目而言,可以采用 \(set\)(如 [HAOI2011] 防线修建)或各种平衡树进行维护(如 [NOI2007] 货币兑换)。

cpp

   //set 版本(摘自本人 [HAOI2011] 防线修建 代码)

   //写 set 就要注意一些边界条件,以防 RE

   struct idx{int x;};

   int n,q,xc[N],yc[N];

   bool operator<(idx x,idx y){

   	return xc[x.x]==xc[y.x]?yc[x.x]<yc[y.x]:xc[x.x]<xc[y.x];

   }set<idx>s;

   int cmp(int x,int y){

   	return xc[x]==xc[y]?yc[x]<yc[y]:xc[x]<xc[y];

   }int check(int x,int y,int z){

   	return dy(y,x)*dx(z,y)<=dy(z,y)*dx(y,x);

   }void add(int x){

   	auto itl=s.lower_bound({x}),itr=itl;itl--;

   	if(check(itl->x,x,itr->x)) return;

   	while(1){

   		itl=s.lower_bound({x}),itr=--itl;

   		if(itr==s.begin()) break;itl--;

   		if(!check(itl->x,itr->x,x)) break;

   		s.erase(itr);

   	}while(1){

   		itr=s.upper_bound({x}),itl=itr++;

   		if(itr==s.end()) break;

   		if(!check(x,itl->x,itr->x)) break;

   		s.erase(itl);

   	}s.insert({x});

   }
cpp

   //平衡树(此为 fhq-treap)版本

   //这个代码量就会很大,但更灵活,边角条件也没那么复杂

   const db eps=1e-6;

   int n;db mx;

   struct dot{db x,y;}fs,ed;

   namespace FHQ{

   	#define ls(x) pl[x].ls

   	#define rs(x) pl[x].rs

   	#define sz(x) pl[x].sz

   	#define rk(x) pl[x].rk

   	#define xc(x) pl[x].xc

   	#define yc(x) pl[x].yc

   	struct fhq{

   		int ls,rs,sz,rk;

   		db xc,yc;

   	}pl[N];int rt,tl;

   	int mk(db a,db b){

   		return pl[++tl]={0,0,1,rand(),a,b},tl;

   	}void push_up(int x){

   		sz(x)=sz(ls(x))+sz(rs(x))+1; 

   	}void spilt(int x,db sp,int &a,int &b){

   		if(!x) return a=b=0,void();

   		if(xc(x)<=sp) a=x,spilt(rs(x),sp,rs(x),b);

   		else b=x,spilt(ls(x),sp,a,ls(x));push_up(x);

   	}int merge(int x,int y){

   		if(!x||!y) return x|y;

   		if(rk(x)<rk(y)) return rs(x)=merge(rs(x),y),push_up(x),x;

   		return ls(y)=merge(x,ls(y)),push_up(y),y;

   	}void insert(db x,db y){

   		int a,b;spilt(rt,x,a,b);

   		rt=merge(merge(a,mk(x,y)),b);

   	}void erase(db x,db y){

   		int a,b,c;spilt(rt,x-eps,a,b);

   		spilt(b,x,b,c),rt=merge(a,c);

   	}int kth(int x,int k){

   		if(k<=sz(ls(x))) return kth(ls(x),k);

   		if(k==sz(ls(x))+1) return x;

   		return kth(rs(x),k-sz(ls(x))-1);

   	}int hv(dot x){

   		int a,b,c;spilt(rt,x.x,a,c);

   		spilt(a,x.x-eps,a,b);int re=sz(b);

   		return rt=merge(merge(a,b),c),re;

   	}

   }dot pre(dot x,db dl=0){

   	int a,b;FHQ::spilt(FHQ::rt,x.x-dl,a,b);

   	int kt=FHQ::kth(a,FHQ::sz(a));dot re={FHQ::xc(kt),FHQ::yc(kt)};

   	return FHQ::rt=FHQ::merge(a,b),re;

   }dot nxt(dot x,db ad=0){

   	int a,b;FHQ::spilt(FHQ::rt,x.x+ad,a,b);

   	int kt=FHQ::kth(b,1);dot re={FHQ::xc(kt),FHQ::yc(kt)};

   	return FHQ::rt=FHQ::merge(a,b),re;

   }int check(dot a,dot b,dot c){

   	return (c.x-b.x)*(b.y-a.y)<=(b.x-a.x)*(c.y-b.y);

   }bool operator==(dot x,dot y){

   	return x.x==y.x&&x.y==y.y;

   }void solve(int abc){

   	int l=2,r=FHQ::sz(FHQ::rt);

   	db ak,bk,rk,ans=0;cin>>ak>>bk>>rk;

   	while(l<=r){

   		int mid=(l+r)/2,ida=FHQ::kth(FHQ::rt,mid);

   		int idb=FHQ::kth(FHQ::rt,mid-1);

   		db xa=FHQ::xc(ida),ya=FHQ::yc(ida);

   		db xb=FHQ::xc(idb),yb=FHQ::yc(idb);

   		if(xa*ak+ya*bk<=xb*ak+yb*bk) r=mid-1;

   		else l=mid+1,ans=xa*ak+ya*bk;

   	}if(abc!=1) mx=max({ans,fs.x*ak+fs.y*bk,mx});

   	dot x={mx*rk/(ak*rk+bk),mx/(ak*rk+bk)};

   	if(!FHQ::rt) return fs=ed=x,FHQ::insert(x.x,x.y);

   	if(fs.x>x.x||(fs.x==x.x&&fs.y<x.y)) fs=x;

   	else if(ed.x<x.x||(ed.x==x.x&&ed.y<x.y)) ed=x;

   	else if(check(pre(x),x,nxt(x,eps))) return;dot lst={0,0};

   	if(!(fs==x)) while(1){

   		dot pr=pre(x),pe;

   		if(pr==fs) break;pe=pre(pr,eps);

   		if(!check(pe,pr,x)) break;

   		FHQ::erase(pr.x,pr.y);

   	}if(!(ed==x)) while(1){

   		dot nx=nxt(x),nt;

   		if(nx==ed) break;nt=nxt(nx,eps);

   		if(!check(x,nx,nt)) break;

   		FHQ::erase(nx.x,nx.y);

   	}FHQ::insert(x.x,x.y);

   }

3.删除操作

这是凸包维护中比较困难的部分,目前遇到过两种:

  1. 线段树分治最伟大!(如 [CTSC2016] 时空旅行,适合决策单调性一类的)
  2. 多层凸包,剥完一层还有一层(如 [NOI2017] 分身术,适合计算几何题)

这种就主要看题目,灵活采用了。

二、凸壳解决决策单调性问题

这基本上是经典了。实际上,斜率优化 \(dp\) 就是用凸壳解决了 \(dp\) 的单调性问题。

基本思路是维护上/下凸壳,然后对于询问用二分找到答案。有时遇到涉及区间求值的问题,我们就可以再套一个线段树,每个线段树区间内都维护一个凸壳。特定问题中,你甚至可以使用树状数组套凸壳。至于如何建模,我将会以 [SDOI2014] 向量集一题做具体讲解。

发现题目问 \((a_i,b_i)\cdot(x,y)\) 的最大值,那么选择第 \(i\) 个向量比第 \(j\) 个向量优,当且仅当(下设 \(a_i>a_j,y>0\)):

\[a_ix+b_iy>a_jx+b_jy \]

\[(b_i-b_j)y>-(a_i-a_j)x \]

\[\dfrac{b_i-b_j}{a_i-a_j}>-\dfrac xy \]

我们发现,假如我们将 \((a_i,b_i)\) 看作坐标系上的点,那么 \(\dfrac{b_i-b_j}{a_i-a_j}\) 相当于两点斜率:当斜率 \(>-\dfrac xy\) 时,\(a_i\) 大的更优;反之,\(a_i\) 小的更优。

这个时候,我们就会发现:假如我们建立所有点的上凸壳,那么无论任何时候,答案都在这个上凸壳上面。查询答案时,我们对上凸壳进行二分即可。当然,这都是 \(y>0\) 的情况,假如 \(y\le 0\),那我们就需要在下凸壳上二分了。

本题有区间查询,所以可以使用线段树套凸壳的方式进行维护。本题还涉及到一个小小的 \(trick\):当加入的点在末尾时,我们可以在一个区间被填满后再进行凸壳的建立,因为在加入最后一个点前,这个区间不可能被访问到。

综上,凸壳解决单调性问题的基本套路为:找到点的形式,判断上、下凸壳,选择数据结构。


__EOF__

原创作者: chang-an-22-lyh 转载于: https://www.cnblogs.com/chang-an-22-lyh/p/18954850/tu_bao-zj
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值