凸包,是指能包含点集中所有点的最小凸多边形(三维就是凸多面体)。显然,凸包的所有顶点都是点集中的点。凸包通常有两种出题套路,分别是计算几何(直接求有关凸包的信息)和决策单调性(类似于斜率优化 dp)。由于凸包本身并不好维护且不一定全都用得上,在信息学竞赛中,通常维护上凸壳或下凸壳。凸包可以有这两个凸壳拼接而得。下图是一只可爱的凸包:
该图中,红线围起的多边形为凸包,蓝色点与其连线为上凸壳,绿色点与其连线为下凸壳。
一、凸包构建方法
注:该版块内容仅讨论上凸壳。
1.静态构建
首先,我们按照 \(x\) 轴坐标进行排序,然后按顺序一个一个插入点。
我们假设一条线段也是凸壳,那么我们先将前两个点加入上凸壳,然后考虑拓展这个上凸壳:
- 斜率减小
下图就是一个斜率减小的例子:
此时,若采用绿色线段的模式,将第二个蓝色点扔出上凸壳,显然不符合凸包包含所有点的要求,所以直接加入黑点即可。同时,根据这一事实,我们也可以推断出:上凸壳每条边斜率递减。相对应的,下凸壳每条边斜率递增。 - 斜率增大
下图就是一个斜率增大的例子:
我们发现,假如我们采取红色折线的模式,那么这个凸包就不是一个凸多边形了,所以要将第二个橘色点扔出上凸壳。同时要注意的是,假如仍然保留第一个橘色点,这个凸包也不是一个多边形,所以第一个橘色点也要舍去。说明在舍去一个点后,对于此时凸包最右侧的点是否保留,需要继续递归处理,直至上凸壳只剩两个点。
根据上述讨论,我们总结出了上凸壳的构建方式:
- 对所有点根据 \(x\) 坐标值进行升序排序。
- 直接加入前两个点。
- 将当前第二靠右的点 \(a\),第一靠右的点 \(b\) 和当前枚举点 \(c\) 进行比较:
- 若当前上凸壳中点数为 \(1\),跳出循环;
- 当线段 \((a,b)\) 的斜率大于线段 \((b,c)\) 的斜率时,直接加入点 \(c\),跳出循环;
- 否则弹出 \(b\),继续循环。
这和单调栈如出一辙。时间复杂度瓶颈为排序,因此时间复杂度为 \(O(n\log n)\),单调栈部分则为 \(O(n)\)。假如要求时间复杂度非均摊,我们可以用二分找出进行完 \(3\) 操作后的栈顶,单次时间复杂度为 \(O(\log n)\)(这类题有 [NOI2014] 购票)。
//单调栈法
#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--;
}
//二分法(摘自本人 [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] 货币兑换)。
//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});
}
//平衡树(此为 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.删除操作
这是凸包维护中比较困难的部分,目前遇到过两种:
- 线段树分治最伟大!(如 [CTSC2016] 时空旅行,适合决策单调性一类的)
- 多层凸包,剥完一层还有一层(如 [NOI2017] 分身术,适合计算几何题)
这种就主要看题目,灵活采用了。
二、凸壳解决决策单调性问题
这基本上是经典了。实际上,斜率优化 \(dp\) 就是用凸壳解决了 \(dp\) 的单调性问题。
基本思路是维护上/下凸壳,然后对于询问用二分找到答案。有时遇到涉及区间求值的问题,我们就可以再套一个线段树,每个线段树区间内都维护一个凸壳。特定问题中,你甚至可以使用树状数组套凸壳。至于如何建模,我将会以 [SDOI2014] 向量集一题做具体讲解。
发现题目问 \((a_i,b_i)\cdot(x,y)\) 的最大值,那么选择第 \(i\) 个向量比第 \(j\) 个向量优,当且仅当(下设 \(a_i>a_j,y>0\)):
我们发现,假如我们将 \((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__
- 本文作者: 长安一片月_22
- 本文链接: https://www.cnblogs.com/chang-an-22-lyh/p/18954850/tu_bao-zj
- 关于博主: 评论和私信会在第一时间回复。或者直接私信我。
- 版权声明: 除特殊说明外,转载请注明出处~[知识共享署名-相同方式共享 4.0 国际许可协议]
- 声援博主: 如果您觉得文章对您有帮助,可以点击文章右下角【推荐】一下。