实验一 线性结构编程题4. 凸包

问题描述】

至少有三条直线边的平面封闭图形称为多边形。图1a 的多边形有 6条边,图1b 的多边形有 8 条边。多边形既包含其边线上的点,也包含边线内的点。一个多边形如果它的任意两个点的连线都不包含该多边形以外的点,就称为凸多边形,图1a 的多边形是凸多边形,而图1b 的多边形不是凸多边形。图1b 的两条虚线段虽然其端点属于多边形,但它们都包含了多边形以外的点。

一个平面点集S的凸包是指包含S的最小多边形。该多边形的顶点(即角)称为S的极点。图2是平面上的13个点,它的凸包是由实线连成的多边形,极点用圆圈来标识。如果S的点都落在一条直线段上(即这些点是共线点 ),那么S的凸包就退化为包含S的最短直线。

寻找一个平面点集的凸包是计算几何的基本问题。计算几何还有其他问题(如包含一个指定点集的最小矩形),求解这些问题需要计算凸包。此外,凸包在图象处理和统计学中也有应用。

假定在S的凸包内部取一个点X,然后从X向下画一条垂直线(如图3a所示)。后面的算法补充说明1介绍了如何选择点X。这条垂直线与X和S的第i个点的连线之间有一个逆时针夹角,称为极角,用ai表示图3a给出了极角a2。现在按照极角非递减次序来排列S的点,对于极角相同的点,按照它们与X的距离从小到大来排列。在图3 中,所有点按照上述次序被依次编号为1~ 13。

从X向下的垂线沿逆时针扫描,按照极角的次序会依次遇到 S的极点。如果 u、v和w是按照逆时针排列的三个连续的极点,那么从u到v与从w到两条连线之间的时针夹角大于180度。(图3b给出了点8、11、12之间的逆时针夹角。) 当按照极角次序排列的3个连续点之间的逆时针夹角小于或等于 180 度时,第二个点不是极点。当u、v、w间的逆时针夹角小于180度时,如果从u走到v再走到w,那么在v点将会向右转。当按逆时针方向在一个凸多边形上行走时,所有的转弯都是向左转。根据这种观察,得出了以下的算法用以寻找 S的极点和凸包。

 

步骤1) [处理退化情况]
如果S的点少于3个,则返回S
如果 S 的所有点都在一条直线上,即共线,则计算并返回包含S所有点的最短直线的两个端点
 
步骤2) [按极角排序]
在S的凸包内找到一个点X
按照极角递增次序来排列 S 的点,对于极角相同的点,按照它们与X的距离从小到大来排列创建一个以S的点为元素,按照上述顺序排列的双向循环链表
令rght 指向后继,left指向前驱
 
步骤 3)  [删除非极点的点]
令p是y坐标最小的点(也可以是 x 坐标最大的)
for(x-p; rx=x右边的下一个点; p!=rx; )
{
    rrx= rx 右边的点;
    if(x, rx和rrx的逆时针夹角小于或等于 180度)
    {
        从链表中删除 rx;
        rx=x;x=rx 左边的点;
    }
    else {x=rx; rx=rrx;}
}

步骤1) 处理退化情况,即S的点数为1或2,或S的所有点是共线的。这一步用时O(n),其中n是S的点数。判断共线的方法是,任取两个点,求出两点连线的方程式,然后检查余下的 m-2 个点是否在这条直线上。如果所有点是共线的,也可以确定最短直线的端点。 

步骤2) 按照极角的次序排列 S的点,并把它们存人一个双向链表。之所以采用双向链表是因为步骤3)需要消除非极点的点,并在链表中反向移动,当然也可以采用单向链表。因为需要排序,可以采用 O(nlogn)时间内的算法完成排序,因此步骤2)的时间复杂度可以计为 O(nlogn)。

步骤3) 依次检查按逆时针次序排列的三个连续点,如果它们的逆时针夹角小于或等于180度,则中间的点x 不是极点,要从链表中删除之。如果夹角超过 180 度,则rx 可能是也可能不是极点,可将点x移到下一个点。当 for 循环终止时,在链表中每三个连续的点所形成的逆时针夹角都超过 180 度,因此链表的点都是极点。沿着链表的 next 指针域移动,就是按逆时针方向遍历凸包的边界。从y坐标最小的点开始,是因为这个点肯定在凸包上。

现在分析步骤3)的时间复杂度。在 for 循环中,每次检查一个角之后,或者顶点 rx 被删除,x在链表中后移一个位置,或者x前移一个位置。由于被删除的顶点数为 O(n),x 最多向后移动O(n)个位置。因此第二种情形只会发生O(n)次,因而 for 循环将执行 O(n)次。因为检查一个夹角需要耗时Θ(1),所以步骤3)的复杂度为 O(n)。结果,为了找到n个点的凸包,需要耗时 O(nlogn)。

 

算法补充说明:

1)   令u、v、w是平面上的三个点。假设这三点不在同一直线上。编写一个方法,从这三个点所构成的三角形中取一个点。

2)   令S是一个平面点集。编写一个方法来判断S的所有点是否共线。如果共线,计算出包含所有点的最短直线的端点。如果不共线,从点集中找出三个不共线的点。利用这三个点和 1)的方法,确定 S凸包内的一个点。方法的复杂度应为 O(n)。证明这个复杂度成立。

3)   使用1)和2)的代码,把上述的算法细化成一个C++程序。程序的输入为点集 S输出为S的凸包。在输入 S时,可把点存入双向链表之中,然后按照极角对这些点排序。排序时,可使用一个算法复杂度为 O(nlogn)的排序算法。

   

   请输出所用构成凸包的极点,输出顺序为按照横坐标从小到大排列,横坐标相等时,按纵坐标从小到大排列。


【输入形式】

   输入的第一行为一个整数 n,表示点集S中的点数,接下来的n,每行两个整数,表示点集S中的所有点的坐标。
【输出形式】

   输出包括若干行,每行两个整数,表示点集S中构成最大凸包的所有极点的集合,输出顺序为按照横坐标从小到大排列,横坐标相等时,按纵坐标从小到大排列。

【样例输入】

4 4
10 3
18 4
11 4
8 6
13 5
14 7

【样例输出】

4 4
8 6
10 3
14 7
18 4

【样例说明】
【评分标准】

【思路分析】

算法都给出来了还要思路?

注意此题对精度要求很高,如果用double类型数据存储X点,会在后续算法中造成误差,且误差较大。由常理可知,所有点中x最小的那个点一定为凸点,选此点作为X点,可有效减少误差。

判断是否共线需要用到叉积。

对点进行排序时可以用选定点与X点的斜率进行判断,由于X点为最左点,故斜率越大,极角越大。

使用链表时不知道当前点的下标,使用vector容器更佳,并且vector容器支持sort排序,不需要自定义cmp。

#include <iostream>
#include <algorithm>
#include <vector>

using namespace std;
double x, y;

bool cmp(vector<double> a, vector<double> b) {
    return b[2]>a[2];
}

int main() {
    int n;
    cin >> n;
    vector<vector<double>> v;
    for (int i = 0; i < n; ++i) {
        vector<double> tmp;
        int a, b;
        cin >> a >> b;
        tmp.push_back(a);
        tmp.push_back(b);
        v.push_back(tmp);
    }//输入点坐标
    sort(v.begin(), v.end());
    if (n == 1) {
        cout << v[0][0] << " " << v[0][1];
        return 0;
    }
    if (n == 2) {
        cout << v[0][0] << " " << v[0][1] << endl;
        cout << v[1][0] << " " << v[1][1];
        return 0;
    }//特判
    int tmp = n-1;
    bool isCollinear = true;
    while ((--tmp) >= 3) {
        double res = v[0][0] * v[n-1][1] + v[0][1] * v[tmp][0] + v[n-1][0] * v[tmp][1] - v[n-1][1] * v[tmp][0] -
                     v[tmp][1] * v[0][0] - v[n-1][0] * v[0][1];
        if (res != 0) {
            isCollinear = false;
            break;
        }
    }//判断是否共线
    if (isCollinear) {
        for (const auto &item: v) {
            cout << item[0] << " " << item[1] << endl;
        }
        return 0;
    }//若共线,则全部输出
    x = v[0][0];
    y = v[0][1];
    for (auto &item: v) {
        double k;
        if ((item[0] - x) != 0) k = (item[1] - y) / (item[0] - x);
        else if (item[1] > y)k = 999999999999999;
        else k = -999999999999999;
        item.push_back(k);
    }//计算k值(斜率)
    sort(v.begin(), v.end(), cmp);//通过k值排序
    while (true) {
        bool b = false;
        for (int i = 2; i < v.size(); ++i) {
            double res =
                    v[i - 2][0] * v[i - 1][1] + v[i - 2][1] * v[i][0] + v[i - 1][0] * v[i][1] - v[i - 1][1] * v[i][0] -
                    v[i][1] * v[i - 2][0] - v[i - 1][0] * v[i - 2][1];
            if (res < 0) {
                v.erase(v.begin() + i - 1);
                b = true;
                i--;
            }
        }
        if (!b) break;
    }//判断极角,并删除非凸点
    sort(v.begin(), v.end());
    for (const auto &item: v) {
        cout << (int) item[0] << " " << (int) item[1] << endl;
    }
    return 0;
}

然而,cg上的得分只有16,最后一个样例无法通过。用网上的各种方法都无法通过该样例。

当然,出题人也有可能失误,这是不可避免的。

  • 43
    点赞
  • 53
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值