凸包结构(Convex Hull construction)
在本文中,我们将讨论从一组点中构建一个凸包的问题。
在一个平面上给定 N N N 个点,目标是生成一个凸包,即包含所有给定点的最小的凸多边形。
我们将看到 Graham 在 1972 年发表的 Graham's scan
算法,以及 Andrew 在1979年发表的 Monotone chain
算法。两者的复杂度都是
O
(
N
log
N
)
\mathcal{O}(N \log N)
O(NlogN) ,而且都是渐进式最优的(因为已经证明没有渐进式更好的算法),只有少数涉及到并行或在线处理的问题除外。
格雷厄姆扫描算法(Graham’s scan Algorithm)
该算法首先找到最底部的点 P 0 P_0 P0 。如果有多个具有相同 Y Y Y 坐标的点,则考虑具有较小 X X X 坐标的点。这一步需要 O ( N ) \mathcal{O}(N) O(N) 时间。
接下来,所有其他点按极角逆时针排序。如果两点之间的极角相同,则选择最近的点。
然后我们逐个遍历每个点,并确保当前点和之前的两个点逆时针转动,否则前一个点被丢弃,因为它会变成非凸的形状。可以通过检查方向来检查顺时针或逆时针性质。
我们用一个堆栈来存储这些点,一旦我们到达原始点 P 0 P_0 P0 ,算法就完成了,我们会按顺时针顺序返回包含凸包所有点的堆栈。
如果你在做格雷厄姆扫描时需要包括相邻的点,你需要在排序之后再做一步。你需要得到与 P 0 P_0 P0 有最大极距的点(这些点应该在排序后的向量的末尾),并且是相邻的。这条线上的点应该被颠倒过来,这样我们就可以输出所有的相邻点,否则算法就会得到这条线上最近的点而跳过。这一步不应该包含在非共线版本的算法中,否则你就得不到最小的凸包。
实现
struct pt {
double x, y;
};
int orientation(pt a, pt b, pt c) {
double v = a.x*(b.y-c.y)+b.x*(c.y-a.y)+c.x*(a.y-b.y);
if (v < 0) return -1; // clockwise
if (v > 0) return +1; // counter-clockwise
return 0;
}
bool cw(pt a, pt b, pt c, bool include_collinear) {
int o = orientation(a, b, c);
return o < 0 || (include_collinear && o == 0);
}
bool collinear(pt a, pt b, pt c) { return orientation(a, b, c) == 0; }
void convex_hull(vector<pt>& a, bool include_collinear = false) {
pt p0 = *min_element(a.begin(), a.end(), [](pt a, pt b) {
return make_pair(a.y, a.x) < make_pair(b.y, b.x);
});
sort(a.begin(), a.end(), [&p0](const pt& a, const pt& b) {
int o = orientation(p0, a, b);
if (o == 0)
return (p0.x-a.x)*(p0.x-a.x) + (p0.y-a.y)*(p0.y-a.y)
< (p0.x-b.x)*(p0.x-b.x) + (p0.y-b.y)*(p0.y-b.y);
return o < 0;
});
if (include_collinear) {
int i = (int)a.size()-1;
while (i >= 0 && collinear(p0, a[i], a.back())) i--;
reverse(a.begin()+i+1, a.end());
}
vector<pt> st;
for (int i = 0; i < (int)a.size(); i++) {
while (st.size() > 1 && !cw(st[st.size()-2], st.back(), a[i], include_collinear))
st.pop_back();
st.push_back(a[i]);
}
a = st;
}
单调链算法(Monotone chain Algorithm)
该算法首先找到最左边和最右边的点 A A A 和 B B B 。如果存在多个这样的点,则将左边的最低点(最低 Y Y Y 坐标)作为 A A A ,将右边的最高点(最高 Y Y Y 坐标)作为显然, A A A 和 B B B 必须都属于凸包,因为它们距离最远,并且它们不能被给定点之间由一对形成的任何线所包含。
现在,通过 A B AB AB 画一条线。这将所有其他点分为两组, S 1 S1 S1 和 S 2 S2 S2 ,其中 S 1 S1 S1 包含连接 A A A 和 B B B 的直线上方的所有点, S 2 S2 S2 包含连接 A A A 和 B B B 的直线下方的所有点。位于连接线上的点 A A A 和 B B B 可能属于任一集合。点 A A A 和 B B B 属于两个集合。现在算法构造上集 S 1 S1 S1 和下集 S 2 S2 S2 ,然后将它们组合起来得到答案。
为了得到上一组,我们按 x x x 坐标对所有点进行排序。对于每个点,我们检查当前点是否是最后一个点(我们定义为 B B B ),或者 A A A 和当前点之间的线与当前点和 B B B 之间的线之间的方向是否为顺时针方向。在那些情况下,当前点属于上集 S 1 S1 S1 。可以通过检查方向来检查顺时针或逆时针性质。
如果给定点属于上集合,我们检查连接倒数第二个点和上凸包中的最后一个点的线与连接上凸包中的最后一个点和当前点的线所形成的角度。如果角度不是顺时针方向,我们将删除添加到上凸包的最近点,因为一旦将当前点添加到凸包,当前点将能够包含前一个点。
相同的逻辑适用于较低的集合 S 2 S2 S2 。如果当前点是 B B B ,或者由 A A A 和当前点以及当前点和 B B B 形成的线的方向是逆时针的,那么它属于 S 2 S2 S2。
如果给定点属于较低的集合,我们的行为与较高集合上的点类似,除了我们检查逆时针方向而不是顺时针方向。因此,如果连接下凸包中的倒数第二个点和最后一个点的连线与连接下凸包中的最后一个点和当前点的连线所成的角度不是逆时针的,则我们删除最近的点添加到较低的凸包,因为一旦添加到包中,当前点将能够包含前一个点。
由上下凸包并集得到最终的凸包,形成顺时针包,实现如下。
如果您需要共线点,您只需要在顺时针/逆时针例程中检查它们。但是,这允许所有输入点在一条线上共线的退化情况,并且算法将输出重复的点。为了解决这个问题,我们检查上层外壳是否包含所有点,如果包含,我们只是反向返回点,因为这就是 Graham 的实现在这种情况下会返回的内容。
实现
struct pt {
double x, y;
};
int orientation(pt a, pt b, pt c) {
double v = a.x*(b.y-c.y)+b.x*(c.y-a.y)+c.x*(a.y-b.y);
if (v < 0) return -1; // clockwise
if (v > 0) return +1; // counter-clockwise
return 0;
}
bool cw(pt a, pt b, pt c, bool include_collinear) {
int o = orientation(a, b, c);
return o < 0 || (include_collinear && o == 0);
}
bool ccw(pt a, pt b, pt c, bool include_collinear) {
int o = orientation(a, b, c);
return o > 0 || (include_collinear && o == 0);
}
void convex_hull(vector<pt>& a, bool include_collinear = false) {
if (a.size() == 1)
return;
sort(a.begin(), a.end(), [](pt a, pt b) {
return make_pair(a.x, a.y) < make_pair(b.x, b.y);
});
pt p1 = a[0], p2 = a.back();
vector<pt> up, down;
up.push_back(p1);
down.push_back(p1);
for (int i = 1; i < (int)a.size(); i++) {
if (i == a.size() - 1 || cw(p1, a[i], p2, include_collinear)) {
while (up.size() >= 2 && !cw(up[up.size()-2], up[up.size()-1], a[i], include_collinear))
up.pop_back();
up.push_back(a[i]);
}
if (i == a.size() - 1 || ccw(p1, a[i], p2, include_collinear)) {
while (down.size() >= 2 && !ccw(down[down.size()-2], down[down.size()-1], a[i], include_collinear))
down.pop_back();
down.push_back(a[i]);
}
}
if (include_collinear && up.size() == a.size()) {
reverse(a.begin(), a.end());
return;
}
a.clear();
for (int i = 0; i < (int)up.size(); i++)
a.push_back(up[i]);
for (int i = down.size() - 2; i > 0; i--)
a.push_back(down[i]);
}