如何求点集的凸包(凸包求解问题)
1.凸包的定义
凸包(Convex Hull)是一个计算几何(图形学)中的概念。
在一个实数向量空间V中,对于给定集合X,所有包含X的凸集的交集S被称为X的凸包。X的凸包可以用X内所有点(X1,...Xn)的凸组合来构造.
在二维欧几里得空间中,凸包可想象为一条刚好包著所有点的橡皮圈。
用不严谨的话来讲,给定二维平面上的点集,凸包就是将最外层的点连接起来构成的凸多边形,它能包含点集中所有的点。
上图中红线所连接的点是凸包点集的所有元素。
2.求凸包的方法
求凸包的方法有很多,从最初的想法开始考虑。
1.最初求解方法
最简单的想法,从三个点开始考虑。如果三个点不在一条直线上,那么一定可以构成一个三角形,那这个三角形的三个顶点都是凸包点集的元素。
如果这个三角形的内部出现一个(或者多个)新的点pi,那么显然这个点集的凸包集还是只有3个元素{p1,p2,p3}
但是这个三角形外部出现新的一点pj,则pj也是凸包集的一个元素。
于是我们可以非常清楚的知道,点集中任意不在一条直线上的三个点所构成的三角形内部的点一定不是凸包点集中的点。
所以我们可以将可能构成的三角形都遍历一遍,把所有出现在内部的点标记,结束后没有被标记的点就都在凸包集中。
显然,这个方法实现起来比较麻烦,而且复杂度也比较高(O()),所以需要寻找更简单的方法。
2.包装法
凸包就通俗的理解就是把最外层的点连接起来构成的凸多边形,试想一下我们包装礼盒的时候,我们会将礼盒用包装纸先绕礼盒一周包裹起来。
这个事就和凸包非常的相似。求凸包就是找一个“比较坚硬的包装纸”将所有的点都包进来。
这样我们想到先找一个点,然后顺着一个方向绕一圈就能找到所有的凸包点。
可以找到最下方的最右边的点一定在凸包中(暂不证明),我们可以以这个点为起始点来找凸包。
我们采用逆时针的方向寻找凸包。
开始的时候,先定义一个基准方向,这里采用90°(指偏离12点钟方向向右旋转的角度)
不难看出方向向左旋转最小角度的点(右旋最大角度)就是下一个目标点。
初始角度每一次都左旋θ角,依次寻找下一个目标点,当下一个目标点是起始点 的时候可以退出寻找,这样就逆时针寻找到该点集的凸包。
该算法复杂度为O()。
代码如下:
/**
* Given the current direction, current location, and a target location, calculate the Bearing
* towards the target point.
* <p>
* The return value is the angle input to turn() that would point the turtle in the direction of
* the target point (targetX,targetY), given that the turtle is already at the point
* (currentX,currentY) and is facing at angle currentBearing. The angle must be expressed in
* degrees, where 0 <= angle < 360.
* <p>
* HINT: look at http://en.wikipedia.org/wiki/Atan2 and Java's math libraries
*
* @param currentBearing current direction as clockwise from north
* @param currentX current location x-coordinate
* @param currentY current location y-coordinate
* @param targetX target point x-coordinate
* @param targetY target point y-coordinate
* @return adjustment to Bearing (right turn amount) to get to target point,
* must be 0 <= angle < 360
*/
public static double calculateBearingToPoint(double currentBearing, int currentX, int currentY,
int targetX, int targetY) {
if (currentX == targetX && currentY == targetY) return 0.0;
/* 向量Y为指向正北方向的单位向量,即(0,1) X=(x2-x1,y2-y1)为目的向量,
* 则X与Y的右偏转夹角减去当前朝向就是所需要偏转的角度 */
/* 使用余弦定理求偏转角度,θ=arccos(a^2+b^2-C^2/2ab)
* a^2=1,b^2=(x2-x1)^2+(y2-y1)^2,c^2=(x2-x1)^2+(y2-y1-1)^2 如果x2<x1 取360-θ */
/* 计算完θ后,如果θ-currentBearing是负值,则360+(θ-currentBearing)*/
double a = 1.0, b = Math.hypot((targetX - currentX), (targetY - currentY)),
c = Math.hypot((targetX - currentX), (targetY - currentY - 1));
double th = Math.toDegrees(Math.acos((Math.pow(a, 2) + Math.pow(b, 2) - Math.pow(c, 2)) / (2 * a * b * 1.0)));
if (targetX < currentX)
th = 360.0 - th;
if ((th - currentBearing) < 0) return 360 + (th - currentBearing);
else return th - currentBearing;
}
/**
* Given a set of points, compute the convex hull, the smallest convex set that contains all the points
* in a set of input points. The gift-wrapping algorithm is one simple approach to this problem, and
* there are other algorithms too.
*
* @param points a set of points with xCoords and yCoords. It might be empty, contain only 1 point, two points or more.
* @return minimal subset of the input points that form the vertices of the perimeter of the convex hull
*/
public static Set<Point> convexHull(Set<Point> points) {
Set<Point> convexHullSet = new HashSet<>();//定义凸包集
Iterator<Point> it = points.iterator();
Point nowPoint, rightDownPoint = new Point(1000, 1000);//定义现在处于的点和最右下方点初始化值(1000,1000)
boolean flog = false;//标记是否有最右下角点
while (it.hasNext()) //找到最下方最右的点
{
nowPoint = it.next();
if (nowPoint.y() < rightDownPoint.y() || (nowPoint.y() == rightDownPoint.y() &&
nowPoint.x() > rightDownPoint.x())) {
rightDownPoint = nowPoint;
flog = true;
}
}
/* 将最右下角元素导入凸包集 */
if (flog) convexHullSet.add(rightDownPoint);
nowPoint = rightDownPoint;
/* n^2遍历用角度大小获取凸包点集 */
Point nextPoint = rightDownPoint, mayPoint;
double maxDegrees, degrees, baseDegrees = 90.0;
while (true) {
/* x1,y1 为原点坐标(初始值为最右下角点) x2,y2为所需遍历点的坐标,
* 计算向量x2-x1,y2-y1与初值向量的夹角(初始值为90.0,每次更新),
* 找最大的夹角的遍历点作为下一个原点,并压入凸包点集
* 当最右下角点为角度最小点时结束
* */
Iterator<Point> it2 = points.iterator();
maxDegrees = 0.0;
while (it2.hasNext()) {
mayPoint = it2.next();
degrees = calculateBearingToPoint(baseDegrees, (int) nowPoint.x(), (int) nowPoint.y(),
(int) mayPoint.x(), (int) mayPoint.y());
//保存角度大的,或者角度相同距离更远的
if (degrees > maxDegrees || (degrees == maxDegrees && Math.hypot(mayPoint.x() - nowPoint.x(),
mayPoint.y() - nowPoint.y()) > Math.hypot(nextPoint.x() - nowPoint.x(),
nextPoint.y() - nowPoint.y()))) {
maxDegrees = degrees;
nextPoint = mayPoint;
}
}
//更新初值向量
baseDegrees = baseDegrees - (360.0 - maxDegrees) > 0 ? baseDegrees - (360.0 - maxDegrees)
: 360 + baseDegrees - (360.0 - maxDegrees);
nowPoint = nextPoint;
if (nowPoint == rightDownPoint) //凸包求解完毕时结束
break;
else convexHullSet.add(nowPoint);
}
return convexHullSet;
}
目录
目录
还是这个图,当找到最右下角的点之后我们无需每一次都去遍历所有点去找下一个点。
这其中每次都去找一些已经找过的确定不是凸包点集上的点,有没有什么办法可以减少这些没有意义的操作。
我们发现p1~p6每个点都和p7有连线,当我们找到下一个点是p6的时候怎么根据之前的数据帮助寻找下一个凸包点呢?
不难看出,依次求点的时候是不允许右旋的。
可以发现θ,θ2,θ3都是左旋的,满足条件,当p2- >p3的时候θ4是右旋的,所以说p2不满足条件,退回到p1,
接着寻找p1->p3,发现是左旋的,满足条件,直到找到初始点就可以退出了,显然的是,这个操作我们只寻找了一遍所有的点就把凸包找出来了,其中最关键的是
如何找到p6,p1,p2,p3,p4,p5,p7这样的遍历顺序。很容易我们可以看到这个顺序是在p7时的初始方向向左旋转角度的递增排序。
由此可知该算法主要的时间开销是排序,所以复杂度是O()。
伪代码:
{
int rightdownpoint=point1;
for(int i = 0 ;i < Size ; i ++)
{
循环找寻最下且最右的点;
}
for(int i = 0 ;i < Size ; i ++)
{
计算最右下角点与除去本身之外的点左旋角度并存储;
}
sort(角度);//对这些角度排序,得到遍历找凸包的顺序;
push(rightdownpoint);
while(有点没有读)
{
if(nowpoint->nextpoint左旋){push(nextpoint);nextpoint++;}
else {pop(STACK);nextpoint++;}
nowpoint=top(STACK);
}
for(!STACKISEMPTY)
{
pop;//顺时针打印;
}
}