lec2 中学习的是如何把一个多边形切割开来;而这节课学习的就是,如何把一堆点给包起来
就像你在地上撒了一把钉子,然后用一根橡皮筋把它们围起来,拉紧之后那个形状就是 “凸包”。

所以,构造凸包的这个过程,我们还是从暴力解法开始

  1. 朴素暴力解法
    我们可以假定一个结论:将点集 S 中的所有点遍历一遍,然后将这些三角形中包住的点都删除;最后剩余的这些没有被包围的点,一定是凸包的顶点。(作为三角形顶点或位于三角形边上的点不被删除)
    那么我们就可以
    #算法步骤(CH1):

  2. 遍历 SS 中所有可能的三元组 (x,y,z)(x, y, z):一共 (n3)=O(n3)\binom{n}{3} = O(n^3) 种组合。

  3. 对每个组合 (x,y,z)(x,y,z),遍历点集中每个点 qq

  4. 检查 qq 是否落在三角形 (x,y,z)(x,y,z) 内部。

  5. 如果是,就把 qqSS 中删除。

  6. 最后剩下的点就是 CH 顶点,把它们按极角排序后连接成凸包。
    总时间复杂度:O(n3n)=O(n4)O(n^3 \cdot n) = O(n^4)

接下来,我们可以对这个算法进行一定的优化
2. 优化算法
我们从另一个角度考虑,S 中的所有点,必然位于凸包的边 xy-> 有向直线的右侧,故我们不需要对三维的所有点进行遍历,只需要对二维的点进行遍历即可,然后判断是否其余所有点都位于他们的右侧
算法步骤(CH2)解释:
3. 遍历所有有序点对 (x,y)(x, y)(注意是有序的)

  1. 假设该边合法(valid ← true)

  2. 对其它点 zz 遍历判断

    • 判断 zz 是否在直线 xyxy 的左侧(说明不合法)
  3. 如果发现一个点在左侧,就将 valid 设置为 false

  4. 最后,如果 valid 仍为 true,说明 xyxy 是 CH 的边之一

  5. 加入这条边到 CH 边集

  6. 所有边加入完毕后,排序构造完整凸包
    总时间复杂度:O(n2n)=O(n3)O(n^2 \cdot n) = O(n^3)


接下来,看看实际当中用的比较多的三种优化算法。

在讲算法之前,老师首先对前面的判断进行了补充:如何判断一个点位于线的左边还是右边
通过叉积,然后根据最后值的正负来判断点位线的哪一边
这边老师带过了一个算法:Graham Scan
想象你在给钉子围上橡皮筋:

  1. 从最左边的点开始;

  2. 你想找到 “下一个最外面的点”;

  3. 怎么找?转一圈,看哪个点相对现在这个点最靠外(最逆时针);

  4. 一直这样找下去,直到绕一圈回到起点。

# ch (凸包) 的三种构造算法

# Gift Wrapping(礼物包装)

通俗来理解可以说:
想象你在给钉子围上橡皮筋:

  1. 从最左边的点开始;

  2. 你想找到 “下一个最外面的点”;

  3. 怎么找?转一圈,看哪个点相对现在这个点最靠外(最逆时针);

  4. 一直这样找下去,直到绕一圈回到起点。

实际上的过程就是:

  • 你从左下角开始;

  • 你看着所有点,假装你有一个 “激光扫描器”;

  • 每次 “顺着边扫一圈”,找到第一个 “最外边” 的点(就是逆时针最远的点);

  • 把这条边加入凸包;

  • 然后从这个新点再开始下一轮扫描。
    你站在当前点 A,看到两个候选点 B 和 C:

  • 谁更逆时针?就是谁形成的角度更大;

  • 用叉积公式判断:

cross(A,B,C)=(BA)×(CA)\text{cross}(A, B, C) = (B - A) \times (C - A)

  • 如果 cross 值为正,说明 C 比 B 更逆时针 ⇒ C 更好!
    所以,就是一个从起点开始,转着找下一个点 -> 再以下一个点为起点,找下一个点的过程,直到找到所有的点。

那么,有优化的空间嘛?
有的!
可以不用每次都遍历所有的点以找下一个点 q,而是可以通过检查角度来判断,如果新的角度大于旧的最大角,那么这个点就是新的点。

所以,他们的区别在于,暴力解法需要对所有的点做叉积比较来判断是否都在左边,而优化后只需要比较角度就可以了
一个是O(n2)O(n^2) 的时间复杂度,因为需要嵌套;而后者为 O (n) 的复杂度,只需要线性即可。

# quick hall

Divide-and-Conquer approach 分治
这个算法可以理解成
想象你在玩 “谁离得最远” 的游戏:

  1. 找出最左点 AAA 和最右点 BBB,画出一条直线 ABABAB;

  2. 把所有点分成两边(上方、下方);

  3. 找出 “离直线最远” 的那个点 CCC,它肯定是边界上的;

  4. 把 C 连上 A 和 B,形成三角形;

  5. 再看哪些点在这个三角形 “外面”,对它们重复上面的步骤;

  6. 直到所有点都在边界三角形里为止,完工!

![[Pasted image 20250603145633.png]]

时间复杂度:
最好情况:每次都能把点均匀切开
即二分查找复杂度。O (nlogn)

最差情况:所有点都在边上,如五角星形的分布
O(n2)O(n^2)

# 扫描线算法

将这些点以 x 轴从小到大排列,并且分为下凸壳和上凸壳,然后最后再将他们拼起来。
在凸包上行走时观察是否右拐,如果左拐了就把这个点删了。如果右拐就继续。
伪代码:

1
2
3
4
5
6
7
8
1.  Sort the points left to right
2. Initialize L_upper with first two points
3. For i = 3 to n:
4. Add p_i to L_upper
5. While L_upper has a left turn:
6. Delete the middle point
7. 同理构造 L_lower
8. 最后将 L_upper 和 L_lower 合并

因为每个点只会加入和删除一次,所以时间复杂度为 O (nlogn)

然后就是计算凸包的下界,我们可以低于 nlogn 吗?
用数学归纳法,将凸包转化为排序问题,而排序算法的下界为 nlogn,所以凸包不可能低于 nlogn

# Chen 算法

Chan 算法就像一个聪明的厨师:

  • Graham 是大锅炒(适合处理很多点);

  • Gift Wrapping 是慢工细活(适合边界点很少);

  • Chan 想了一个办法:先猜边界点有多少(比如 h=4),然后只挑这些来 Gift Wrapping,结果超快!
    相当于是 graham 和 gift wrapping 的结合,将所有点分为 n/h 组,然后对每一组使用 graham 算法,得到每一组的局部凸包,然后对这些局部凸包使用 gift wrapping,所以最终的复杂度为 nlogh