二维凸包学习笔记
二维凸包维护 学习笔记
斜率优化(维护的是上凸包或下凸包)
Part1
首先从经典的例题[P3195 HNOI2008]玩具装箱出发,我们可以用暴力
具体意义就是当前这个玩具以及它前面包含的若干个,在第
注意边界条件
#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来把时间复杂度降低至
还不会决策单调性,所以就斜率优化了,首先考虑斜率优化使用的条件。
使用条件
- 形如
等这种 并不互相影响的式子,我们对每一部分分别取最大值或者是最小值,最后得到的 一定是最优的,这个过程直接枚举 是 的,但是我们只会使用最大值或者是最小值来更新当前,所以可以达到 的效率 - 那么
这种 被搞到同一个单项式里的情况,我们再贪心地选择就不一定正确了,这时候就能用斜率优化 - 如
这种式子,虽然满足 被搞到同一个单项式里,但是也不能使用斜率优化,因为后文会提到,这个式子的斜率是不止一种的
最简单的斜率优化
从我们一个例子
首先观察到我们左边枚举的
写出斜率式
我们利用初中学到的换元法,令:
那么再经过移项,原式子就可以变成:
上式的意义在于:有一条斜率为
图片摘自洛谷巨佬@hhz6830975
很明显的是,我们一定只会在最外层的点里来寻找我们的答案,即我们经常说到的 “凸包”,且凸包上的点之间的斜率一定是具有单调性的 。
写出优劣条件(找最优决策点)
我们考虑当
(注意这个地方如果
也就是说当这两个点之间的斜率小于等于给出的斜率时,
- 如果给出的询问具有单调性,那么我们可以用双指针的思想线性求解
- 如果给出的询问没有单调性,那么我们只能在斜率中二分查询直到找到符合答案的斜率
最后再把
更新凸包(不同情况下只用画画图就能很容易理解)
首先我们要清楚,对于凸包上所有的点,他们之间的连线一定能囊括当前所有的点(不管在不在凸包上面),现在想一想如何维护这个性质
对于当前的题设来说,假设当前加入的点为
- 先考虑所有存在于
之前的点,即 横坐标小于 :
while(head<tail&&slope(x2,x1)>=slope(new,x1))erase(x2)//tail--;
首先我们保证前面有两个以上的点(
-
再考虑存在于
之后的点,即 横坐标大于 :while(head<tail&&slope(x3,x4)<=slope(new,x4))erase(x3)//tail--;
同理,如果加入的边能比后面的边囊括更多的边,那么它的斜率一定大于后面的边
按照上面的规则来维护,就可以做到统计答案的同时又维护凸包了。
玩具装箱问题
分析转移方程
还是像斜率优化那样优先回到转移方程:
把它写成前缀和的形式:
把项按照 归类
同样的,我们把所有仅与
其中
写出斜率式
其中
那么我们现在要做的就是对于给出的斜率,找出一个存在的点
分析优劣条件
设
根据题设有
即当最后一个式子成立时,当前的
然而我们注意到询问给出的
维护当前凸包
和我们在第一道例题里提到的基本是一样的,但是不需要后面的直线了,因为加入点的横坐标
非常重要的细节
我们在维护凸包和二分答案的时候会涉及到斜率,也就是浮点数的计算,或许你会想到转化成交叉相乘的结果,然后用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的
小Tip
- 如果化简出来
在一起的单项式数量超过 ,就不能用斜率优化了,去学决策单调性。 - 在横坐标不单增的情况下只能用平衡树取代单调栈来维护凸包。
- 如果给出的斜率是随着加点单增的,那么可以不用二分查询而是双指针
询问。
流程总结
- 写出
转移方程(本人现在还是挂在这一步的阶段)。 - 把所有的项分为只和
有关( )、只和 有关( )、同时和 有关( ),并把 中与 有关的项作为斜率 。 - 确定我们要让截距
更小还是更大,结合 的正负来敲定我们要维护上凸包还是下凸包。 - 从最初换完元的转移方程出发,根据题意比较
时, 劣于 的条件,进一步限定凸包的形态,并给我们的查询带来条件。 - 根据“4”来查询我们的答案,二分或者双指针,据题目而定,然后更新当前阶段的
值。 - 根据性质,对于当前加入的点,维护凸包。
带修的凸包维护
题意
有三种操作,分别是插入二元组
分析
和斜率优化雷同的地方是,我们都是知道
所以同样有:
这里就进一步把问题转化成跟斜率优化差不多的形式了,如果抛开删除操作不谈,甚至比斜率优化还要简单,根本就不用一些繁琐的计算,只用找到上凸包里面第一个斜率小于等于当前的直线,然后根据定义计算当前的
对于加点的操作,我们要用到平衡树,因为这里的
但是我们是存在删除操作的,光之巨人向我们这种蒟蒻引入了一种牛逼的数据结构维护方式——不带pushdown的线段树,通过每一个节点来维护不同时间戳的点集,查询的时候取并集就行了
具体实现
线段树
离线
Code
这并不是你的电脑的问题,就是我没有写完而已
本文来自博客园,作者:Hanggoash,转载请注明原文链接:https://www.cnblogs.com/Hanggoash/p/16811322.html
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· NetPad:一个.NET开源、跨平台的C#编辑器
· PowerShell开发游戏 · 打蜜蜂
· 凌晨三点救火实录:Java内存泄漏的七个神坑,你至少踩过三个!