凸包
1 概念
凸包指的是在平面上能包含所有给定点的最小凸多边形,可以理解为用一根绳子圈住所有点构成的一个多边形。而一个凸包又可以看做是上下两个部分组成,也就是常说的上凸壳和下凸壳。
实际操作中,我们很难直接维护出一整个凸包,所以一般分为上下凸壳进行维护。
2 维护方法
凸包的维护方法有很多种,在静态、动态插入、动态删除时均有不同的做法,下面简单介绍两种基本场景下的解决方式。
2.1 静态构建
静态构建凸包的算法是 Andrew 算法,其重点在于向量叉积这个运算。我们知道,对于两个向量
而根据凸包概念我们知道,上凸壳从左到右的轨迹总是右拐,而下凸壳从左到右的轨迹总是左拐,于是我们便可以用叉积简单判断出当前点插入是否合法。具体的,我们先对所有点排序,然后用一个单调栈去维护当前的凸包,如果待插入点
代码如下:
struct Point {
int x, y;
bool operator < (Point b) {//排序比较函数
if(x == b.x) return y < b.y;
return x < b.x;
}
Point operator - (Point b) {return {x - b.x, y - b.y};}//求出两点之间的向量
int operator * (Point b) {return x * b.y - y * b.x;}//向量叉积
}a[Maxn];
signed main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> n;
for(int i = 1; i <= n; i++) cin >> a[i].x >> a[i].y;
sort(a + 1, a + n + 1);
//以构建上凸壳为例
for(int i = 1; i <= n; i++) {
while(top > 1 && (a[i] - a[top]) * (a[top] - a[top - 1]) <= 0) top--;
//叉积小于 0 说明向左拐,不合法,弹栈
a[++top] = a[i];
}
return 0;
}
2.2 动态加点
如果每次要动态加入一个点的话,我们就需要动态调整凸包的形态。以上凸壳为例,假设当前待插入点为
找切点可以直接二分查找,不过直接暴力判断也是可以的,因为每个点如果判断完不合法后就会被踢出凸包,并且再也不可能加入进来,所以每个点只会进出凸包一次,复杂度有保障。
实际中常用 set
或平衡树去维护凸包,于是复杂度就是
采用暴力删点的代码如下:
set <Point> s;
void insert(Point p) {
auto it = s.lower_bound(p);
auto itr = it, itl = (--it);//找前驱后继
Point nxt = *itr, pre = *itl;
if((nxt - p) * (p - pre) <= 0) return ;//先判断能否构成凸包
auto tmp = itr++;
while(1) {//向右暴力删点
if(itr == s.end()) break;
Point a = *tmp, b = *itr;
if((b - a) * (a - p) <= 0) {//不合法删去
s.erase(tmp);
}
else break;//合法就跳出
tmp = itr++;
}
tmp = itl--;
while(1) {//向左暴力删点
if(tmp == s.begin()) break;
Point a = *tmp, b = *itl;
if((p - a) * (a - b) <= 0) {
s.erase(tmp);
}
else break;
tmp = itl--;
}
s.insert(p);
}
上面两种就是常见的维护凸包的方式,剩下的就是利用各种数据结构去维护凸包,不过这些方法都或多或少会丢失凸包的具体信息,所以只适用于利用凸包求最优解的问题,下面再举几例:
- 利用线段树维护每个区间上的凸包,通过暴力合并即可得出任意区间上的凸包信息。复杂度会因为线段树变为
。 - 利用分块维护每个块内的凸包,当有一些点会动态改变的时候,对整块记录偏移量,对散块进行暴力重构即可。取块长为
即可得到一个 的复杂度。
3 应用
3.1 应用场景
凸包的常见应用场景实际上无非两种,即实际维护凸包信息以及最优解问题。
- 维护凸包信息:题目中明确说了要求凸包周长、面积,或者可以转化为求凸包周长、面积等需要知道凸包上每一个点的信息时使用。此时上面提到的一些维护方式便不再适用。
- 最优解问题:这个相信大家更熟悉,实际上类似于斜率优化 dp。在某些问题中我们会有若干决策点,而我们可以得出当某个点更优时其斜率满足某个关系,然后可以知道最优解一定在所有决策点构成的上凸壳 / 下凸壳上,然后通过二分就可以找到最优决策点。
3.2 例题
例 1 旅行规划
显然我们需要维护的就是每一个点的前缀和信息,这样第二问就是求一个区间最大值。而第一个操作实际上可以转化为区间
考虑困难的实际上是前一个操作,也就是加等差数列,这个很难用线段树直接维护。那么考虑用万能的分块去解决,对于整块而言,加上若干等差数列后每个位置的增加值依然相当于一个等差数列,并且每一个位置最终的前缀和很容易写成
现在每一个块内的元素贡献就可以写成一个一次函数的形式了,我们要求最后的答案
剩下的细节例如散块的处理等不再赘述,留给读者自行思考。
例 2 [NOI2014] 购票
显然此题需要考虑 dp,那么设
这个式子的转化就太过套路了,我们要求
不过下面还有一个限制
例 3 [CTSC2016] 时空旅行
发现给出的
不过此题的难点在于维护每一个版本的凸壳信息。首先可以发现不同版本的依赖关系构成一棵树,考虑利用 DFS 序进行操作,然后可以进一步发现每一个星球在 DFS 序上出现的区间恰好只有
但是这样还不够,实际上我们还可以省掉一个
例 4 [NOI2017] 分身术
这道题就是一个单纯维护凸包信息的题了,不过依然很难做。首先将凸包拆成上下凸壳分别维护面积,然后考虑删掉
首先考虑
根据这个结论我们不难想到,我们可以维护出
这个操作的实现只需要用线段树即可。对每个凸包维护一个权值线段树表示其包含哪些给定点(注意要先排序)。对于找切点采用线段树二分,对于删除采用线段树分裂,对于合并采用线段树合并即可。在 pushup
时利用向量叉积算出合并时多出来的面积,然后就可以得出最终的答案了。另一个值得注意的点时我们不能破坏原有凸包的信息,所以合并分裂时需要采用新建节点的方式处理。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律