CLINS

在一个实数向量空间 V\mathcal{V} 中,对于给定集合 XX,所有包含 XX 的凸集的交集 SS 被称为 XX 的凸包。XX 的凸包可以用 XX 内所有点 (x1,x2,...,xn)(x_1,x_2,...,x_n) 的线性组合来构造。在二维欧氏空间中,凸包可以想象为一条刚好包括所有点的橡皮圈。

以不严谨的语言来描述,给定二维平面上的点集,凸包就是将最外层的点连接起来构成的凸多边形,它能包含点集中的所有点。如图-1,假设平面共有 p0p12p_0 \sim p_{12} 共 13 个点,过某些点作一个多边形,使这个多边形能把所有点都“包”起来。当这个多边形是凸多边形的时候,就叫它“凸包”。

凸包算法

1 概念

在一个实数向量空间 V\mathcal{V} 中,对于给定集合 XX,所有包含 XX 的凸集的交集 SS 被称为 XX 的凸包。XX 的凸包可以用 XX 内所有点 (x1,x2,...,xn)(x_1,x_2,...,x_n) 的线性组合来构造。在二维欧氏空间中,凸包可以想象为一条刚好包括所有点的橡皮圈。

以不严谨的语言来描述,给定二维平面上的点集,凸包就是将最外层的点连接起来构成的凸多边形,它能包含点集中的所有点。如图-1,假设平面共有 p0p12p_0 \sim p_{12} 共 13 个点,过某些点作一个多边形,使这个多边形能把所有点都“包”起来。当这个多边形是凸多边形的时候,就叫它“凸包”。

图-1 凸包示意

2 解法一(Graham 扫描法)

  • 时间复杂度:O(nlogn)O(n\log n)

2.1 计算步骤

Graham 扫描的思想是先找到凸包上的一个点,然后从那个点开始按逆时针方向逐个找凸包上的点,实际上就是进行极角排序,然后对其查询使用。 以图-2 为例,Graham 扫描法计算步骤为:

图-2 Graham扫描初始图
  1. 把所有点放在二维坐标系中,则纵坐标最小的点一定是凸包上的点,如图-2 中 P0P_0
  2. 将所有点的坐标平移,使 P0P_0 作为原点。
  3. 计算各个点相对于 P0P_0 的幅角 α\alpha ,按从小到大的顺序对各个点排序。当 α\alpha 相同时,距离 P0P_0 比较近的排在前面。如图-2 得到的结果为 P1,P2,P3,P4,P5,P6,P7,P8P_1,P_2,P_3,P_4,P_5,P_6,P_7,P_8。由几何知识可以知道,结果中第一个点 P1P_1 和最后一个点 P8P_8 一定是凸包上的点。以上,已经知道了凸包上的第一个点 P0P_0 和第二个点 P1P_1,把它们放在栈 SS 里。把 P2P_2 压入栈 SS,假设 SS 的栈底元素为 S[0]S[0],栈顶元素为 S[t]S[t]
  4. ii 遍历 3 到 8(所有点的最大下标):
  5. 连接 S[t1]S[t-1]S[t]S[t] 得向量 v1v_1,连接 S[t1]S[t-1]PiP_i 得向量 v2v_2
  6. v2v_2 的方向相对于 v1v_1 为左转,则把 PiP_i 压入 SS,继续步骤 4,否则进入步骤 7。
  7. S[t]S[t] 不满足凸包要求,把 S[t]S[t] 弹出,继续步骤 5。

最后,栈中的元素就是凸包上的点。计算的动态过程如图-3。

图-3 Graham扫描法计算过程

2.2 算法

图-4 Graham扫描法算法

2.3 CPP 代码

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
#include <iostream>
#include <vector>
#include <cmath>

using namespace std;

class mpoint {
//class point(x, y)
public:
double x;
double y;
mpoint(double xx = 0, double yy = 0) {
x = xx;
y = yy;
}

};

int get_miny_point_id(mpoint *points, int size) {
//get the point with min_y
int min_id = 0;
double miny = 10000;
for (int i = 0; i < size; i++) {
if (points[i].y < miny) {
miny = points[i].y;
min_id = i;
}
}
return min_id;
}

void get_cos(mpoint *points, double *mcos, int id, int size) {
//get point's cos
double coss;
for (int i = 0; i < size; i++) {
if (i == id) {
mcos[i] = 2;
} else {
coss = (points[i].x - points[id].x) / sqrt((points[i].x - points[id].x) * (points[i].x - points[id].x) + (points[i].y - points[id].y) * (points[i].y - points[id].y));
mcos[i] = coss;
}
}
}

void sort_points(mpoint *points, double *mcos, int size) {
//sort the points
int i, j;
double temp_cos;
mpoint temp_point;
for (i = 0; i < size; i++) {
for (j = 0; j < size - i - 1; j++) {
//bubble sorting
if (mcos[j] < mcos[j + 1]) {
temp_cos = mcos[j];
mcos[j] = mcos[j + 1];
mcos[j + 1] = temp_cos;

temp_point = points[j];
points[j] = points[j + 1];
points[j + 1] = temp_point;
}
}
}
}

int ccw(mpoint a, mpoint b, mpoint c) {
//judge if it is couter-colockwise
double area = (b.x-a.x) * (c.y-a.y) - (b.y-a.y) * (c.x-a.x);
if (area < 0) {
return -1; // clockwise
} else {
if (area > 0) return 1; // counter-clockwise
else return 0; // collinear
}

}

void get_outpoint(mpoint *points, int size) {
//get points in stack
vector<mpoint> outpoint;
outpoint.push_back(points[0]);
outpoint.push_back(points[1]);
int i = 2;
while (i != size) {
if (ccw(outpoint[outpoint.size() - 2], outpoint[outpoint.size() - 1], points[i]) > 0) {
outpoint.push_back(points[i]);
i = i + 1;
} else {
outpoint.pop_back();
}
}
cout << "The outpoints are: " << endl;
for (int k = 0; k < outpoint.size(); k++) {
cout << outpoint[k].x << " " << outpoint[k].y << endl;
}
}

int main() {
int size = 4;
double px, py;
cout << "Please input the size: ";
cin >> size;
mpoint *points;
int miny_point_id;
double *mcos;
points = new mpoint[size];
mcos = new double[size];
for (int i = 0; i < size; i++) {
cin >> px;
cin >> py;
points[i].x = px;
points[i].y = py;
}
miny_point_id = get_miny_point_id(points, size);
get_cos(points, mcos, miny_point_id, size);
sort_points(points, mcos, size);
get_outpoint(points, size);
}

2.4 左转的判断

对于前面的 v2v_2 相对 v1v_1 是左转还是右转,计算方式可以使用叉乘:

v1×v2=[x1x2y1y2]=x1y2x2y1v_1 \times v_2 = \left[ \begin{matrix} x_1 &x_2 \\ y_1 &y_2 \end{matrix} \right] =x_1 y_2 - x_2 y_1

若值为正,则 v1v_1v2v_2 的右侧(顺时针方向),若为负则 v1v_1v2v_2 的左侧(逆时针方向)。

3 解法二(Javis-March 算法或 Gift Wrapping 算法)

  • 时间复杂度:O(NM)O(NM)NN 为点的数量,MM 为凸包的顶点数。

3.1 计算步骤

  1. 先确定凸包边界上的点 p1p_1 和与下一个点 p2p_2(可以利用 Graham 扫描法中的方法);
  2. 在点集里去寻找下一个点 p3p_3,使得 p1,p3,p2p_1,p_3,p_2 满足 CCW(Counterclockwise,以逆时针方向旋转);
  3. 如果满足,这就说明 p3p_3 是更外围的点,把 p3p_3 的值覆盖 p2p_2,即 p2=p3p_2 = p_3
  4. 重复第 3 步,直到所有点都访问过,即为找到了下一个最外边的点;
  5. p2p_2 的值覆盖 p1p_1,即 p1=p2p_1 = p_2
  6. 重复第 2 步,直到下一个点回到起点。

3.2 CPP 代码

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
typedef vector<pair<double, double>> vpdd;
typedef pair<double, double> pdd;

bool ccw(pdd a, pdd b, pdd c) {
// 叉乘
return ((c.first - a.first) * (b.second - a.second) - (c.second - a.second) * (b.first - a.first)) < 0;
}

void jarvis_march(const vpdd &input) {

int n = input.size();
int left = 0;
for (int i = 0; i < n; i++) {
if (input[i].first < input[left].first) {
left = i;
}
}

int first_point = left;
int third_point;
do {
hull_jmarch.push_back(input[first_point]);
third_point = (first_point + 1) % n;
for (int i = 0; i < n; i++) {
if (ccw(input[first_point], input[i], input[third_point])) {
third_point = i;
}
}
first_point = third_point;

} while (first_point != left);
}

参考