Eric Way's Personal Site

I Write $\sin(x)$ Not Tragedies

二维凸包

2021-01-09 Coding

  1. 1. 旋转卡壳
    1. 1.1. 凸包的直径
    2. 1.2. 最小覆盖矩形

首先将所有点按横坐标、纵坐标排序,并删掉重复的点。然后分别求下凸壳和上凸壳。初始状态是点$P_0$和$P_1$。本质是单调队列,每次添加一个连边,保证删除队尾在凸包内的点。队尾的点在凸包内外,可以被转化为向量叉积的符号。遍历第一次后一定添加了$P_{n-1}$,就完成了下凸壳,然后倒序遍历,相当于把整个图像旋转$180 ^ \circ$再重新进行刚才的操作,就可以完成上凸壳。注意除非只有一个点,最后必然也会重复添加$P_0$,因此凸包的大小会多1。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int ConvelHull() {
int k, m=0;
sort(p, p+n);
n = distance(p, unique(p, p+n));
For(i,n){
while(m>1 && Cross(ch[m-1]-ch[m-2], p[i]-ch[m-2]) <= 0) m--;
ch[m++] = p[i];
}
k = m;
rev(i, n-2, 0){
while(m>k && Cross(ch[m-1]-ch[m-2], p[i]-ch[m-2]) <= 0) m--;
ch[m++] = p[i];
}
if (n > 2) m--;
return m;
}

旋转卡壳

对于任何凸包的任何一条边$P_uP_{u+1}$,考虑凸包上它的对踵点$P_v$,即到它距离最远的那个点。注意这个距离可以通过三角形$P_uP_{u+1}P_v$的面积来比较。若我们从$P_{u+1}$开始,逆时针枚举凸包上所有点$P_v$,三角形$P_uP_{u+1}P_v$的面积呈单峰函数,即先增再减,从而当$P_uP_{u+1}P_{v+1} \le P_uP_{u+1}P_{v}$ 时,$v$取到三角形面积的最大值,即$P_v$是对踵点;注意当$P_uP_{u+1}P_{v+1} = P_uP_{u+1}P_{v}$时,$P_{v+1}$也是对踵点。这里注意,我们在建凸包的时候需要提前保证凸包的边上没有输入点,这样才能保证一条边的对踵点至多有两个。当我们逆时针枚举凸包上的所有边,对应的对踵点也必然以逆时针的方向移动,即满足决策单调性。再考虑到刚才提及的单峰函数性质,每次枚举边$P_uP_{u+1}$,都在前一条边$P_{u-1}P_{u}$所取的对踵点$v’$的基础上,尝试逐步自增$v’$到使得三角形面积最大的$v$即可。(一般资料称凸包上某点是另一点的对踵点,但这里延伸了这个概念。一点的对踵点一定是包含这一点的两条边的对踵点的其中一个。)

凸包的直径

凸包的直径是凸包上任意两个点的距离最大值,也可以化为:凸包上任意一条边的任意一个点与这条边的对踵点的距离的最大值。这样,按前面的算法,只需$O(n)$时间复杂度。

最小覆盖矩形

若要求最小覆盖矩形,就要在逆时针枚举凸包上的所有边的时候,在考虑其对踵点的同时,考虑其“最左点”和“最右点”。可以直观地理解这两个概念的定义:试把凸包旋转,使某一边成为水平底边,再以这水平底边为$x$轴建立平面直角坐标系,这时凸包横坐标最小的点即为“最左点”、横坐标最大的点即为“最右点”,而纵坐标最大的点就是对踵点。当然实际操作并不需要旋转坐标系之类的操作,本质就是比较点在这条边上投影的有向线段的大小,这用点积可以实现。这里的决策单调性和单峰函数性质同样满足,因此求法和对踵点是一样的。但特别注意最左点的初始值需要从对踵点开始,才能保证其单峰函数的性质。求完了三个极点之后,利用平面几何进行演算即可,注意利用点到直线的投影。 例题:最小矩形覆盖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
Point Project(Point a1, Point a2, Point b) {
Vector v = a2-a1;
return a1 + v*(Dot(v, b-a1) / Dot(v, v));
}
Point ret[4];
double solve() {
int m = ConvelHull();
if (m == 1) {
For(j, 4) ret[j] = ch[0];
return 0;
}
if (m == 2) {
ret[0] = ret[3] = ch[0];
ret[1] = ret[2] = ch[1];
return 0;
}
double ans = 1e100;
ch[m] = ch[0];
int v = 1, l, r = 1;
#define nxt(i) i=(i+1)%m
For(u, m) {
while(Area(ch[u], ch[u+1], ch[v]) <= Area(ch[u], ch[u+1], ch[v+1])) nxt(v);
if (!u) l = v; // important
while(Dot(ch[u+1]-ch[u], ch[l+1]-ch[u]) <= Dot(ch[u+1]-ch[u], ch[l]-ch[u])) nxt(l);
while(Dot(ch[u+1]-ch[u], ch[r+1]-ch[u]) >= Dot(ch[u+1]-ch[u], ch[r]-ch[u])) nxt(r);

Point Vp = Project(ch[u], ch[u+1], ch[v]);
Vector hv = ch[v] - Vp;
double h = Length(hv);
Point A[4];
A[0] = Project(ch[u], ch[u+1], ch[l]);
A[1] = Project(ch[u], ch[u+1], ch[r]);
double w = Length(A[0]-A[1]);

if (h * w < ans) {
ans = h * w;
A[2] = A[1] + hv;
A[3] = A[0] + hv;
For(j, 4) ret[j] = A[j];
}
}
return ans;
}
This article was last updated on days ago, and the information described in the article may have changed.