二维凸包学习笔记
二维凸包维护 学习笔记
斜率优化(维护的是上凸包或下凸包)
Part1
首先从经典的例题[P3195 HNOI2008]玩具装箱出发,我们可以用暴力 \(O(n^2)\) 的效率过掉 \(70pts\) 的分数,定义 \(dp[i]\) 为装完前 \(i\) 个玩具所需要的最小代价,具体的转移方程是:
具体意义就是当前这个玩具以及它前面包含的若干个,在第 \(j\) 个玩具之后重新组成一个新的箱子,即从 \(j\) 后面开始断开。
注意边界条件 \(dp\) 初值是正无穷,\(dp_0\)的初值是 \(0\) ,表示从 \(0\) 后面断开,即从 \(1\) 开始到 \(j\) 选连续的一段装到一个箱子里
#include<bits/stdc++.h>
#define int long long
using namespace std;
int a[50010],sum[50010],dp[50010];
signed main()
{
memset(dp,0x7f,sizeof dp);
int n,L;
scanf("%lld%lld",&n,&L);
for(register int i=1;i<=n;++i)scanf("%lld",&a[i]),sum[i]=sum[i-1]+a[i];
dp[0]=0;
for(register int i=1;i<=n;++i)
{
for(register int j=0;j<i;++j)
{
int x=sum[i]-sum[j]+i-j-1;
dp[i]=min(dp[i],dp[j]+(x-L)*(x-L));
}
}
printf("%lld",dp[n]);
return 0;
}
Part2
这样明显是过不掉所有数据的,那么就要用到决策单调性或者是斜率优化小的trick来把时间复杂度降低至 \(O(n)\) 或者是 \(O(n\log n)\)
还不会决策单调性,所以就斜率优化了,首先考虑斜率优化使用的条件。
使用条件
- 形如 \(dp[i]=a[j]+b[i]\) 等这种\(i,j\)并不互相影响的式子,我们对每一部分分别取最大值或者是最小值,最后得到的 \(dp[i]\) 一定是最优的,这个过程直接枚举 \(i,j\) 是 \(O(n^2)\) 的,但是我们只会使用最大值或者是最小值来更新当前,所以可以达到 \(O(n)\) 的效率
- 那么 \(dp[i]=a[i]\times b[j]+c[j]\) 这种 \(i,j\) 被搞到同一个单项式里的情况,我们再贪心地选择就不一定正确了,这时候就能用斜率优化
- 如 \(dp[i]=a[i]\times b[j]+b[i]\times a[j]+b[j]\) 这种式子,虽然满足\(i,j\) 被搞到同一个单项式里,但是也不能使用斜率优化,因为后文会提到,这个式子的斜率是不止一种的
最简单的斜率优化
从我们一个例子 \(dp[i]=\min (a[i]\times b[j]+c[j]+d[j])\) 出发,保证 \(a[i]<0\) ,我们该怎样让 \(dp[i]\) 通过斜率优化的方式变得最优呢?
首先观察到我们左边枚举的 \(dp\) 阶段是 \(i\), 那么后面所有和 \(i\) 有关的项就是确定的,在当前阶段就可以看成常数了,而后面一坨的 \(c[j]+d[j]\) 是不会受 \(i\) 影响的,所以可以看成一个整体因变量。
写出斜率式
我们利用初中学到的换元法,令: \(k=-a[i],y=c[j]+d[j],x=b[j]\)
那么再经过移项,原式子就可以变成: \(dp[i]=-kx+y \iff kx+dp[i]=y \iff y=kx+dp[i]\)
上式的意义在于:有一条斜率为 \(k\) ,过 \((x,y)\) 的直线,它的截距就是 \(dp[i]\),我们要求的就是对于这条斜率为 \(k=-a[i]>0\) 的直线,平面中存在若干个这种由 \(j\) 的值决定的 \((x,y)\) 点,直线过哪个点时的截距最小,如下图:
图片摘自洛谷巨佬@hhz6830975
很明显的是,我们一定只会在最外层的点里来寻找我们的答案,即我们经常说到的 “凸包”,且凸包上的点之间的斜率一定是具有单调性的 。
写出优劣条件(找最优决策点)
我们考虑当 \(j1<j2\) 时,在什么时候 \(j1\) 点的答案会比 \(j2\) 点劣,形式的表达为:
(注意这个地方如果 \(x_j\) 没有单调性的话,需要用平衡树来维护强制使其单增了,下面假设是具有单调性的)
也就是说当这两个点之间的斜率小于等于给出的斜率时,\(j_2\) 的答案一定是更优的,我们从头开始找,一直到第一个大于给出斜率的两点之间的斜率,此时的 \(j_1\) 就是我们所说的“最优决策点”。
- 如果给出的询问具有单调性,那么我们可以用双指针的思想线性求解
- 如果给出的询问没有单调性,那么我们只能在斜率中二分查询直到找到符合答案的斜率
最后再把 \(dp[i]\) 的答案用当前 \(j_1\) 根据计算式更新就行
更新凸包(不同情况下只用画画图就能很容易理解)
首先我们要清楚,对于凸包上所有的点,他们之间的连线一定能囊括当前所有的点(不管在不在凸包上面),现在想一想如何维护这个性质
对于当前的题设来说,假设当前加入的点为 \(new\) ,记 \(i,j\) 两点之间的斜率为 \(slope(i,j)\),\(new\) 的前后四个点分别为 \(x1,x2,x3,x4\)
- 先考虑所有存在于 \(new\) 之前的点,即 横坐标小于 \(new\):
while(head<tail&&slope(x2,x1)>=slope(new,x1))erase(x2)//tail--;
首先我们保证前面有两个以上的点(\(head<tail\)),否则更新就没有意义了,如果当前加入的边能够比前面的边囊括更多 点,那么它的斜率一定小于前面的边
-
再考虑存在于 \(new\) 之后的点,即 横坐标大于\(new\):
while(head<tail&&slope(x3,x4)<=slope(new,x4))erase(x3)//tail--;
同理,如果加入的边能比后面的边囊括更多的边,那么它的斜率一定大于后面的边
按照上面的规则来维护,就可以做到统计答案的同时又维护凸包了。
玩具装箱问题
分析转移方程
还是像斜率优化那样优先回到转移方程:
把它写成前缀和的形式:
把项按照 \(i,j\) 归类
同样的,我们把所有仅与 \(i\) 有关的项归类;所有和 \(j\) 有关的项分为 仅与 \(j\) 有关,和同时与 \(i,j\) 有关的项归类,可以整理出
其中\(a(i)=sum[i]+i-L-1,b[j]=sum[j]+j\),且和 \(i\) 直接有关的项都是确定的,所以依然可以看成斜率的形式
写出斜率式
其中 \(f(b(j))=dp[j]+b(j)^2,k=2a(i),t=dp[i]-a(i)^2\)
那么我们现在要做的就是对于给出的斜率,找出一个存在的点\((b(j),f(b(j)))\)使得截距 \(t\) 最小,并且斜率恒定为正且单增,所以我们要维护一个下凸包
分析优劣条件
设 \(j1<j2\),我们考虑什么时候 \(j1\) 比 \(j2\) 劣:
根据题设有
即当最后一个式子成立时,当前的 \(j_1\) 不如 \(j_2\) 优,所以我们可以二分查找到凸包上第一个斜率大于当前 \(k\) 的直线,这时的 \(j_1\) 一定是当前的最优解了。
然而我们注意到询问给出的 \(k\) 其实是具有单调性的(单增),所以我们直接 \(O(n)\) 双指针遍历即可
维护当前凸包
和我们在第一道例题里提到的基本是一样的,但是不需要后面的直线了,因为加入点的横坐标 \(b(j)\) 一定是单增的,所以加入的时候后面不会有边,只需要更新前面
非常重要的细节
我们在维护凸包和二分答案的时候会涉及到斜率,也就是浮点数的计算,或许你会想到转化成交叉相乘的结果,然后用long long 规避引入浮点数的问题,实际上这样是会爆掉的,所以要么使用__int128,要么就老老实实算斜率
Code
这一份是浮点数计算的
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<set>
#include<cstring>
#define int long long
using namespace std;
const int maxn=5e4+100;
int n,L;
double v[maxn],sum[maxn],dp[maxn];
int q[maxn],head,tail;
double f(int i){return sum[i]+i-L-1.0;}
double g(int j){return sum[j]+j;}
double m(int j){return dp[j]+g(j)*g(j);}
double k(int i){return f(i)*2.0;}
double slope(int i,int j){return 1.0*(m(i)-m(j))/(g(i)-g(j));}
signed main()
{
scanf("%lld%lld",&n,&L);
for(register int i=1;i<=n;++i)scanf("%lf",&v[i]),sum[i]=sum[i-1]+v[i];
q[head=tail=1]=0;
for(register int i=1;i<=n;++i)
{
while(head<tail&&slope(q[head],q[head+1])<=2.0*f(i))++head;
dp[i]=dp[q[head]]+(sum[i]+i-L-1-sum[q[head]]-q[head])*(sum[i]+i-L-1-sum[q[head]]-q[head]);
while(head<tail&&slope(q[tail],q[tail-1])>=slope(q[tail-1],i))--tail;
q[++tail]=i;
}
printf("%lld",(long long)dp[n]);
return 0;
}
这一份是__int128的
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<set>
#include<cstring>
#define int __int128
using namespace std;
inline int read()
{
int x=0,f=1;
char c=getchar();
while(!isdigit(c))
{
if(c=='-')f=-1;
c=getchar();
}
while(isdigit(c))
{
x=(x<<1)+(x<<3)+(c^48);
c=getchar();
}
return x*f;
}
const int maxn=5e4+100;
int n,L;
int v[maxn],sum[maxn],dp[maxn];
int q[maxn],head,tail;
int f(int i){return sum[i]+i-L-1;}
int g(int j){return sum[j]+j;}
int m(int j){return dp[j]+g(j)*g(j);}
int k(int i){return f(i)<<1;}
bool check(int j1,int j2,int i){return 2*f(i)*(g(j2)-g(j1))>=m(j2)-m(j1);}//j1<j2
bool check1(int k,int j,int i){return (m(j)-m(i))*(g(k)-g(i))>=(m(k)-m(i))*(g(j)-g(i));}
signed main()
{
n=read(),L=read();
for(register int i=1;i<=n;++i)v[i]=read(),sum[i]=sum[i-1]+v[i];
q[head=tail=1]=0;
for(register int i=1;i<=n;++i)
{
while(head<tail&&check(q[head],q[head+1],i))++head;
dp[i]=dp[q[head]]+(sum[i]+i-L-1-sum[q[head]]-q[head])*(sum[i]+i-L-1-sum[q[head]]-q[head]);
while(head<tail&&check1(i,q[tail],q[tail-1]))--tail;
q[++tail]=i;
}
printf("%lld",(long long)dp[n]);
return 0;
}
小Tip
- 如果化简出来 \(i,j\) 在一起的单项式数量超过 \(1\) ,就不能用斜率优化了,去学决策单调性。
- 在横坐标不单增的情况下只能用平衡树取代单调栈来维护凸包。
- 如果给出的斜率是随着加点单增的,那么可以不用二分查询而是双指针\(O(n)\)询问。
流程总结
- 写出 \(dp\) 转移方程(本人现在还是挂在这一步的阶段)。
- 把所有的项分为只和 \(i\) 有关(\(b\))、只和 \(j\) 有关(\(y\))、同时和 \(i,j\) 有关(\(x\)),并把 \(x\) 中与 \(i\) 有关的项作为斜率 \(k\)。
- 确定我们要让截距\(b\)更小还是更大,结合\(k\)的正负来敲定我们要维护上凸包还是下凸包。
- 从最初换完元的转移方程出发,根据题意比较\(j_1<j_2\)时,\(j_1\)劣于\(j_2\)的条件,进一步限定凸包的形态,并给我们的查询带来条件。
- 根据“4”来查询我们的答案,二分或者双指针,据题目而定,然后更新当前阶段的 \(dp\) 值。
- 根据性质,对于当前加入的点,维护凸包。
带修的凸包维护
题意
有三种操作,分别是插入二元组 \(x_i,y_i\),删除一个二元组,和给出一组 \(a,b\) 求所有存在的\(ax+by\)的最大值
分析
和斜率优化雷同的地方是,我们都是知道 \(x,y\),然后求一个最大值或者是最小值。于是我们就想着能不能把 \(ax+by\) 转化成一个更加“斜率”一点的形式,设 \(ax+by\) 的值为 \(m\) :\(ax+by=m,b!=0\)
所以同样有: \(y=-\dfrac{a}{b}x+\dfrac{m}{b}\)
这里就进一步把问题转化成跟斜率优化差不多的形式了,如果抛开删除操作不谈,甚至比斜率优化还要简单,根本就不用一些繁琐的计算,只用找到上凸包里面第一个斜率小于等于当前的直线,然后根据定义计算当前的 \(m\) 就行了。
对于加点的操作,我们要用到平衡树,因为这里的 \(x\) 并不是按照插入顺序单增的。
但是我们是存在删除操作的,光之巨人向我们这种蒟蒻引入了一种牛逼的数据结构维护方式——不带pushdown的线段树,通过每一个节点来维护不同时间戳的点集,查询的时候取并集就行了
具体实现
线段树
离线
Code
这并不是你的电脑的问题,就是我没有写完而已
本文来自博客园,作者:Hanggoash,转载请注明原文链接:https://www.cnblogs.com/Hanggoash/p/16811322.html