Post

凸包问题

起因, Leetcode里的587.安装栅栏, 第二次遇到凸包问题了, 不会就学, 老碰见很烦

基本概念

  • 凸多边形: 所有内角大小都在 $[0, \pi]$ 范围内的多边形
  • 凸包(Convex Hull): 在平面上能包含所有给定点的最小凸多边形叫做凸包
  • 向量积判断点在直线哪一侧: 右手螺旋定则, 直线方向向量与直线上任意一点与判断点向量的积, 为正则在左侧, 为零则在线上, 为负则在右侧(向量积的结果方向符合右手螺旋定则)

Andrew Algorithm

将集合内的所有点按照 x 坐标以及 y 坐标升序排序, 我们可以认为排序后的首尾两点, 从左下至右上将所求的凸包分成了上凸包与下凸包两部分

我们按照排序顺序逐个遍历点, 可以想得到的是, 我们遍历点的方向是右手螺旋的, 因此, 如果遍历到的点, 相较于前两个点左旋时, 我们应当弹出上一点, 压入该点, 因此用一个栈来存储点比较合适, 同时我们可以用前文通过向量积的知识来判断遍历点是否左旋.

同理, 我们可以用相同的方式遍历获得上凸包, 应当注意的是, 遍历下凸包时, 我们根据栈中点个数大于二来决定是否需要判断新增点右旋, 而遍历上凸包时, 我们需要保证在下凸包的点不被遍历(除开起点), 且下凸包的点不应该被弹出, 因此需要记录是否遍历过

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
#include <vector>
#include <iostream>
using namespace std;

// 判断是否右旋
bool isDextral(pair<int, int> &a, pair<int, int> &b, pair<int, int> &c)
{
    pair<int, int> ba, ac;
    ba = {a.first - b.first, a.second - b.second};
    ac = {c.first - a.first, c.second - a.second};
    return ba.first * ac.second - b2a.second * ac.first < 0;
}
vector<pair<int, int>> convexHull(vector<pair<int, int>> &trees)
{
    int sz = trees.size();
    // 点数小于三个自成凸包
    if (sz < 4)
        return trees;
    sort(trees.begin(), trees.end(), [&](const pair<int, int> &a, const pair<int, int> &b)
         { return a.first == b.first ? a.second < b.second : a.first < b.first; });
    vector<int> stk;
    vector<bool> visit(sz, false);
    // 存入左下点, 且visit = false使得遍历上凸壳时能够遍历起点
    stk.push_back(0);
    // 下凸壳
    for (int idx = 1; idx < sz; ++idx)
    {
        auto p = trees[idx];
        while (stk.size() >= 2)
        {
            // 右旋, 则弹出栈顶点
            if (isDextral(trees[stk.back()], trees[stk[stk.size() - 2]], p))
            {
                visit[stk.back()] = false;
                stk.pop_back();
            }
            // 左旋, 则加入栈
            else
                break;
        }
        stk.push_back(idx);
        visit[idx] = true;
    }
    int lowerConvexHullSz = stk.size();
    // 遍历上凸壳
    for (int idx = sz - 1; idx >= 0; --idx)
    {
        if (visit[idx])
            continue;
        auto p = trees[idx];
        // 栈内点数至少大于下凸壳点数, 且不弹出下凸壳点
        while (stk.size() > lowerConvexHullSz)
        {
            if (isDextral(trees[stk.back()], trees[stk[stk.size() - 2]], p))
            {
                visit[stk.back()] = false;
                stk.pop_back();
            }
            else
                break;
        }
        stk.push_back(idx);
        visit[idx] = true;
    }
    vector<pair<int, int>> res;
    // 起点重复
    for (int idx = 1; idx < stk.size(); ++idx)
        res.push_back(trees[stk[idx]]);
    return res;
}

int main()
{
    int m;
    cin >> m;
    vector<pair<int, int>> points(m);
    for (int idx = 0; idx < m; ++idx)
        cin >> points[idx].first >> points[idx].second;
    vector<pair<int, int>> res = convexHull(points);
    for (auto p : res)
        cout << p.first << "," << p.second << endl;
    return 0;
}

排序时间复杂度 $O(n\log(n))$, 每个点在遍历上下凸壳时只遍历一次, 时间 $O(2n)$, 总时间复杂度 $O(n\log(n))$, 空间复杂度 $O(n)$

Jarvis Algorithm

其实知道 Andrew 算法就行了, 因为那个是优化版本, 这个 Jarvis 算法等于是 Andrew 的前辈了, 时间复杂度 $O(n^2)$

大体思路与 Andrew 类似, 从最左点开始加入凸包, 找到下一个点使得其余剩余点都在该线的左侧, 然后加入凸包, 不断重复直至没有这样一个点为止

Graham Algorithm

极坐标下的 Andrew Algorithm

This post is licensed under CC BY 4.0 by the author.