目录
本次实验过程中,任务二包含凸包算法的设计,对该任务的思考与解决记录如下。
1.凸包的定义
1.1凸多边形
了解凸包之前,首先要先明确凸多边形的定义。什么是凸多边形?对于一个多边形,延长其任意一条边形成一条直线,它的其余各条边均在该条直线的同一边,那么该多边形就称为凸多边形。简单来说,凸多边形就是一个没有“凹陷”的多边形。
如下图,第一个图形即为凸多边形,而第二个图形则并不是,它有着一个凹陷。
2.2凸包
明确了凸多边形的定义后,就可以进一步说明凸包的定义。
凸包在计算几何中的定义为:在一个实数向量空间V中,对于给定集合X,所有包含X的凸集的交集S被称为X的凸包。X的凸包可以用X内所有点(X1,...Xn)的凸组合来构造。
用更加通俗的话来讲,对于二维平面上的点集,最外层的点所连成的凸多边形就是该图的凸包,凸包包裹该点集的所有点。
2.计算转向角
为了方便凸包问题的求解,在这里首先解决一个前置问题。以某点作为出发点,面向某个方向,要前往指定坐标点,需要转过多少角度。为了方便计算,我们规定,以向上作为0角度,以顺时针作为正方向,只可顺时针转动。
我们实现该功能的函数命名为calculateBearingToPoint。可知,要实现该函数需要五个参数,即当前面向的角度、当前坐标的X和Y、目标坐标的X和Y。
以下图情况为例进行分析。原点为出发点,蓝点为目标点,红色的线为当前朝向,蓝色的线为目标朝向。绿色弧线标出的为当前角度,称为角1;黄色弧线为计算所得角,称为角2;紫色弧线标出的为需要转向的角度,称为角3。
首先,我们通过Math.atan2()函数计算反三角函数,得到角2的大小。而角1通过函数的参数是已知的,那么我们就可以轻易的得到目标角3的值为:角3 = 90 - 角2 - 角1。
之后,验证该公式对其他情况是否适用。将目标点选取到其他象限以及坐标轴上,带入该公式,发现依旧可以得到所需结果。只是在一些情况中会出现所得结果是负数的情况,因此要通过加360的方式对该情况进行处理。
则该函数完整代码如下:
public static double calculateBearingToPoint(double currentBearing, int currentX, int currentY,
int targetX, int targetY) {
//运用三角函数计算夹角
double angle = Math.atan2(targetY - currentY, targetX - currentX) * 180.0 / Math.PI;
if (angle < 0)//处理小于零的情况
angle += 360.0;
//计算得到与y轴的夹角,并减去当前角
double bearing = (360 - angle + 90 >= 360 ? 90 - angle : 360 - angle + 90) - currentBearing;
//返回一个正值
return bearing < 0 ? 360.0 + bearing : bearing;
}
3.凸包问题求解
首先,根据点集中点的个数,我们将其分为两种情况。当点集中的点少于等于两个时,凸包由点集中的所有点组成,直接将两点返回。当多余两个点时,再进行筛选。这里需要注意,仅含三个点时也不可直接返回,因为可能出现三个点在一条直线的情况,此时只包含两端的两点。
如何进行查找呢?先不考虑计算机的语言,先用我们的思想进行考虑。当从某点出发,我们要连接下一个点,那这个下一个点必然是剩下的点中“最靠外的点”。那这个“最靠外的点”如何通过计算机的语言来表示?很显然。那就是转过角度最小的点。明确了这个,那我们的思路就基本清晰了。
现在,选定一个起始点就可以开始进行比较筛选了。那这个初始点又该如何选取?我们选取的这个点要保证包含在凸包内才可以,即该点要在最外面。结合刚才我们编写计算转角函数时将向上作为0角度,顺时针作为正方向,我们不妨选择最左下的角作为出发点 ,便于我们的思考。因此,在程序开始时,我们要通过循环遍历整个点集,查找横纵坐标最小的点。找到该点后记录下来,并加入到凸包的点集中。
for (Point temp : points){
if (temp.x() < first.x() || (temp.x() == first.x() && temp.y() < first.y()))
first = temp;
}
之后就可以开始对点集进行遍历,通过前一个问题中我们编写的函数,计算每一步的偏角,找到偏角最小的点依次加入集合。
if ((!convexHullPoints.contains(temp) || (temp == first && curpoint != first)) && corner < mincorner) {
mincorner = corner;
nextpoint = temp;
distance = Math.sqrt((temp.y() - curpoint.y()) * (temp.y() - curpoint.y()) + (temp.x() - curpoint.x()) * (temp.x() - curpoint.x()));
}
同时我们应该考虑到,除了这种情况之外,可能会出现两个点转向角相同的情况,对于这种情况我们要选择距离更远的点。
if ((!convexHullPoints.contains(temp) || (temp == first && curpoint != first)) && corner == mincorner) {
if (Math.sqrt((temp.y() - curpoint.y()) * (temp.y() - curpoint.y()) + (temp.x() - curpoint.x()) * (temp.x() - curpoint.x())) > distance) {
nextpoint = temp;
distance = Math.sqrt((temp.y() - curpoint.y()) * (temp.y() - curpoint.y()) + (temp.x() - curpoint.x()) * (temp.x() - curpoint.x()));
}
}
当我们筛选下一个点又返回到起始点时,说明已经形成了一个闭环,结果集合中所储存的即为连接成凸包的点集,将结果点集返回即可。
完整代码如下:
public static Set<Point> convexHull(Set<Point> points) {
if (points.size() < 3) //小于三个点的情况,直接返回
return points;
Set<Point> convexHullPoints = new HashSet<Point>();
Point first = new Point(Integer.MAX_VALUE, Integer.MAX_VALUE);
for (Point temp : points){ //找到最左下角的点
if (temp.x() < first.x() || (temp.x() == first.x() && temp.y() < first.y()))
first = temp;
}
convexHullPoints.add(first); //加入到结果集合
Point curpoint = first, nextpoint = null;
double angle = 0, mincorner = 360, corner = 0, distance = 0;
while (nextpoint != first) { //没有回到起点,即没有结束
for (Point temp : points) { //遍历所有点
//计算当前遍历的点与所处点的转角角度
corner = calculateBearingToPoint(angle, (int) curpoint.x(), (int) curpoint.y(), (int) temp.x(), (int) temp.y());
//角度最小的情况
if ((!convexHullPoints.contains(temp) || (temp == first && curpoint != first)) && corner < mincorner) {
mincorner = corner;
nextpoint = temp;
distance = Math.sqrt((temp.y() - curpoint.y()) * (temp.y() - curpoint.y()) + (temp.x() - curpoint.x()) * (temp.x() - curpoint.x()));
}
//角度相等距离最远的情况
if ((!convexHullPoints.contains(temp) || (temp == first && curpoint != first)) && corner == mincorner) {
if (Math.sqrt((temp.y() - curpoint.y()) * (temp.y() - curpoint.y()) + (temp.x() - curpoint.x()) * (temp.x() - curpoint.x())) > distance) {
nextpoint = temp;
distance = Math.sqrt((temp.y() - curpoint.y()) * (temp.y() - curpoint.y()) + (temp.x() - curpoint.x()) * (temp.x() - curpoint.x()));
}
}
}
convexHullPoints.add(nextpoint); //加入结果集合
curpoint = nextpoint;
angle += mincorner; //当前角度
mincorner = 360;
}
return convexHullPoints;
}