斜率/凸包板子
先开个板子博再说。
除了计算几何,其他板块中的“凸包”都指“凸壳”。
计算几何中的凸包维护
开始以为专题里“凸包”指的是计算几何,就先把板子写了。结果题全是斜率优化。。
两个向量 &
表示点积, *
表示叉积。
求二维凸包用的是 \(Andrew\) 算法,把凸包分成两个凸壳维护。将点按坐标排序后每次检查当前点是否在当前栈顶线段的右侧。如果在右侧则当前点优于栈顶,弹栈。
判断左右关系可以用叉积。正着做一遍倒着做一遍就可以得到一个完整的凸包。
洛谷P2742[模板]二维凸包
#include<bits/stdc++.h>
using namespace std;
namespace IO{
typedef double DB;
typedef long long LL;
LL read(){
LL x=0,f=0; char ch=getchar();
while(ch>'9'||ch<'0'){ f|=(ch=='-'); ch=getchar(); }
while(ch>='0'&&ch<='9'){ x=(x<<1)+(x<<3)+(ch^48); ch=getchar(); }
return f?-x:x;
} char output[50];
void write(LL x,char sp){
int len=0;
if(x<0) putchar('-'), x=-x;
do{ output[len++]=x%10+'0'; x/=10; }while(x);
for(int i=len-1;~i;i--) putchar(output[i]); putchar(sp);
}
void ckmax(int& x,int y){ x=x>y?x:y; }
void ckmin(int& x,int y){ x=x<y?x:y; }
} using namespace IO;
const int NN=100010;
int n,siz;
DB ans;
namespace Geometry{
#define pot vec
const DB eps=1e-8;
struct vec{
DB x,y;
vec(){ x=y=0; }
vec(DB _x){ x=_x; y=0; }
vec(DB _x,DB _y){ x=_x; y=_y; }
vec operator-()const{ return vec(-x,-y); }
vec operator+(const vec& t)const{ return vec(x+t.x,y+t.y); }
vec operator-(const vec& t)const{ return vec(x-t.x,y-t.y); }
vec operator*(const DB& t)const{ return vec(x*t,y*t); }
vec operator/(const DB& t)const{ return vec(x/t,y/t); }
DB operator&(const vec& t)const{ return x*t.x+y*t.y; }
DB operator*(const vec& t)const{ return x*t.y-y*t.x; }
}p[NN],hul[NN];
void read(vec& t){ scanf("%lf%lf",&t.x,&t.y); }
bool cmpx(vec a,vec b){ return a.x!=b.x?(a.x<b.x):(a.y<b.y); }
bool left(pot fst,pot snd,pot nxt){ return (snd-fst)*(nxt-fst)>0; }
DB dis(pot x,pot y){ return sqrt((x.x-y.x)*(x.x-y.x)+(x.y-y.y)*(x.y-y.y)); }
int gethull(int top=1,int lim=0){
sort(p+1,p+n+1,cmpx);
hul[0]=p[1]; hul[1]=p[2];
for(int i=3;i<=n;i++){
while(top>lim&&!left(hul[top-1],hul[top],p[i])) --top;
hul[++top]=p[i];
}
lim=top; hul[++top]=p[n-1];
for(int i=n-2;i;i--){
while(top>lim&&!left(hul[top-1],hul[top],p[i])) --top;
hul[++top]=p[i];
}
return top;
}
} using namespace Geometry;
signed main(){
n=read();
for(int i=1;i<=n;i++) read(p[i]);
if(n==1) return puts("0"),0;
if(n==2) return printf("%.2lf\n",dis(p[1],p[2])),0;
siz=gethull();
for(int i=1;i<=siz;i++) ans+=dis(hul[i-1],hul[i]);
printf("%.2lf\n",ans);
return 0;
}
李超线段树
虽然好像不是凸包,但跟斜率相关主要是无脑且好用,还是提一下。很多斜率优化的题整出来式子后都可以直接上李超树。下面很多维护凸包的方式实际上也能用李超树代替。
只要没什么太毒瘤的限制一般斜率优化都可以用,平时的斜率优化式子交换 \(y\) 与纵截距 \(b\) 之后就能套到李超树里。
好像可以区间查,但目前还不会···
区间插线段两个 \(\log\) ,如果插直线则为一个。
[NOI2007]货币兑换
#include<bits/stdc++.h>
using namespace std;
namespace IO{
typedef double DB;
typedef long long LL;
typedef pair<int,int>PII;
LL read(){
LL x=0,f=0; char ch=getchar();
while(ch>'9'||ch<'0'){ f|=(ch=='-'); ch=getchar(); }
while(ch>='0'&&ch<='9'){ x=(x<<1)+(x<<3)+(ch^48); ch=getchar(); }
return f?-x:x;
} char output[50];
void write(LL x,char sp){
int len=0;
if(x<0) putchar('-'), x=-x;
do{ output[len++]=x%10+'0'; x/=10; }while(x);
for(int i=len-1;~i;i--) putchar(output[i]); putchar(sp);
}
void ckmax(LL& x,LL y){ x=x>y?x:y; }
void ckmin(LL& x,LL y){ x=x<y?x:y; }
} using namespace IO;
const int NN=200010,TN=NN<<2;
int n;
DB f,h[NN],a[NN],b[NN],r[NN];
namespace LC_SegmentTree{
#define ld rt<<1
#define rd (rt<<1)|1
struct seg{
DB k,b;
seg(){} seg(DB _k,DB _b){ k=_k; b=_b; }
DB val(DB x){ return x*k+b; }
}s[TN];
void build(int rt=1,int l=1,int r=n){
s[rt]=seg(0,-1e18);
if(l==r) return;
int mid=l+r>>1;
build(ld,l,mid); build(rd,mid+1,r);
}
void insert(seg v,int opl=1,int opr=n,int rt=1,int l=1,int r=n){
int mid=l+r>>1;
if(l==r){ if(s[rt].val(h[l])<v.val(h[l])) s[rt]=v; return; }
if(l>=opl&&r<=opr){
if(s[rt].val(h[l])<=v.val(h[l])&&s[rt].val(h[r])<=v.val(h[r])){ s[rt]=v; return; }
if(s[rt].val(h[l])>=v.val(h[l])&&s[rt].val(h[r])>=v.val(h[r])) return;
if(s[rt].val(h[mid])<v.val(h[mid])) swap(s[rt],v);
if(s[rt].val(h[l])<v.val(h[l])) insert(v,opl,opr,ld,l,mid);
else insert(v,opl,opr,rd,mid+1,r);
return;
}
if(opl<=mid) insert(v,opl,opr,ld,l,mid);
if(opr>mid) insert(v,opl,opr,rd,mid+1,r);
}
DB query(int pos,int rt=1,int l=1,int r=n){
DB res=s[rt].val(h[pos]);
int mid=l+r>>1;
if(l==r) return res;
if(pos<=mid) return max(res,query(pos,ld,l,mid));
else return max(res,query(pos,rd,mid+1,r));
}
} using namespace LC_SegmentTree;
signed main(){
n=read(); f=read();
for(int i=1;i<=n;i++){
scanf("%lf%lf%lf",&a[i],&b[i],&r[i]);
h[i]=a[i]/b[i];
}
sort(h+1,h+n+1); build();
for(int i=1;i<=n;i++){
int pos=lower_bound(h+1,h+n+1,a[i]/b[i])-h;
f=max(f,b[i]*query(pos));
DB x=f*r[i]/(a[i]*r[i]+b[i]),y=f/(a[i]*r[i]+b[i]);
insert(seg(x,y));
}
printf("%.3lf\n",f);
return 0;
}
线段树维护凸包
对于一些带有区间查询,且需要斜率优化的题目,通常都可以用线段树维护凸包解决。
如果题目可以离线,可以将所有待插入的点坐标排序后依次插进线段树。不然可以等线段树区间内所有位置都存在点之后再 pushup
来保证复杂度,因为不满的区间一定不会做贡献。
两个凸包合并后形成的凸包一定都是原来凸包上的点,所以 pushup
时直接把左右儿子区间的点全拿出来求一遍凸包即可。
[SDOI2014]向量集
#include<bits/stdc++.h>
using namespace std;
namespace IO{
#define x first
#define y second
typedef double DB;
typedef long long LL;
typedef pair<LL,LL>PII;
LL read(){
LL x=0,f=0; char ch=getchar();
while(ch>'9'||ch<'0'){ f|=(ch=='-'); ch=getchar(); }
while(ch>='0'&&ch<='9'){ x=(x<<1)+(x<<3)+(ch^48); ch=getchar(); }
return f?-x:x;
} char output[50];
void write(LL x,char sp){
int len=0;
if(x<0) putchar('-'), x=-x;
do{ output[len++]=x%10+'0'; x/=10; }while(x);
for(int i=len-1;~i;i--) putchar(output[i]); putchar(sp);
}
void ckmax(LL& x,LL y){ x=x>y?x:y; }
void ckmin(LL& x,LL y){ x=x<y?x:y; }
} using namespace IO;
const int NN=400010,TN=NN<<2;
int n,f,cnt;
LL ans;
char op;
namespace SegmentTree{
#define ld rt<<1
#define rd (rt<<1)|1
#define fst(x) x.back()
#define snd(x) x[x.size()-2]
vector<PII>tmp,hu[TN],hd[TN];
int siz[TN],len[TN];
LL calc(int rt,PII v){
if(!v.y) return max(v.x*hu[rt][0].x,v.x*fst(hu[rt]).x);
if(v.y>0){
int l=1,r=hu[rt].size()-1,res=0;
while(l<=r){
int mid=l+r>>1;
if((hu[rt][mid].y-hu[rt][mid-1].y)*v.y+
(hu[rt][mid].x-hu[rt][mid-1].x)*v.x>=0) res=mid,l=mid+1;
else r=mid-1;
}
return hu[rt][res].x*v.x+hu[rt][res].y*v.y;
} else{
int l=1,r=hd[rt].size()-1,res=0;
while(l<=r){
int mid=l+r>>1;
if((hd[rt][mid].y-hd[rt][mid-1].y)*v.y+
(hd[rt][mid].x-hd[rt][mid-1].x)*v.x>=0) res=mid,l=mid+1;
else r=mid-1;
}
return hd[rt][res].x*v.x+hd[rt][res].y*v.y;
}
}
void pushup(int rt){
if(siz[rt]^len[rt]) return;
tmp.clear();
for(auto x:hu[ld]) tmp.push_back(x);
for(auto x:hu[rd]) tmp.push_back(x);
sort(tmp.begin(),tmp.end());
for(auto x:tmp){
while(hu[rt].size()>1&&(fst(hu[rt]).y-snd(hu[rt]).y)*(x.x-fst(hu[rt]).x)<=
(fst(hu[rt]).x-snd(hu[rt]).x)*(x.y-fst(hu[rt]).y)) hu[rt].pop_back();
hu[rt].push_back(x);
}
tmp.clear();
for(auto x:hd[ld]) tmp.push_back(x);
for(auto x:hd[rd]) tmp.push_back(x);
sort(tmp.begin(),tmp.end());
for(auto x:tmp){
while(hd[rt].size()>1&&(fst(hd[rt]).y-snd(hd[rt]).y)*(x.x-fst(hd[rt]).x)>=
(fst(hd[rt]).x-snd(hd[rt]).x)*(x.y-fst(hd[rt]).y)) hd[rt].pop_back();
hd[rt].push_back(x);
}
}
void build(int rt=1,int l=1,int r=n){
len[rt]=r-l+1;
if(l==r) return;
int mid=l+r>>1;
build(ld,l,mid); build(rd,mid+1,r);
}
void insert(int pos,PII v,int rt=1,int l=1,int r=n){
++siz[rt];
if(l==r) return hu[rt].push_back(v),hd[rt].push_back(v);
int mid=l+r>>1;
if(pos<=mid) insert(pos,v,ld,l,mid);
else insert(pos,v,rd,mid+1,r);
pushup(rt);
}
LL query(int opl,int opr,PII v,int rt=1,int l=1,int r=n){
if(l>=opl&&r<=opr) return calc(rt,v);
int mid=l+r>>1; LL res=-1e18;
if(opl<=mid) ckmax(res,query(opl,opr,v,ld,l,mid));
if(opr>mid) ckmax(res,query(opl,opr,v,rd,mid+1,r));
return res;
}
} using namespace SegmentTree;
signed main(){
n=read(); cin>>op; f=(op!='E');
build();
for(int t=1;t<=n;t++){
op=getchar(); while((op^'A')&&(op^'Q')) op=getchar();
if(op=='A'){
LL x=read()^(f?(ans&0x7fffffff):0);
LL y=read()^(f?(ans&0x7fffffff):0);
insert(++cnt,{x,y});
} else{
LL x=read()^(f?(ans&0x7fffffff):0);
LL y=read()^(f?(ans&0x7fffffff):0);
int l=read()^(f?(ans&0x7fffffff):0);
int r=read()^(f?(ans&0x7fffffff):0);
write(ans=query(l,r,{x,y}),'\n');
}
}
return 0;
}
\(\texttt{set}\) 维护凸包
当凸包需要动态维护(加点)时,需要平衡树维护凸包。可以用 set
维护凸包简化代码。
每次加点时向左右分别扩展,检查左右的点是否需要被删去,最后检查当前点是否在凸包上。
[HAOI2011]防线修建
#include<bits/stdc++.h>
using namespace std;
namespace IO{
#define x first
#define y second
typedef double DB;
typedef long long LL;
typedef pair<int,int>PII;
LL read(){
LL x=0,f=0; char ch=getchar();
while(ch>'9'||ch<'0'){ f|=(ch=='-'); ch=getchar(); }
while(ch>='0'&&ch<='9'){ x=(x<<1)+(x<<3)+(ch^48); ch=getchar(); }
return f?-x:x;
} char output[50];
void write(LL x,char sp){
int len=0;
if(x<0) putchar('-'), x=-x;
do{ output[len++]=x%10+'0'; x/=10; }while(x);
for(int i=len-1;~i;i--) putchar(output[i]); putchar(sp);
}
void ckmax(LL& x,LL y){ x=x>y?x:y; }
void ckmin(LL& x,LL y){ x=x<y?x:y; }
} using namespace IO;
const int NN=200010;
int n,m,q;
PII p[NN];
bool ban[NN];
DB now,ans[NN];
struct opt{ int tp,id; }o[NN];
DB dis(PII a,PII b){ return sqrt(pow(a.x-b.x,2)+pow(a.y-b.y,2)); }
namespace Hull{
typedef set<PII>::iterator sit;
set<PII>s;
bool cmp(PII a,PII b,PII c){
int x=(b.y-a.y)*(c.x-b.x);
int y=(b.x-a.x)*(c.y-b.y);
return x>y;
}
void del(sit v){
sit l=--v; ++v; sit r=++v; --v;
now+=dis(*l,*r)-dis(*v,*r)-dis(*v,*l);
s.erase(v);
}
void add(PII u){
s.insert(u); sit v=s.find(u);
sit l=--v; ++v; sit r=++v; --v;
now+=dis(*v,*l)+dis(*v,*r)-dis(*l,*r);
}
void insert(PII v){
sit p=s.lower_bound(v);
if(p!=s.end()&&p->x==v.x)
if(p->y>=v.y) return;
else del(p);
while(!s.empty()){
p=s.lower_bound(v);
if(p==s.begin()||(--p)==s.begin()) break;
sit q=p; --p;
if(!cmp(*p,*q,v)) del(q);
else break;
}
while(!s.empty()){
p=s.lower_bound(v);
if(p==s.end()||p==(--s.end())) break;
sit q=p; ++p;
if(!cmp(v,*q,*p)) del(q);
else break;
}
p=s.lower_bound(v);
sit q=p; --p;
if(cmp(*p,v,*q)) add(v);
}
} using namespace Hull;
signed main(){
n=read();
p[1]={read(),0}; p[1].y=read();
m=read()+1; s.insert({0,0}); s.insert({n,0}); now=n;
for(int i=2;i<=m;i++) p[i]={read(),0}, p[i].y=read();
q=read();
for(int i=1;i<=q;i++){
o[i].tp=read();
if(o[i].tp==1) ban[o[i].id=read()+1]=1;
}
for(int i=1;i<=m;i++) if(!ban[i]) insert(p[i]);
for(int i=q;i;i--)
if(o[i].tp==1) insert(p[o[i].id]);
else ans[i]=now;
for(int i=1;i<=q;i++) if(o[i].tp==2) printf("%.2lf\n",ans[i]);
return 0;
}
CDQ分治进行斜率优化
在斜率优化时会遇到动态维护凸包的问题。这时除了使用 set
维护凸包外,也可以用CDQ分治解决。
具体流程:现将所有元素按斜率 \(k\) 排序,然后每次分治时将时间在前一半的元素放到左区间,递归左区间。
处理好左区间后解决左区间对右区间的贡献。对左区间建出凸包,现在因为右区间斜率单调,可以省去凸包上二分。
因为要建凸包,所以处理完当前区间后按横坐标 \(x\) 递增归并排序。
[NOI2007]货币兑换
#include<bits/stdc++.h>
using namespace std;
namespace IO{
typedef double DB;
typedef long long LL;
typedef pair<int,int>PII;
LL read(){
LL x=0,f=0; char ch=getchar();
while(ch>'9'||ch<'0'){ f|=(ch=='-'); ch=getchar(); }
while(ch>='0'&&ch<='9'){ x=(x<<1)+(x<<3)+(ch^48); ch=getchar(); }
return f?-x:x;
} char output[50];
void write(LL x,char sp){
int len=0;
if(x<0) putchar('-'), x=-x;
do{ output[len++]=x%10+'0'; x/=10; }while(x);
for(int i=len-1;~i;i--) putchar(output[i]); putchar(sp);
}
void ckmax(LL& x,LL y){ x=x>y?x:y; }
void ckmin(LL& x,LL y){ x=x<y?x:y; }
} using namespace IO;
const int NN=200010,TN=NN<<2;
const DB eps=1e-8,inf=1e10;
int n,stk[NN];
struct data{
DB k,x,y,a,b,r,f; int id;
bool operator<(const data& t)const{ return k<t.k; }
}d[NN],tmp[NN];
DB ans;
DB slope(int i,int j){
if(abs(d[i].x-d[j].x)<eps) return inf;
return (d[i].y-d[j].y)/(d[i].x-d[j].x);
}
void merge_sort(int l,int r){
int mid=l+r>>1,il=l,ir=mid+1;
for(int i=l;i<=r;i++){
if(il>mid) tmp[i]=d[ir++];
else if(ir>r) tmp[i]=d[il++];
else if(d[il].x<d[ir].x) tmp[i]=d[il++];
else tmp[i]=d[ir++];}
for(int i=l;i<=r;i++) d[i]=tmp[i];
}
void solve(int l,int r){
if(l==r){
d[l].f=max(d[l].f,ans); ans=max(ans,d[l].f);
d[l].y=d[l].f/(d[l].a*d[l].r+d[l].b);
d[l].x=d[l].y*d[l].r;
return;
}
int mid=l+r>>1,il=l,ir=mid+1,top=0;
for(int i=l;i<=r;i++)
d[i].id<=mid?tmp[il++]=d[i]:tmp[ir++]=d[i];
for(int i=l;i<=r;i++) d[i]=tmp[i];
solve(l,mid);
for(int i=l;i<=mid;i++){
while(top>1&&slope(stk[top],i)+eps>slope(stk[top-1],stk[top])) --top;
stk[++top]=i;
}
for(int i=mid+1;i<=r;i++){
while(top>1&&slope(stk[top],stk[top-1])<=d[i].k+eps) --top;
d[i].f=max(d[i].f,d[stk[top]].x*d[i].a+d[stk[top]].y*d[i].b);
}
solve(mid+1,r); merge_sort(l,r);
}
signed main(){
n=read(); ans=read();
for(int i=1;i<=n;i++){
scanf("%lf%lf%lf",&d[i].a,&d[i].b,&d[i].r);
d[i].k=-d[i].a/d[i].b; d[i].id=i;
}
sort(d+1,d+n+1); solve(1,n);
printf("%.3lf\n",ans);
return 0;
}