在这些笔记中,我将开发一个算法来解决计算几何中的一个问题。问题是这样的:给定平面上的一组点,构造一个围绕这些点的凸多边形。
这是我们正在寻找的一个例子。在下面的图片中,平面上有一些点。我在这些点周围包了一个多边形,把它们都包起来了。注意,这个多边形也是一个凸多边形,这意味着连接周长上任意两点的任何线段都完全停留在这个多边形中。
下面我将介绍的算法将利用一个特殊的运算,即两个向量的“叉乘”。如果向量有分量吗x1和y1和
向量有分量吗x2和y2我们有
我把“叉乘”这个词加了引号,因为这个运算不是向量微积分中可能遇到的叉乘运算。这个外积只适用于三维空间的向量,不适用于二维空间的向量。我们这里要处理的“外积”就是如果你取一对二维向量,通过加上一个值为0的z坐标,把它们变成三维向量,然后取这两个向量的常规外积,然后返回那个外积的z坐标。
这种“叉乘”操作的重要性在于,它为我们提供了关于二维向量如何相对于彼此定向的有用信息。任何向量在平面上把平面分成两半:
任何其他向量在平面上的方向相同
,或者它会躺在左边或右边
。如果
×
是正的,那么向量呢
谎言的左边
。如果
×
是负的,那么向量呢
谎言的右边
。如果
×
等于0,那么这两个向量在同一个方向上。
由于点对决定向量,我们也可以用这个操作来回答关于点和线段的相对方向的问题。考虑平面上三个点的集合。点p0和p1一起确定一条线段。假设我们想要确定这条线段的哪一边是第三个点,p2谎言。这是我们可以用外积来求的。第一步是形成一对向量:向量向量是沿着有点的线段的方向运动的吗p0和p1作为它的端点,还有向量
向……的方向跑去p0来p2。这两个向量的外积可以告诉我们p2位于线段的左侧或右侧。如果叉乘是正的,p2位于线段的左侧。如果叉乘是负的,p2位于线段的右侧。
下面我们要用到的另一个核心运算是点相对于参考点的极角。下图显示了平面上的点p。
图中的参照点是原点。该点相对于原点的极坐标角θ可以通过公式计算。
θ = arccos(x/|p|)
这个公式的问题是它只适用于位于x轴上方的点。计算极角的一个更有用的公式是使用atan2函数,定义为
平面上一组点Q的凸包是一个凸多边形,它包围了赢博体育的点。给定平面上的一组点,凸包是一条将这些点的子集连接在一起的曲线。下面是解决这个问题的一个算法大纲,Graham扫描算法。
GRAHAM-SCAN (Q)p0是Q中y坐标和x坐标最小的点,设<p1,p2、……pn>为Q中剩余的点,按极角逆时针方向排列p0。如果有多个点具有相同的极角,则除去最远的点以外的赢博体育点p0设S为空堆栈p0, S) (p1, S) (p2,S),当i = 3到m时,由点NEXT-TO-TOP(S), TOP(S),和p我非左转POP(S) PUSH(S)p我,S)返回S
该算法的一个关键步骤是确定三个点的集合是构成左转还是右转。在这种情况下,我们上面定义的“叉乘”是有用的。给定三个点,p1, p2, p3,我们将第一对点组成一条线段。使用上一节中描述的程序,然后我们使用“叉乘”来确定p3是位于该线段的左侧还是右侧。如果p3在左边,这三个点形成一个向左转弯。如果p3在右边,这三个点形成一个向右转弯。
下面是一系列图片,以一个典型的例子来说明该算法的操作。
要在Java中实现这个算法,我们首先要设置两个有用的助手类。第一个是一个简单的类,用来表示平面上的一个点。
包convexhull;公共类点{私有浮点x;Private float y;public Point(float x,float y) {this。X = X;这一点。Y = Y;}公共浮点getX(){返回x;}公共浮动getY(){返回y;}公共空print () {System.out.println(“(”+ x +”,“+ y +”)”);}}
第二个是Vector类,它提供了有用的polarAngle()和crossProduct()方法。
包convexhull;公共类向量{私有float x;Private float y;public Vector(Point from,Point to) {this。x = to.getX() - from.getX();这一点。y = to.getY() - from.getY();}公共浮动polarAngle(){返回(float)数学。量化(y、x);} public static float crossProduct(Vector v0,Vector v1){返回v0.x*v1。Y - v1.x* v1. Y;}}
现在我们准备实现这个算法。
我们从一些从文本文件加载点的代码开始。
new ArrayList<Point>();扫描仪输入;try {input = new Scanner(new File("points.txt"));} catch (Exception ex) {ex. printstacktrace ();返回;} while(input.hasNextFloat()) {float x = input.nextFloat();float y = input.nextFloat();Point p = new Point(x,y);points.add (p);} input.close ();
算法的第一步是找到y坐标最小的点。在平局的情况下,我们将选择具有最小y坐标和最小x坐标的点。为了实现这第一步,我将按照y坐标和x坐标的顺序对点列表进行排序。ArrayList类有一个sort()方法,但它需要一些帮助。为了能够对对象列表进行排序,sort()方法需要能够比较对象对,以确定它们的顺序是否正确。由于sort()方法不知道如何进行这种比较,它将要求我们提供一个方法,该方法将两个对象作为其参数,然后如果第一个对象按照我们想要的顺序出现在第二个对象之前,则返回一个负数,如果两个对象相同则返回0,如果第一个对象按照我们想要的顺序出现在第二个对象之后,则返回一个正数。不幸的是,Java没有提供将一个方法传递给另一个方法的方法,因此我们必须将我们想要传递给sort()的方法放在一个对象中,然后将该对象传递给sort()。这又导致了另一个问题:sort()如何知道要调用对象中的哪个方法?这个问题的解决方案是使用接口。Java类库定义了一个Comparator接口,其中包含一个compare()方法供我们实现。
为了按照我们想要的顺序对点列表进行排序,我们定义了以下类,该类实现了Compartor接口并提供了一个版本的compare()方法:
类CompareXY实现比较器<点>{@覆盖公共int比较(点01,点o2){如果(01 . gety () < 02 . gety()){返回-1;} else if(01 . gety () > 02 . gety()){返回1;} else if(01 . getx () < 02 . getx()){返回-1;} else if(01 . getx () > 02 . getx()){返回1;}返回0;}}
现在我们可以对积分表进行排序了。排序后,我们取出y和x值最小的点。
点。排序(新CompareXY ());点p0 = points.get(0);points.remove (0);
算法的下一步是根据相对于p0点的极坐标对剩下的点进行排序。为此,我们引入另一个比较器类
类comparareangle实现比较器<点>{私有点p0;public CompareAngle(Point p) {p0 = p;} @覆盖公共int比较(点o1,点o2){向量v1 =新向量(p0,o1);Vector v2 = new Vector(p0,o2);float angle1 = v1.polarAngle();float angle2 = v2.polarAngle();如果(angle1 < angle2){返回-1;} else if(angle1 > angle2){返回1;}返回0;}}
然后我们可以把剩下的点按极角排序。
点。排序(新CompareAngle (p0));
接下来我们需要建立一个堆栈,并把三个点推到上面。幸运的是,Java类库有一个可以使用的Stack类。
栈<点> s = new栈<点>();s.push (p0);s.push (points.get (0));s.push (points.get (1));
现在我们已经为算法中的主循环做好了准备。在主循环的主体中,我们需要做的一件事是判断三个点的序列是构成了一个向左转还是向右转。我已经写了一个辅助函数来做这个判断。
public static boolean isRightTurn(Point p0,Point p1,Point p2) {Vector v0 = new Vector(p0,p1);Vector v1 = new Vector(p0,p2);return Vector.crossProduct(v0,v1) < 0;}
下面是算法主循环的代码。
for(int n = 2;n < points.size();n++){下一个点= points.get(n);Point top = s.peek();s.pop ();Point nextToTop = s.peek();s.push(上);while(isRightTurn(nextToTop,top,next)) {s.pop();Top = s.peek();s.pop ();nextToTop = s.peek();s.push(上);} s.push(下);}
在循环结束时,构成边界的点将存储在堆栈中。我们所要做的就是遍历堆栈中的点并将它们打印出来。
for(点p: s) {p.print();}