二维凸包学习笔记

二维凸包维护 学习笔记

斜率优化(维护的是上凸包或下凸包)

Part1

首先从经典的例题[P3195 HNOI2008]玩具装箱出发,我们可以用暴力 O(n2) 的效率过掉 70pts 的分数,定义 dp[i] 为装完前 i 个玩具所需要的最小代价,具体的转移方程是:

dpi=min(dpi,dpj+(t=j+1ivt+i(j+1)L)2)

具体意义就是当前这个玩具以及它前面包含的若干个,在第 j 个玩具之后重新组成一个新的箱子,即从 j 后面开始断开。

注意边界条件 dp 初值是正无穷,dp0的初值是 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(nlogn)

还不会决策单调性,所以就斜率优化了,首先考虑斜率优化使用的条件。

使用条件

  • 形如 dp[i]=a[j]+b[i] 等这种i,j并不互相影响的式子,我们对每一部分分别取最大值或者是最小值,最后得到的 dp[i] 一定是最优的,这个过程直接枚举 i,jO(n2) 的,但是我们只会使用最大值或者是最小值来更新当前,所以可以达到 O(n) 的效率
  • 那么 dp[i]=a[i]×b[j]+c[j] 这种 i,j 被搞到同一个单项式里的情况,我们再贪心地选择就不一定正确了,这时候就能用斜率优化
  • dp[i]=a[i]×b[j]+b[i]×a[j]+b[j] 这种式子,虽然满足i,j 被搞到同一个单项式里,但是也不能使用斜率优化,因为后文会提到,这个式子的斜率是不止一种的

最简单的斜率优化

从我们一个例子 dp[i]=min(a[i]×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+ykx+dp[i]=yy=kx+dp[i]

上式的意义在于:有一条斜率为 k ,过 (x,y) 的直线,它的截距就是 dp[i],我们要求的就是对于这条斜率为 k=a[i]>0 的直线,平面中存在若干个这种由 j 的值决定的 (x,y) 点,直线过哪个点时的截距最小,如下图:
image

图片摘自洛谷巨佬@hhz6830975

很明显的是,我们一定只会在最外层的点里来寻找我们的答案,即我们经常说到的 “凸包”,且凸包上的点之间的斜率一定是具有单调性的 。

写出优劣条件(找最优决策点)

我们考虑当 j1<j2 时,在什么时候 j1 点的答案会比 j2 点劣,形式的表达为:

(注意这个地方如果 xj 没有单调性的话,需要用平衡树来维护强制使其单增了,下面假设是具有单调性的)

kixj1+yj1kixj2+yj2yj2yj1xj2xj1ki

也就是说当这两个点之间的斜率小于等于给出的斜率时,j2 的答案一定是更优的,我们从头开始找,一直到第一个大于给出斜率的两点之间的斜率,此时的 j1 就是我们所说的“最优决策点”。

  • 如果给出的询问具有单调性,那么我们可以用双指针的思想线性求解
  • 如果给出的询问没有单调性,那么我们只能在斜率中二分查询直到找到符合答案的斜率

最后再把 dp[i] 的答案用当前 j1 根据计算式更新就行

更新凸包(不同情况下只用画画图就能很容易理解)

首先我们要清楚,对于凸包上所有的点,他们之间的连线一定能囊括当前所有的点(不管在不在凸包上面),现在想一想如何维护这个性质

对于当前的题设来说,假设当前加入的点为 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--;

    同理,如果加入的边能比后面的边囊括更多的边,那么它的斜率一定大于后面的边

按照上面的规则来维护,就可以做到统计答案的同时又维护凸包了。

玩具装箱问题

分析转移方程

还是像斜率优化那样优先回到转移方程:

dpi=min(dpi,dpj+(t=j+1ivt+i(j+1)L)2)

把它写成前缀和的形式:

dpi=min(dpi,dpj+(sum[i]sum[j]+i(j+1)L)2)

把项按照 i,j 归类

同样的,我们把所有仅与 i 有关的项归类;所有和 j 有关的项分为 仅与 j 有关,和同时与 i,j 有关的项归类,可以整理出

dp[i]=2a(i)b(j)+(dp[j]+a(j)2)+a(i)22a(i)×b(j)+(dp[i]a(i)2)=(dp[j]+b(j)2)

其中a(i)=sum[i]+iL1,b[j]=sum[j]+j,且和 i 直接有关的项都是确定的,所以依然可以看成斜率的形式

写出斜率式

f(b(j))=k×b(j)+t

其中 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,我们考虑什么时候 j1j2 劣:

根据题设有

kb(j1)+(dp[j1]+a(j1)2)+a(i)2kb(j2)+(dp[j2]+a(j2)2)+a(i)2k(b(j2)b(j1))f(b(j2))f(b(j1)),b(j2)>b(j1)f(b(j2))f(b(j1))b(j2)b(j1)k

即当最后一个式子成立时,当前的 j1 不如 j2 优,所以我们可以二分查找到凸包上第一个斜率大于当前 k 的直线,这时的 j1 一定是当前的最优解了。

然而我们注意到询问给出的 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的

小Tip

  1. 如果化简出来 i,j 在一起的单项式数量超过 1 ,就不能用斜率优化了,去学决策单调性。
  2. 在横坐标不单增的情况下只能用平衡树取代单调栈来维护凸包。
  3. 如果给出的斜率是随着加点单增的,那么可以不用二分查询而是双指针O(n)询问。

流程总结

  1. 写出 dp 转移方程(本人现在还是挂在这一步的阶段)。
  2. 把所有的项分为只和 i 有关(b)、只和 j 有关(y)、同时和 i,j 有关(x),并把 x 中与 i 有关的项作为斜率 k
  3. 确定我们要让截距b更小还是更大,结合k的正负来敲定我们要维护上凸包还是下凸包。
  4. 从最初换完元的转移方程出发,根据题意比较j1<j2时,j1劣于j2的条件,进一步限定凸包的形态,并给我们的查询带来条件。
  5. 根据“4”来查询我们的答案,二分或者双指针,据题目而定,然后更新当前阶段的 dp 值。
  6. 根据性质,对于当前加入的点,维护凸包。

带修的凸包维护

题意

有三种操作,分别是插入二元组 xi,yi,删除一个二元组,和给出一组 a,b 求所有存在的ax+by的最大值

分析

和斜率优化雷同的地方是,我们都是知道 x,y,然后求一个最大值或者是最小值。于是我们就想着能不能把 ax+by 转化成一个更加“斜率”一点的形式,设 ax+by 的值为 max+by=m,b!=0

所以同样有: y=abx+mb

这里就进一步把问题转化成跟斜率优化差不多的形式了,如果抛开删除操作不谈,甚至比斜率优化还要简单,根本就不用一些繁琐的计算,只用找到上凸包里面第一个斜率小于等于当前的直线,然后根据定义计算当前的 m 就行了。

对于加点的操作,我们要用到平衡树,因为这里的 x 并不是按照插入顺序单增的。

但是我们是存在删除操作的,光之巨人向我们这种蒟蒻引入了一种牛逼的数据结构维护方式——不带pushdown的线段树,通过每一个节点来维护不同时间戳的点集,查询的时候取并集就行了

具体实现

线段树

离线

Code

这并不是你的电脑的问题,就是我没有写完而已

posted @   Hanggoash  阅读(38)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· NetPad:一个.NET开源、跨平台的C#编辑器
· PowerShell开发游戏 · 打蜜蜂
· 凌晨三点救火实录:Java内存泄漏的七个神坑,你至少踩过三个!
动态线条
动态线条end
点击右上角即可分享
微信分享提示