超详细的凸包问题的分治算法说明及C++实现

题目要求: 输入数字, 再输入点(x,y); 以顺时针方向输出最外侧的点, 使外侧的点组成的多边形能包含所有的输入点

参考: <算法设计与分析> 第122-123页

准备步骤: 首先, 将输入点按x轴从小到大排序, 如果x相同,则按y排序


这时候, 我们就能得到p[0]和p[n-1]的直线, 将求解凸包问题分解为求解上凸包和下凸包两个问题.

这里以上凸包为例, 我们要找到一个点Pmax, 使三角形P0-Pmax-Pn-1面积最大. 


直观上也很好理解, 假设我们这里只能找三角形, 那么三角形面积越大, 包含的点肯定就越多.

注意, 这里从P0->Pmax->Pn-1是顺时针顺序, 后面还会用到.


那么怎么求三角形面积呢?

我们可能会想到底x高x1/2的方法, 但是, 这里我们不用这个直观上很简单的方法

我自己理解的原因有两个: 

第一,虽然我这里给出的P0-Pn-1是一条平行于x轴的直线, 但如果说, 它是一条斜线, 这个方法就显然很麻烦;

第二, 这个方法求出来的面积是没有方向的, 这不便于我们进一步把凸包分为上凸包和下凸包进行递归调用


看这张图, 我们可以发现, 如果是上凸包, 从P0->Pmax->Pn-1是顺时针顺序;

如果是下凸包, P0->Pmax->Pn-1是逆时针顺序.

换言之, Pn-1->Pmax->P0是顺时针顺序. (注意这种逆向思维, 后面会用作简化代码)

我们就用这种方式区分上凸包和下凸包

(可能又有人会说了, 我直接用y坐标的大小来判断不就好了嘛. 在这里参见原因一, 如果变为斜线以后就不能用y了)


准备工作做到这里, 我们需要一点数学知识了:

如果我们有三个点A(x1,y1),B(x2,y2), C(x3,y3)

我们可以用以下这个行列式求面积 (也就是我们常说的"叉积")

这个行列式对于平面上任意三角形, 求解面积都很方便, 所得结果是三角形ABC面积的两倍 (可以回忆高中知识~)

而且, 当A->B->C为顺时针顺序时, 该值为正, 反之则为负

 (或者按书上的解释, 当C点在AB左侧时, 该值为正; 我个人觉得顺时针/逆时针的方法更好理解~)

我们可以先写好这个函数的代码:

int Djudge(POINT a1, POINT a2, POINT a3)
{
    int calculate = a1.x*a2.y + a3.x*a1.y + a2.x*a3.y - a3.x*a2.y - a2.x*a1.y - a1.x*a3.y;
    return calculate;
}

然后我们再来考虑, 怎么递归

我们先来考虑上凸包, 这样下凸包就可以如法炮制了~

假设有个函数DealLeft(int first, int last)


我就来以这张图上的情况, 大致的讲讲是怎么递归的~

第一步, 令first=0, last=n-1, 也就是说, 调用DealLeft(0,n-1),求P0PmaxPn-1最大的点

我们从图上可以看到, 找到的三角形内部的点无需再考虑,但外部的点有可能被遗漏.

我们用个标记数组visit, 令其值为1, 标记一下找到的这个Pmax

第二步, 令first=Pmax, last=Pn-1, 看作一个新的凸包问题进一步进行求解, 把外部的点包含进来.

我们发现这个例子中,新的点Pmax2和Pmax, Pn-1构成一个逆时针的三角形 (Pmax2在PmaxPn-1直线的右边), 

我们这时候就考虑: 要不要再写一个DealRight函数呢?

我看了很多其他的递归程序, 一般都是写了上凸包和下凸包两个函数, 我觉得是没有必要的, 为什么呢?

回忆一下, 我们刚刚讲到:

如果是下凸包, P0->Pmax->Pn-1是逆时针顺序. Pn-1->Pmax->P0是顺时针顺序

那么, 我们在求解下凸包时,只要调用DealLeft(n-1,0),也就是把n-1看作起始点,  

就可以把逆时针的三角形转化为顺时针的三角形, 而不用再另外写一个函数.

这也是我为什么觉得顺时针, 逆时针会比"左右"要更好理解一些.

讲到这里, 基本上这个问题已经可以解决啦~


但是, 我们再讲一个细节问题:


在这个例子中, 我们发现, 圆圈中圈出来的点也在凸包上, 但是也在直线Pmax2-Pn-1上.

写程序的时候很容易忽略这一点.

我最开始写求最大值的函数时就没有考虑到, 而递归到这一步时, 函数会认为没有面积更大的三角形了, 就把这个点忽略掉.

这也导致我写出来的代码一直通不过, 如何保证这个点的输出也耗费了我大量时间...

我们注意到, 如果扫描到圆圈这个点时, 三点共线, 求出来的行列式为0.

这时候, 我们也用visit数组给这一点做出标记.

(关于这一点, 我不是很确定; 因为测试平台上只给出了一组数据, 欢迎大家讨论~)


最后,再讨论一下"顺时针输出"这个要求.

如果充分理解了上面讲到行列式的作用, 就比较好理解.

我们以P0-Pn-1直线为分界线, 先输出上凸包, 然后再输出下凸包即可



有的时候, 开心就是长成这个样子的~~~

==========================

附上完整代码~

#include <iostream>
using namespace std;
#include <algorithm>
#include <stdlib.h>
#define N 10000
int n = 0;
struct POINT
{
    int x, y;
}p[N],ans[N];
int visit[N],mark[N];
int Djudge(POINT a1, POINT a2, POINT a3)
{
    int calculate = a1.x*a2.y + a3.x*a1.y + a2.x*a3.y - a3.x*a2.y - a2.x*a1.y - a1.x*a3.y;
    return calculate;
}
bool cmpxy(const POINT a, const POINT b) //按x轴排序,如果x相同,按y轴排序
{
    if (a.x != b.x)
        return a.x < b.x;
    else
        return a.y < b.y;
}
void DealLeft(int first, int last)
{
    int max = 0, index = -1;
    int i = first;
    if (first < last)
    {
        for (i = first+1; i < last; i++) //注意两端,对于first和last,没必要再进行计算
        {
            int calcu = Djudge(p[first], p[i], p[last]);
            if (calcu == 0) {  visit[i] = 1; } //
            if (calcu > max)
            {
                max = calcu;
                index = i;
            }

        }
    }
    else
    {
        for (i-1; i >last; i--) //如果first>last,重复上述过程,注意这里下界不是0.
        {
            int calcu = Djudge(p[first], p[i], p[last]);
            if (calcu == 0) {visit[i] = 1;} //
            if (calcu >  max)
            {
                max = calcu;
                index = i;
            }
        }
    }
    if (index != -1)
    {
        visit[index] = 1; //对取到的点进行标注  
        DealLeft(first, index);
        DealLeft(index, last);//分治的部分
    }
}

int main()
{
    cin >> n;
    for (int i = 0; i < n; i++)
    {
        cin >> p[i].x >> p[i].y;
        visit[i] = 0;
    }
    visit[0] = 1;
    visit[n - 1] = 1;
    sort(p, p + n, cmpxy);
    DealLeft(0, n - 1); //查找上凸包;
    DealLeft(n - 1, 0); //查找下凸包;
    int t = 0;
    for (int i = 0; i < n; i++)
    {
        if (visit[i] == 1)
        {
            ans[t].x = p[i].x;
            ans[t].y = p[i].y;
            t++;
        }
    }
//顺时针输出
    mark[0] = mark[t - 1] = 1; //数组mark避免重复检查降低效率
    for (int i = 1; i < t - 1; i++)
    {
        mark[i] = 0;
    }
    cout << ans[0].x << " " <<ans[0].y<< endl;
    for (int i =1; i < t-1; i++)
    {
        int d = Djudge(ans[0], ans[t-1], ans[i]);
        if (d >= 0)
        {
            cout << ans[i].x << " " << ans[i].y << endl;
            mark[i] = 1;
        }
    }
    cout << ans[t - 1].x << " " << ans[t - 1].y << endl;
    for (int i = 1; i < t; i++)
    {
        if (mark[i] != 1)
        {
            int d = Djudge(ans[0], ans[t - 1], ans[i]);
            if (d < 0)
            {
                cout << ans[i].x << " " << ans[i].y << endl;
            }
        }
    }

    system("pause");
    return 0;
}


评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值