凸包(Convex Hull)是一个计算几何(图形学)中的概念。经典凸包问题
在坐标系中的某些点组成的凸多边形,这个多边形能把所有点都“包”起来,同时凸多边形的边长最短。如下图中的4个红点把6个点全部包含起来了。
仔细观察可以发现,最小值与最大值一定位于凸包的最左边与最右边,从左向右看,我们将凸壳考虑成
我们首先将最初始的两个点添加到凸壳中,然后遍历排好序的 trees 数组。对于每个新的点,我们检查当前点是否在最后两个点的逆时针方向上,轨迹是否是左拐。如果是的话,当前点直接被压入凸壳 hull 中,$cross$ 函数返回的结果为正数;如果不是的话,$cross$ 返回的结果为负数,并且可以知道栈顶的元素在凸壳里面而不是凸壳边上,则需要从 hull 中弹出元素直到当前点处于栈顶两个点的逆时针方向上。
这个方法中,我们不需要显式地考虑共线的点,因为这些点已经按照
现在我们需要求出凸壳的上半部分。我们继续找下一个逆时针的点并将不在边界上的点从栈中弹出,但这次我们遍历的顺序是按照
class Solution {
public:
// 计算向量叉乘,判断方向
int cross(const vector<int>& p, const vector<int>& q, const vector<int>& r) {
int p_q_x = q[0] - p[0], p_q_y = q[1] - p[1]; // 向量a
int q_r_x = r[0] - q[0], q_r_y = r[1] - q[1]; // 向量b
return p_q_x * q_r_y - p_q_y * q_r_x; // a.x*b.y - a.y*b.x
}
// Andrew 算法
vector<vector<int>> outerTrees(vector<vector<int>>& trees) {
// 特判
int n = trees.size();
if(n < 4) {
return trees;
}
// 排序
sort(trees.begin(), trees.end(), [](const vector<int>& a, const vector<int>& b) ->bool {
if(a[0] == b[0]) return a[1] < b[1];
return a[0] < b[0];
});
// 关键数据结构——单调栈
vector<int> hull; // 存下标
vector<bool> used(n, false); // 剪枝
/* 求出凸包的下半部分 */
// 把最左边的2个点放入
hull.emplace_back(0); // hull[0] 需要入栈两次,不进行标记
for(int i = 1; i < n; i++) {
if(!used[i]) {
while(hull.size() > 1 && cross(trees[hull[hull.size() - 2]], trees[hull.back()], trees[i]) < 0) {
used[hull.back()] = false;
hull.pop_back();
}
used[i] = true;
hull.emplace_back(i);
}
}
/* 求出凸包的上半部分 */
// 从导数第2个位置向前遍历
int m = hull.size();
for(int i = n - 2; i >= 0; i--) {
if(!used[i]) {
while(hull.size() > m && cross(trees[hull[hull.size() - 2]], trees[hull.back()], trees[i]) < 0) {
used[hull.back()] = false;
hull.pop_back();
}
used[i] = true;
hull.emplace_back(i);
}
}
hull.pop_back(); // hull[0] 同时参与凸包的上半部分检测,因此需去掉重复的 hull[0]
// 返回值
vector<vector<int>> res;
for(auto& pos : hull) {
res.emplace_back(trees[pos]);
}
return res;
}
};