二维凸包学习笔记

二维凸包维护 学习笔记

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

Part1

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

\[dp_i=\min(dp_i,dp_j+(\sum_{t=j+1}^i v_t+i-(j+1)-L)^2) \]

具体意义就是当前这个玩具以及它前面包含的若干个,在第 \(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)\) 点,直线过哪个点时的截距最小,如下图:
image

图片摘自洛谷巨佬@hhz6830975

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

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

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

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

\[-k_ix_{j1}+y_{j1}\ge-k_ix_{j2}+y_{j2}\iff \frac{y_{j2}-y_{j1}}{x_{j2}-x_{j1}}\le k_i \]

也就是说当这两个点之间的斜率小于等于给出的斜率时,\(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--;

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

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

玩具装箱问题

分析转移方程

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

\[dp_i=\min(dp_i,dp_j+(\sum_{t=j+1}^i v_t+i-(j+1)-L)^2) \]

把它写成前缀和的形式:

\[dp_i=\min(dp_i,dp_j+(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)^2\\ \iff\\ 2a(i)\times b(j)+(dp[i]-a(i)^2)=(dp[j]+b(j)^2) \]

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

写出斜率式

\[f(b(j))=k\times 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\),我们考虑什么时候 \(j1\)\(j2\) 劣:

根据题设有

\[-kb(j_1)+(dp[j_1]+a(j_1)^2)+a(i)^2\ge -kb(j_2)+(dp[j_2]+a(j_2)^2)+a(i)^2 \\\iff\\ k(b(j_2)-b(j_1))\ge f(b(j_2))-f(b(j_1)),b(j_2)>b(j_1) \\\iff\\ \frac{f(b(j_2))-f(b(j_1))}{b(j_2)-b(j_1)}\le k \]

即当最后一个式子成立时,当前的 \(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

  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. 从最初换完元的转移方程出发,根据题意比较\(j_1<j_2\)时,\(j_1\)劣于\(j_2\)的条件,进一步限定凸包的形态,并给我们的查询带来条件。
  5. 根据“4”来查询我们的答案,二分或者双指针,据题目而定,然后更新当前阶段的 \(dp\) 值。
  6. 根据性质,对于当前加入的点,维护凸包。

带修的凸包维护

题意

有三种操作,分别是插入二元组 \(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

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

posted @ 2022-10-20 21:06  Hanggoash  阅读(34)  评论(0编辑  收藏  举报
动态线条
动态线条end