凸包问题 graham-scan 分治

本题参考数据来自 uva 11626
解法一:贪心
凸包问题上课讲了两种方法,其中第一种做法是按照极坐标排序+单调栈解决,这种做法比较容易理解。
单调栈判断就是看新的点在直线左边还是右边,可以用叉乘的右手法则,即判断x1y2 - x2y1的符号。
但有个问题就是当有多个点和初始点连成一条线时,这些点的极坐标顺序不好判断。
这里的解决方法是,看上一个加入集合的点,若上个点和新的两个极坐标相同的点连成一条线,就按离上个点距离从小到大来做。

#include <bits/stdc++.h>

typedef long long ll;
using namespace std;

typedef tuple<int, int> P;
const int INF = 1 << 30;
double base_x, base_y;
P base;

ll isleft(P a, P start, P end)
{
    ll x1, y1, x2, y2;
    x1 = get<0>(end) - get<0>(start);
    y1 = get<1>(end) - get<1>(start);
    x2 = get<0>(a) - get<0>(start);
    y2 = get<1>(a) - get<1>(start);
    return x1*y2 - x2*y1;
}

int get_manhattan_dist(P a, P b)
{
    return abs(get<1>(b) - get<1>(a)) + abs(get<0>(b) - get<0>(a));
}

int main()
{
    freopen("in", "r", stdin);
    int t, a, b, N;
    scanf("%d", &N);
    for(int i=0;i<N;i++)
    {
        vector<P> v;
        scanf("%d", &t);
        for (int i = 0; i < t; i++) {
            scanf("%d %d", &a, &b);
            v.push_back(P(a, b));
            scanf("%c %c", &a, &a);
        }

        // 排序
        sort(v.begin(), v.end(), [](P a, P b){
            return get<1>(a) > get<1>(b);
        });

        // 令最靠下的元素为基本点
        base = v.back();
        v.pop_back();

        // 按照距离base点的角度排序
        sort(v.begin(), v.end(), [](P a, P b) {
            double ang1 = atan2(get<1>(a) - get<1>(base), get<0>(a) - get<0>(base));
            double ang2 = atan2(get<1>(b) - get<1>(base), get<0>(b) - get<0>(base));
            if(ang1 == ang2)return get_manhattan_dist(base, a) > get_manhattan_dist(base, b);
            return ang1 < ang2;
        });

        // 一个一个来,用单调栈做,往左拐正常,往右拐就弹出
        vector<P> s{base};

        for (int i = 0; i < v.size();)
        {
            // 如果出现共线的点,考虑是否逆转部分数组
            int j = i;
            while(j+1 < v.size() && isleft(base, v[j], v[j+1]) == 0)j++;

            if(j>i)
            {
                if (isleft(s[s.size()-1], v[i], v[i+1]) == 0)
                {
                    int d1 = get_manhattan_dist(s[s.size()-1], v[i]);
                    int d2 = get_manhattan_dist(s[s.size()-1], v[i+1]);
                    if(d1 > d2)reverse(v.begin()+i, v.begin()+j+1);
                }
            }

            for(int k=i;k<=j;k++)
            {
                while (s.size() >= 2 && isleft(v[k], s[s.size() - 2], s[s.size() - 1]) < 0)
                    s.pop_back();
                s.push_back(v[k]);
            }

            i = j+1;
        }

        // 找到最小的左边
        int start = 0;
        for(int i=1;i<s.size();i++)
        {
            if(get<0>(s[i]) == get<0>(s[start]) && get<1>(s[i]) < get<1>(s[start]))
                start = i;
            else if(get<0>(s[i]) < get<0>(s[start]))
                start = i;
        }

        printf("%d\n", s.size());
        for (int i = 0; i < s.size(); i++)
        {
            int j = (i+start) % s.size();
            printf("%d %d\n", get<0>(s[j]), get<1>(s[j]));
        }

    }
}

解法二:分治
首先,要将图形分割到能得到逆序排列的点为止。然后形成一个左集合和两个右集合(见课件),将这三个集合合并即可。合并时,如果共线,按左右划分,左右都是划分好的有序集合,反转下即可。最终全部转为倒序(在上一个点和两个候选点都共线的情况下)。

#include <bits/stdc++.h>

typedef long long ll;
const int INF = 1 << 30;
using namespace std;
typedef tuple<int, int> P;

ll isleft(P a, P start, P end)
{
    ll x1, y1, x2, y2;
    x1 = get<0>(end) - get<0>(start);
    y1 = get<1>(end) - get<1>(start);
    x2 = get<0>(a) - get<0>(start);
    y2 = get<1>(a) - get<1>(start);
    return x1*y2 - x2*y1;
}

int get_manhattan_dist(P a, P b)
{
    return abs(get<1>(b) - get<1>(a)) + abs(get<0>(b) - get<0>(a));
}

int decreasing(P base, vector<P>& v, int l, int r)
{
    for(int t=l+1;t<=r;t++)
    {
        int d1 = get_manhattan_dist(base, v[t-1]);
        int d2 = get_manhattan_dist(base, v[t]);
        if(d1 < d2)return 0;
    }
    return 1;
}

void rotate(vector<P>& merged_s, P& base)
{
    for (int i = 1; i < merged_s.size();)
    {
        // 如果出现共线的点,考虑是否逆转部分数组
        int j = i;
        while(j+1 < merged_s.size() && isleft(base, merged_s[j], merged_s[j+1]) == 0)j++;

        // 通过旋转的方式,处理位置在i-j的元素
        if(j>i)
        {
            // 只需处理上半部分,下半部分已经从小到大排好了
            if (isleft(merged_s[i-1], merged_s[i], merged_s[i+1]) != 0)
            {
                // 若是递减序列,直接排除
                if(decreasing(base, merged_s, i, j))return;

                // 需要大的优先,整个旋转一次,之后旋转前半部分(若前半部分开始递增)
                reverse(merged_s.begin()+i, merged_s.begin()+j+1);

                // 如果是递减序列,不用进行下面的操作
                if(decreasing(base, merged_s, i, j))return;

                for (int t = j - 1; t >= i; t--)
                {
                    int d1 = get_manhattan_dist(base, merged_s[t + 1]);
                    int d2 = get_manhattan_dist(base, merged_s[t]);
                    if (d1 < d2)
                    {
                        if(!decreasing(base, merged_s, i, t))
                            reverse(merged_s.begin() + i, merged_s.begin() + t + 1);
                        if(!decreasing(base, merged_s, t+1, j))
                            reverse(merged_s.begin() + t + 1, merged_s.begin() + j + 1);
                        break;
                    }
                }
            }
        }

        i = j+1;
    }
}

// 输入按照x排序的点集,返回凸包上的点,逆时针排列
vector<P> divide_conquer(vector<P>& v, int l, int r)
{
    if(r-l+1 <= 2)
    {
        // 找到逆时针排列的点的集合
        vector<P> s;
        for(int i=l;i<=r;i++)s.push_back(v[i]);
        return s;
    }

    // 分开后返回按逆时针排列好的子集
    int mid = (l+r)/2;
    vector<P> s1=divide_conquer(v, l, mid), s2=divide_conquer(v, mid+1, r);

    // 找到右边点集的上顶点和下顶点。
    int right_top=0, right_bot=0, bot_min=get<1>(s2[0]), top_max=get<1>(s2[0]);
    for(int i=0;i<s2.size();i++)
    {
        int y = get<1>(s2[i]);
        if(y > top_max)top_max=y, right_top=i;
        if(y < bot_min)bot_min=y, right_bot=i;
    }

    // 扎到右边两个集合各自长度
    int len_seq, len_anti;
    if(right_top >= right_bot)
    {
        len_anti = right_top - right_bot + 1;
        len_seq = s2.size() - len_anti;
    }
    else
    {
        len_seq = right_bot - right_top - 1;
        len_anti = s2.size() - len_seq;
    }

    // 构建右边两个集合
    vector<P> r_seq, r_anti;
    for(int i=1;i<=len_seq;i++)
    {
        int j = right_bot - i;
        if(j < 0)j += s2.size();
        r_seq.push_back(s2[j]);
    }
    for(int i=0;i<len_anti;i++)
    {
        int j = (i+right_bot)%s2.size();
        r_anti.push_back(s2[j]);
    }

    // 为了保证按顺序选取所有点,应选取最左边,最靠上(保证刚出去时近的优先)的为基本点
    int left_most = distance(s1.begin(), min_element(s1.begin(), s1.end()));
    P base = s1[left_most];

    // 循环,每次合并一个元素
    vector<P> merged_s{base};
    vector<int> pt{left_most+1, 0, 0};
    for(int i=0;i<s1.size()+s2.size()-1;i++)
    {
        // mod
        if(pt[0] >= s1.size())pt[0] = 0;

        // 每次插入一个元素,要考虑元素连在一起的情况。
        map<P, int> cand; // 将候选放入候选map中 候选点->集合序号
        if(pt[0] != left_most)cand[s1[pt[0]]] = 0;
        if(pt[1] < r_seq.size())cand[r_seq[pt[1]]] = 1;
        if(pt[2] < r_anti.size())cand[r_anti[pt[2]]] = 2;

        // 对cand中的点进行比较
        P max_p = cand.begin()->first;
        for(auto it=cand.begin();++it!=cand.end();)
        {
            P p = it->first;
            // 看p和max_p哪个更符合base的要求
            double ang_best = atan2(get<1>(max_p)-get<1>(base), get<0>(max_p)-get<0>(base));
            double ang_new = atan2(get<1>(p)-get<1>(base), get<0>(p)-get<0>(base));

            // 角度相同时,看和初始点距离
            if(abs(ang_best - ang_new) < 1e-8)
            {
                int d_best = get_manhattan_dist(max_p, base), d_new = get_manhattan_dist(p, base);
                if(d_new < d_best)max_p = p;
            }

            // 角度不同时,比较角度
            else if(ang_new < ang_best)max_p = p;
        }

        merged_s.push_back(max_p);
        pt[cand[max_p]]++;
    }

    // 对相同的点,左右分别旋转一次
    rotate(merged_s, base);

    // 合并后用graham-scan对合并后的集合进行扫描
    vector<P> s_ret{merged_s[0], merged_s[1]};
    for(int i=2;i<merged_s.size();i++)
    {
        while(s_ret.size() >= 2 && isleft(merged_s[i], s_ret[s_ret.size()-2], s_ret[s_ret.size()-1]) < 0)
            s_ret.pop_back();
        s_ret.push_back(merged_s[i]);
    }

    return s_ret;
}

int main()
{
    freopen("in", "r", stdin);
    int t, a, b, N;
    scanf("%d", &N);
    for(int i=0;i<N;i++)
    {
        vector<P> v;
        scanf("%d", &t);
        for (int i = 0; i < t; i++) {
            scanf("%d %d", &a, &b);
            v.push_back(P(a, b));
            scanf("%c %c", &a, &a);
        }

        // 计算s数组,存放凸包的最终结果。
        sort(v.begin(), v.end());
        vector<P> s = divide_conquer(v, 0, v.size()-1);

        // 找到最小的左边
        int start = 0;
        for(int i=1;i<s.size();i++)
        {
            if(get<0>(s[i]) == get<0>(s[start]) && get<1>(s[i]) < get<1>(s[start]))
                start = i;
            else if(get<0>(s[i]) < get<0>(s[start]))
                start = i;
        }

        printf("%d\n", s.size());
        for (int i = 0; i < s.size(); i++)
        {
            int j = (i+start) % s.size();
            printf("%d %d\n", get<0>(s[j]), get<1>(s[j]));
        }

    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值