问题描述】
至少有三条直线边的平面封闭图形称为多边形。图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,最后一个样例无法通过。用网上的各种方法都无法通过该样例。
当然,出题人也有可能失误,这是不可避免的。