JAVA PTA 大作业2
前言
PTA 上的JAVA大作业, 已经是第二次了。这一次的题目只要是在前几次的点、线、三角形的基础上跟进一步, 添加了四边形、五边形等更加复杂的图形。正因如此, 这次大作业是更多的是对前面框架的补充和修改以及解决上次遗留的问题。因此这里不会再过多的叙述整体的设计。想要查看之前的作业可以点击这里。
设计
在讲设计前, 我们还是先看看题目
- 第一题
用户输入一组选项和数据,进行与四边形有关的计算。
以下四边形顶点的坐标要求按顺序依次输入,连续输入的两个顶点是相邻顶点,第一个和最后一个输入的顶点相邻。
选项包括:
1:输入四个点坐标,判断是否是四边形、平行四边形,判断结果输出true/false,结果之间以一个英文空格符分隔。
2:输入四个点坐标,判断是否是菱形、矩形、正方形,判断结果输出true/false,结果之间以一个英文空格符分隔。 若四个点坐标无法构成四边形,输出"not a quadrilateral"
3:输入四个点坐标,判断是凹四边形(false)还是凸四边形(true),输出四边形周长、面积,结果之间以一个英文空格符分隔。 若四个点坐标无法构成四边形,输出"not a quadrilateral"
4:输入六个点坐标,前两个点构成一条直线,后四个点构成一个四边形或三角形,输出直线与四边形(也可能是三角形)相交的交点数量。如果交点有两个,再按面积从小到大输出四边形(或三角形)被直线分割成两部分的面积(不换行)。若直线与四边形或三角形的一条边线重合,输出"The line is coincide with one of the lines"。若后四个点不符合四边形或三角形的输入,输出"not a quadrilateral or triangle"。
后四个点构成三角形的情况:假设三角形一条边上两个端点分别是x、y,边线中间有一点z,另一顶点s:
1)符合要求的输入:顶点重复或者z与xy都相邻,如x x y s、x z y s、x y x s、s x y y。此时去除冗余点,保留一个x、一个y。
2) 不符合要求的输入:z 不与xy都相邻,如z x y s、x z s y、x s z y
5:输入五个点坐标,输出第一个是否在后四个点所构成的四边形(限定为凸四边形,不考虑凹四边形)或三角形(判定方法见选项4)的内部(若是四边形输出in the quadrilateral/outof the quadrilateral,若是三角形输出in the triangle/outof the triangle)。如果点在多边形的某条边上,输出"on the triangle或者on the quadrilateral"。若后四个点不符合四边形或三角形,输出"not a quadrilateral or triangle"。 - 第二题
用户输入一组选项和数据,进行与五边形有关的计算。
以下五边形顶点的坐标要求按顺序依次输入,连续输入的两个顶点是相邻顶点,第一个和最后一个输入的顶点相邻。
选项包括:
1:输入五个点坐标,判断是否是五边形,判断结果输出true/false。
2:输入五个点坐标,判断是凹五边形(false)还是凸五边形(true),如果是凸五边形,则再输出五边形周长、面积,结果之间以一个英文空格符分隔。 若五个点坐标无法构成五边形,输出"not a pentagon"。
3:输入七个点坐标,前两个点构成一条直线,后五个点构成一个凸五边形、凸四边形或凸三角形,输出直线与五边形、四边形或三角形相交的交点数量。如果交点有两个,再按面积从小到大输出被直线分割成两部分的面积(不换行)。若直线与多边形形的一条边线重合,输出"The line is coincide with one of the lines"。若后五个点不符合五边形输入,若前两点重合,输出"points coincide"。
以上3选项中,若输入的点无法构成多边形,则输出"not a polygon"。
4:输入十个点坐标,前、后五个点分别构成一个凸多边形(三角形、四边形、五边形),判断它们两个之间是否存在包含关系(一个多边形有一条或多条边与另一个多边形重合,其他部分都包含在另一个多边形内部,也算包含)。
两者存在六种关系:
1、分离(完全无重合点)2、连接(只有一个点或一条边重合) 3、完全重合 4、被包含(前一个多边形在后一个多边形的内部)5、交错 6、包含(后一个多边形在前一个多边形的内部)。
5:输入十个点坐标,前、后五个点分别构成一个凸多边形(三角形、四边形、五边形),输出两个多边形公共区域的面积。注:只考虑每个多边形被另一个多边形分割成最多两个部分的情况,不考虑一个多边形将另一个分割成超过两个区域的情况。
6:输入六个点坐标,输出第一个是否在后五个点所构成的多边形(限定为凸多边形,不考虑凹多边形),的内部(若是五边形输出in the pentagon/outof the pentagon,若是四边形输出in the quadrilateral/outof the quadrilateral,若是三角形输出in the triangle/outof the triangle)。输入入错存在冗余点要排除,冗余点的判定方法见选项5。如果点在多边形的某条边上,输出"on the triangle/on the quadrilateral/on the pentagon"。
注意这里故意没有复制输入输入要求, 因为和前面是一样的。
阅读题目我们可以发现。后面的题目要求和之前的三角形有共同点, 相同的地方主要有
- 都要判断构成多边形的合法性
- 都要求多边形的面积、周长
- 都有多边形被线分割求面积的要求
- 四边形和五边形都要求判断凹凸性
- 都要判断点是否在多边形内部
但是题目也有新的要求:
- 需要判断不同多边形之间的位置关系
- 需要求多边形相交的面积
- 需要过滤点, 判读是几边形
- 需要判断四边形的形状
根据以上的要求我们可以开始设计。
- 多边形的合法性根据前面的框架在构造函数解决
- 求周长、面积已经在多边形类中实现与多边形边数无关的算法。
- 在先前被线分割是针对三角形单独实现在三角形类中的, 这也是上次问题有点大的一个方法。现在需要分割四边形和五边形!经过我的初步尝试, 如果对于不同边数的多边形都实现一个方法, 在四边形的时候就过于复杂。思考后, 我认为可以在这里实现一个与多边形边数无关的算法。
- 判断凹凸性。我认为也可以在多边形类中实现一个和多边形边数无关的算法。
- 判断点是否在多边形内, 前面采用的射线法实现在多边形类内, 就是一个与多边形边数无关的算法。
- 判断多边形位置关系。如果分别考虑三角形和三角形相交, 三角形和四边形相交。。。种类太多太杂。所以还是要想办法找一个万能的方法。因此这个方法必须实现再多边形中。
- 求多边形相交面积。同上, 也必须实现在多边形类中
- 过滤点以及判断是几变形我们可以写一个Util(工具)类 实现一些不太好分类的方法,
主要是懒, 不想再写类了 - 判断四边形形状。这只能实现在四边形形类中。
经过以上的分析, 我的类图最终变成了这样(这里是写好了所有方法之后的类图, 刻意隐去了一些private方法)

以及新建了一个Util类类图如下:
实现
通过以上的设计, 我们就可以开始实现。这里一个一个问题进行解决
1. 过滤点和构建多边形
与同学交流发现, 很多人(至少我认识的大多数)都直接把这个操作写在了四边形(五边形类中)。然后用一个方法判断能否构成四边形,不能构成就返回一个false。然后再三角形类中在根据点的关系(那几个点共线, 那几个点重合)构造一个三角形。我认为这样写是非常非常不合理的。因为构成能否三角形就不能依赖于四边形类中的方法。如果四边形类中的方法就写错了, 后面的就全部没有用了。并且如果这样写, 如果要增加五边形、六边形、甚至更多边数的多边形就是噩梦。
我认为比较好的做法是在构造多边形之前就要确定到底是几边形。也就是构造多边形之前就要知道有效点的数量。所以过滤的方法在这里单独拿出来在Util类中。在输入完成后过滤点。再根据点的数量构造多边形。
-
首先是要移除重复的点
// file Util.java // class Util /** * 移除点数组中的重复点 * 这里采用 list 去重的方式, 主要是为了保持原来点的顺序。 * @param points 需要去重的点的数组 * @return 返回去重之后的点 */ public static Point[] removeRepeatPoint(Point[] points) { List<Point> old = new ArrayList<>(List.of(points)); List<Point> t = new ArrayList<>(); old.forEach(point -> { if (!t.contains(point)) t.add(point); }); return t.toArray(new Point[0]); } -
然后要移除三点共线的点
// file Util.java // class Util /** * 移除相邻的三点共线的点 * 这里主要是通过判断前后点构成的线是否平行 * 如果平行则中间点必须在前后两点构成的线段上 * @param points 需要判断的点的数组 * @return 返回过滤之后的点 */ public static Point[] removeOneLinePointOnMid(Point[] points) { List<Point> old = new ArrayList<>(List.of(points)); int length = points.length; Line[] lines = new Line[length]; lines[0] = new Line(points[0], points[1]); for (int i = 1; i < length; ++i) { lines[i] = new Line(points[i], points[(i + 1) % length]); if (lines[i].isParallelLine(lines[i - 1]) && old.size() != 2) { if (Line.isBetweenLinePoint(points[i - 1] , points[(i + 1) % length], points[i])) old.remove(points[i]); } } // 注意特判第一个点是否在最后一个点和第二个点构成的线上 if (lines[length - 1].isParallelLine(lines[0])) { if (Line.isBetweenLinePoint(points[length - 1], points[1], points[0])) old.remove(points[0]); } return old.toArray(new Point[0]); } -
构建多边形
我们把点过滤出来之后就可以根据点的数量构建多边形。更进一步,我们可以利用把构建多边形封装成一个方法, 类似于设计模式里面的工厂模式。如下:
// file Util.java // class Util /** * 根据点的数量构建多边形 * @param points 传入的点集 * @return 返回一个多边形 */ public static Polygon buildPolygon(Point[] points) { if (points.length == 3) return new Triangle(points); if (points.length == 4) return new Quadrilateral(points); if (points.length == 5) return new Pentagon(points); assert false; return null; }有人可能会说这样子就失去了这个多边形到底是几条边这个信息。但是没有关系, 应为根据我们上面的设计, 执行的各种方法已经和基本和这个多边形有几条边没有关系。再不济还可以在子类覆盖重写方法。
2. 判段多边形凹凸性
判断多边形的凹凸性, 这里为了确保对于任意多边形都成立。这里直接使用凸多边形的定义:
凸多边形(Convex Polygon)指如果把一个多边形的所有边中,任意一条边向两方无限延长成为一直线时,其他各边都在此直线的同旁,那么这个多边形就叫做凸多边形
也就是说,我们只需要循环检查多边形的每一条边, 查看剩余的点是否在多边形一侧, 如果有一个点不符合就可以断定为凹多边形。所以我们首先要有一个判断点是否在线一侧的方法。
也就是下面这样
// file Line.java
// class Line
/**
* 判断 点是否在 线的 左边(上面)
* 如果点在线上则认为点不在 线的左边(上面)
* 如果线没有斜率, 则通比较横坐标
* 否则通过比较纵坐标
* @param p 要比较的点
* @return 返回点是否在线的 左侧(上面)
*/
public boolean isPointOnLineLeft(Point p) {
if (isLinePoint(p)) return false;
if (!isNoSlope && Compare.isEqual(getSlope(), 0))
return Compare.isLess(c, p.getY());
double a = getXByY(p.getY());
return Compare.isLess(a, p.getX());
}
有了上面这个方法, 就可以在多边形类中写判断是否为凸多边形的方法了。
// file Polygon.java
// class Polygon
/**
* 判断多边形是否为凸多边形
* 根据凸多变形的定义, 如果这个多边形是凸多边形,
* 则所有的点都在线的一侧
* @return 返回是否是图多边形
*/
public boolean isConvexPolygon() {
int len = vertexes.length;
Point[] p = vertexes;
for (int i = 1; i < len; ++i) {
boolean start = sides[i].isPointOnLineLeft(p[(i + 2) % len]);
for (int j = 3; j < len; ++j) {
boolean now = sides[i].isPointOnLineLeft(p[(i + j) % len]);
if (start != now) return false;
}
}
return true;
}
好像还有一种用向量叉乘判断是否是凸多边形的方法,但是这里直接用定义就解决了, 而且也不是很难。就不去管其他的方法了。
3. 分割多边形
在之前的三角形类里面就写过一个分割三角形的方法。那时的思路是考虑线过点的位置关系。也就是考虑线过一个顶点的情况和线没有过顶点的情况。这个情况还比较少, 但是当时写下来还是写的比较复杂。在写四边形的时候, 一开始我也是考虑线过四边形过一个点、两个点的情况。但是发现有一种情况也就是这样

特别难确定交点的顺序关系(至少按我当时写的算法但是是这样的。。但是经过我的测试, 样例里面好像没有这种情况, 手动狗头)。然后就放弃了。开始考虑一种万能的方法。
然后就发现, 一条线如果和凸多边形相交。这条线只会把凸多边形分成两个部分。此时被分割成的多边形点的分布只有三种情况。
- 在分割的线上
- 在分割的线左边(线下方)
- 在分割的线右边(线上方)
所以我们只需要根据原来多边形的点以及多边形和线交点与线相对的位置关系, 把点分成两个部分, 再根据点的数量构建一个多边形就可以了!!根据点的数量构建多边形我已经在上面解决, 判断点在线的上方或者下方上面也已经解决。剩下一个关键的问题就是怎么确定点的顺序。注意到其实输入的时候就是按点的顺序输入的。我们就可以利用输入的时候的点的顺序, 绕多边形一周把点分成两个集合!!具体的过程可以看下面这个动图。

根据上面描述的过程。我们就可以开始写代码。
首先, 我们要有写一个方法获得割线割多边形对应的点和线(也就是上面图中的点4和点6以及点4和点6对应的边。注意, 因为点4是顶点,所以对应两条边);
// file Polygon.java
// class Polygon
/**
* 获得线分割点 对应的 多边形的边和点
* 返回值以 Pair<Point, Line> 的形式出现
* 分别表示 点 以及点对应的 线
* 如果交点是顶点 则会把交点对应的两条边都返回
* @param l 要相交的线
* @return 返回对应的点线Pair的ArrayList
*/
private ArrayList<Pair<Point, Line>> intersectByLine(Line l) {
ArrayList<Pair<Point, Line>> pairs = new ArrayList<>();
int length = sides.length;
for (int i = 0; i < length; ++i) {
if (l.isParallelLine(sides[i])) continue;
Point p = l.intersectionPoint(sides[i]);
if (Line.isBetweenLinePoint(vertexes[i], vertexes[(i + 1) % length], p)
|| isPolygonVertex(p))
pairs.add(new Pair<>(p, sides[i]));
}
return pairs;
}
然后就可以开始写splitByLine这个方法了。
注意这个方法返回的是 Pair<Integer, ArrayList
分别对应交点的个数以及被切割成的两个多边形(这里使用了多态)。
// file Polygon.java
// class Polygon
/**
* 使用 线切割多边形
* 如果线和多边形的一条边重合抛出异常
* RuntimeException("The line is coincide with one of the line");
* 如果线切割多边形没有构成多边形则 ArrayList 为空
* @param l 要切割的线
* @return 返回交点个数和被切成的多边形
*/
public Pair<Integer, ArrayList<Polygon>> splitByLine(Line l) {
if (isPolygonLine(l))
throw new RuntimeException("The line is coincide with one of the line");
ArrayList<Pair<Point, Line>> pairs = intersectByLine(l);
// 没有交点
if (pairs.isEmpty()) return new Pair<>(0, new ArrayList<>());
// 有且只有一个交点
if (pairs.size() == 2 &&
pairs.get(0).getFirst().equals(pairs.get(1).getFirst()))
return new Pair<>(1, new ArrayList<>());
// 两个以上交点
Pair<ArrayList<Point>, ArrayList<Point>>
collection = getSplitPointCollection(pairs, l);
ArrayList<Polygon> polygons = new ArrayList<>();
ArrayList<Point> p1 = collection.getFirst();
ArrayList<Point> p2 = collection.getSecond();
// 去重后 构建多边形
Point[] parr1 = Utils.removeRepeatPoint(p1.toArray(new Point[0]));
Point[] parr2 = Utils.removeRepeatPoint(p2.toArray(new Point[0]));
polygons.add(Utils.buildPolygon(parr1));
polygons.add(Utils.buildPolygon(parr2));
return new Pair<>(2, polygons);
}
注意到这个方法里面有一个getSplitPointCollection, 这个方法的作用是把 Pair 种的 点 分成两个集合并放回 对应的两个ArrayList。如下
// file Polygon.java
// class Polygon
/**
* splitByLine 方法的辅助方法。
* 将传入的Pair中的点以及多边形的顶点 根据割线分成两个集合
* 如果点在割线上方 则点被加入集合 1
* 如果点在割线下方 则点被加入集合 2
* 如果点在割线上 则 同时加入集合1和集合2
* @param pairs 传入的 pair
* @param l 割线
* @return 返回被分割而成的两个多边形点的集合
*/
private Pair<ArrayList<Point>, ArrayList<Point>>
getSplitPointCollection(ArrayList<Pair<Point, Line>> pairs, Line l) {
// 如果 Pair 中的 点是交点 则 一个点会对应两条边
// 所以这里先去重
pairs = removeRepeatPairByFirst(pairs);
int side1 = indexOf(pairs.get(0).getSecond());
int side2 = indexOf(pairs.get(1).getSecond());
ArrayList<Point> lower = new ArrayList<>();
ArrayList<Point> upper = new ArrayList<>();
for (int i = 0; i < vertexes.length; ++i) {
if (i == side1) {
if (!pairs.get(0).getFirst().equals(vertexes[i]))
splitAddNewPoint(l, i, lower, upper);
lower.add(pairs.get(0).getFirst());
upper.add(pairs.get(0).getFirst());
} else if (i == side2) {
if (!pairs.get(1).getFirst().equals(vertexes[i]))
splitAddNewPoint(l, i, lower, upper);
lower.add(pairs.get(1).getFirst());
upper.add(pairs.get(1).getFirst());
} else {
splitAddNewPoint(l, i, lower, upper);
}
}
return new Pair<>(lower, upper);
}
/**
* getSplitPointCollection 的 辅助方法
* 根据 Pair 的 第一个元素 First 去重
* @param pairs 去重的Pair
* @return 返回去重后的 Pair
*/
private ArrayList<Pair<Point, Line>>
removeRepeatPairByFirst(ArrayList<Pair<Point, Line>> pairs) {
HashSet<Point> points = new HashSet<>();
ArrayList<Pair<Point, Line>> arrayList = new ArrayList<>();
for (Pair<Point, Line> p : pairs) {
if (!points.contains(p.getFirst())) {
points.add(p.getFirst());
arrayList.add(p);
}
}
return arrayList;
}
/**
* getSplitPointCollection的辅助函数, 把点加入对应的集合
* 参数就不写了, 都看得懂。
* 注意因为 JAVA 传对象默认是 引用(指针)
* 所以在 这里 使用 upper(lower).add 会影响到外面的 upper(lower)
*/
private void
splitAddNewPoint(Line l, int index
, ArrayList<Point> lower, ArrayList<Point> upper) {
if (l.isPointOnLineLeft(vertexes[index]))
lower.add(vertexes[index]);
else
upper.add(vertexes[index]);
}
4. 判断多边形位置关系
判断多边形的位置关系在这里主要使用分离轴定理(SAT), 这是一种游戏中常用的碰撞检测算法。
因为网上搜到没有具体的定义。这里简单描述一下
对于任意两个凸多边形, 从任意角度将多边形投影到一条线上, 如果存在一个角度在线上投影的区间没有重合, 则两个多边形分离。
从上面的定义可以发现, 我们需要求任意角度的投影判断投影的区间是否相交。实际上, 任意的角度我也不知道怎么实现。但是有大佬指出实际上自需要从多边形的每一条边垂直的方向做投影就足够了。更详细的介绍可以看一看这篇博客。
看到这里有人可能会有疑问。分离轴定理实际上是检测多边形是否分离的。但是题目要求检测多边形是否包含、相交、连接、重合等等。这和分离轴定理有什么关系。
确实是这样。分离轴定理不能检测这么多的状态。但是换一个角度想一想。如果两个多边形是包含关系。那么他们之间的投影区间也一定是包含关系。如果多边形是相交关系。那么, 他们对应的投影至少有一个为相交的关系, 并且没有分离的关系(因为投影的角度有很多, 并不是所有区间都是相交的关系)。同理, 如果是分离关系则有上面的定义。所有投影的区间一定是分离关系。
这里最不好处理的就是连接关系。应为上述的方法确实没有办法区分相交的情况。但是不要紧。分离的判断相对于其他的判断来说比较简单。只需单独写一个算法。而重合就最简单了。判断线是否相等即可。
总结一下就是下面这样(下面这张图相交的情况画错了, 因为投影的线没有和边垂直, 原图已经删了, 就不修改了):

所以通过以上的分析。问题就变成了我们要怎么求投影的区间。这其实可以分解成一下两个问题?
- 垂直于多边形的某条边有无线条。到底要投影到哪条?
- 就算知道了要投影到那条垂直的边, 又要怎么算投影的区间?
其实我们可以通过向量解决上面两个问题(有人可能会说这是不是又要写向量类, 其实不用, 因为一个点就可以代表一个向量)。
首先解决第一个问题。我们知道如果两条边互相垂直且斜率存在则有 \(k1 \times k2 = -1\)。所以对于一个斜率为 \(k\) 的边, 向量\((k, -1)\)一定垂直于这条边。对于没有斜率的边, 可以特判返回向量\((1, 0)\);因此, 可以写出下面的代码
// file Polygon.java
// class Polygon
/**
* 获得垂直于一条边的向量
* 这里使用 Point 类代表一个向量
* 如果线有斜率返回向量 (k, -1)
* 否则返回向量 (1,0)
* @param s 要求的线
* @return 返回垂直于该线的向量
*/
private static Point getVerticalVector(Line s) {
if(s.isNoSlope()) return new Point(1, 0);
return new Point(s.getSlope(), -1);
}
第二个问题就是要求区间了。要求区间其实就将多边形所有的点投影在线上。然后取两端的点作为区间的范围。所以实际要解决的问题是怎么找出点在线上的投影坐标。这里先给出结论。
一个向量(也就是一个点)假设为\((a, b)\)在另外一个向量上,假设为\((x, y)\)的投影向量为 \((\frac{ax^2 + bxy}{x^2+y^2}, \frac{axy + by^2}{x^2+y^2})\)。
也就是说,对于多边形上的一点\((a,b)\), 投影到点\((0,0)\)和点\((x,y)\)构成的线上的坐标为\((\frac{ax^2 + bxy}{x^2+y^2}, \frac{axy + by^2}{x^2+y^2})\)
下面来看证明:

如图要求 \(\vec{p}\) 在向量 \(\vec{a}\) 上的投影
做出 \(\vec{e}\) 使得 \(\vec{e}\) ⊥ \(\vec{a}\), 令交点向量为 \(x \times \vec{a} (x是一个常数)\)。有
由(1)有
左右移项化简得
两边同时乘 a
将具体的数值带入公式(4)进行计算
假设 \(\vec{a} 为(x,y), \vec{p}为(a,b)\)
既向量\(\vec{p}在\vec{a}上的投影为\vec{a}x = (\frac{ax^2 + bxy}{x^2+y^2}, \frac{axy + by^2}{x^2+y^2})\)
更进一步, 注意到两个坐标分母都是 \(x^2 + y^2\), 反正都是求投影, 将所有的投影放大\(x^2 + y^2\)倍, 区间的关系也不会变。
因此, 我们可以写出下面的代码
// file Polygon.java
// class Polygon
/**
* 将一个向量投影到另一个向量
* 注意投影到的向量不能为(0,0)
* @param which 被投影的向量
* @param to 投影到的向量
* @return 返回投影的向量 扩大(vx ^ 2 + vy ^ 2) 倍
* vx vy 为投影到的向量的坐标
*/
private static Point projectionToVector(Point which, Point to) {
double vx = to.getX();
double vy = to.getY();
double wx = which.getX();
double wy = which.getY();
double x = wx * vx * vx + wy * vx * vy;
double y = wx * vx * vy + wy * vy * vy;
return new Point(x, y);
}
有了一个点在向量上的投影, 我们就可以开始求区间了。更进一步, 实际上所有的点都在一条线(向量上), 要比较投影区间就是比较最大和的最小的\(x\)坐标, 当然还要特判斜路不存在的情况, 此时比较最大和最小的y坐标。因此有下面的代码。
// file Polygon.java
// class Polygon
/**
* 获得多边形在向量上的投影区间
* 如果投影的向量有斜率则返回 x 的区间
* 否则放回 y 的区间
* @param vector 要投影到的 向量
* @param polygon 多边形
* @return 返回区间 [interval[0], interval[1]]
*/
private static double[] getProjectInterval(Point vector, Polygon polygon) {
ArrayList<Point> otherProject = new ArrayList<>();
// 先获得多边形每一个点在向量上的投影
for(Point p : polygon.vertexes)
otherProject.add(projectionToVector(p, vector));
double[] interval = new double[]{ Double.MAX_VALUE, -Double.MAX_VALUE };
boolean b = Compare.isEqual(vector.getX(), 0);
// 循环比较取得区间范围
for(Point p : otherProject) {
double value = b ? p.getY() : p.getX();
interval[0] = Math.min(value, interval[0]);
interval[1] = Math.max(value, interval[1]);
}
return interval;
}
以上我们就得到了区间啦。然后就可以开始判断多边形之间的关系
首先是最简单的包含
// file Polygon.java
// class Polygon
/**
* 判断多边形是否包含另一个多边形
* @param polygon 被怀疑包含在里面的多边形
* @return 返回是否包含的 boolean 值
*/
public boolean isContains(Polygon polygon) {
for(Line l : sides) {
// 获得垂直的向量
Point vector = getVerticalVector(l);
// 获得投影的区间
double[] myInterval = getProjectInterval(vector, this);
double[] otherInterval = getProjectInterval(vector, polygon);
// 区间只能为包含关系
if(!(Compare.isLessOrEqual(myInterval[0], otherInterval[0])
&& Compare.isGreatOrEqual(myInterval[1], otherInterval[1])))
return false;
}
return true;
}
然后是同样简单的分离
// file Polygon.java
// class Polygon
/**
* 判断多边形是否分离
* @param polygon 被怀疑分离的多边形
* @return 返回是否分离的 boolean 值
*/
public boolean isSeparate(Polygon polygon) {
for(Line l : sides) {
// 获得垂直的向量
Point vector = getVerticalVector(l);
// 获得投影区间
double[] myInterval = getProjectInterval(vector, this);
double[] otherInterval = getProjectInterval(vector, polygon);
// 区间存在分离关系这多边形分离
double max = Math.min(myInterval[1], otherInterval[1]);
double min = Math.max(myInterval[0], otherInterval[0]);
double len = max - min;
if(Compare.isLess(len, 0)) return true;
}
return false;
}
然后是稍微复杂一点的相交
// file Polygon.java
// class Polygon
/**
* 判断多边形是否相交
* @param polygon 被怀疑相交的多边形
* @return 返回是否相交的 boolean 值
*/
public boolean isInterlace(Polygon polygon) {
int interlaceCnt = 0;
for(Line l : sides) {
// 获得垂直向量
Point vector = getVerticalVector(l);
// 获得投影区间
double[] myInterval = getProjectInterval(vector, this);
double[] otherInterval = getProjectInterval(vector, polygon);
// 区间至少要有一个相交, 并且没有区间分离
double max = Math.min(myInterval[1], otherInterval[1]);
double min = Math.max(myInterval[0], otherInterval[0]);
double minLen = Math.min(myInterval[1] - myInterval[0],
otherInterval[1] - otherInterval[0]);
double len = max - min;
if(Compare.isLess(len, 0))
return false;
if(Compare.isLessOrEqual(len, minLen)
&& Compare.isNotEqual(len, 0))
++interlaceCnt;
}
return Compare.isGreat(interlaceCnt, 0);
}
因为使用这种方法, 连接会被判断成相交, 所以需要特判, 但也不会很难。
这里判断连接的思路是:
没有点在另一个多边形内, 两个多边形没有交点在边上(不包括顶点), 如果有交点, 则交点只能有一个。如果有边重合, 则构成边的两个点必须相同。
有人可能又会说。这不是又要添加很多方法(因为要判断每个点是否在多边形内, 还要求多边形交点)。但是实际上, 这些方法一开始是为了求多边形相交面积准备的, 也就是下一个要解决的问题。所以这里只是借用了一下下面要用的方法。实际上新增的代码不会很多。因此这里直接跳过这些方法, 直接看判断多边形连接的算法。
// file Polygon.java
// class Polygon
/**
* 判断多边形是否连接
* @param p 被怀疑连接的多边形
* @return 返回是否连接的boolean值
*/
public boolean isConnected(Polygon p) {
// 排除包含关系
if(isContains(p) || p.isContains(this))
return false;
// 没有点在另一个多边形内
ArrayList<Point> emptyArr = pointsInPolygon(p);
if(!emptyArr.isEmpty()) return false;
// 交点不能在多边形的边上(这里实际上计算了顶点)
ArrayList<Point> points = intersectionPoint(p);
Point[] arr = Utils.removeRepeatPoint(points.toArray(new Point[0]));
// 没有交点
if(arr.length == 0) return false;
// 一个交点必须为顶点
if(arr.length == 1) {
boolean a = isPolygonVertex(points.get(0));
boolean b = p.isPolygonVertex(points.get(0));
return a && b;
}
// 两个以上交点不能在边上
for(Point point : arr)
if(isPointOnPolygonSide(point)) return false;
return arr.length == 2;
}
回过头来看, 这里好像没有判断边重合的情况。擦, 难怪一直有个连接的点过不去。。
判断重合太简单了,为了不让文章太长就不写了,手动狗头。
5. 求多边形相交面积
要求相交的面积。我们可以延续用线分割多边形的思路。先把点找出来, 然后再用点构成一个多边形。要找的点有两种, 第一种是再另一个多边形内的点。第二种是多边形互相的交点。问题在于怎么让点按照一定顺序排列。因为这没有办法绕多边形选择点。但是解决办法还是有的。我们只需要按照点的极角对点进行排序就可以了(如果极角相同, 就按极径大小排序)。但是我们必须排除一种情况。也就是原点和多边形的对角线共线的情况(这个时候按极径大小排就会出错)。解决的思路也很简单: 以多变形的一个顶点为准, 把多边形的这个顶点平移到原点即可。总的来说就像这样:

所以根据上面的思路, 就可以写代码了。
首先要有一个获得所有在另一个多边形内的方法
// file Polygon.java
// class Polygon
/**
* 获得多边形所有在另一个多边形内的点
* 其实只是循环调用 isPointInPolygon
* @param polygon 另一个多边形
* @return 返回点的 ArrayList
*/
private ArrayList<Point> pointsInPolygon(Polygon polygon) {
ArrayList<Point> points = new ArrayList<>();
for(Point p : vertexes) {
try {
if(polygon.isPointInPolygon(p))
points.add(p);
} catch (Exception ignore) {}
}
for(Point p : polygon.vertexes) {
try {
if(isPointInPolygon(p))
points.add(p);
} catch (Exception ignore) {}
}
return points;
}
然后要有一个获得所有交点的方法
// file Polygon.java
// class Polygon
/**
* 获得和另一个多边形的ArrayList
* 其实只是循环调用 intersectionPoint 和 isPointOnPolygonSide
* @param p 另一个多边形
* @return 返回点的集合
*/
private ArrayList<Point> intersectionPoint(Polygon p) {
ArrayList<Point> points = new ArrayList<>();
int mLen = vertexes.length;
int pLen = p.vertexes.length;
for(int i = 0; i < mLen; ++i) {
for(int j = 0; j < pLen; ++j) {
if(sides[i].isParallelLine(p.sides[j])) continue;
Point in = sides[i].intersectionPoint(p.sides[j]);
boolean a = isPointOnPolygonSide(in);
boolean b = p.isPointOnPolygonSide(in);
if(a && b) points.add(in);
else if(a && p.isPolygonVertex(in))
points.add(in);
else if(b && isPolygonVertex(in))
points.add(in);
else if(isPolygonVertex(in) && p.isPolygonVertex(in))
points.add(in);
}
}
return points;
}
再接着要有一个平移点的方法
// file Polygon.java
// class Polygon
/**
* 将点集 以第一个点为基准, 平移到原点
* @param arr 点集
* @return 返回平移后的点集
*/
private Point[] translateOnePointToZero(Point[] arr) {
Point[] points = new Point[arr.length];
double x = arr[0].getX();
double y = arr[0].getY();
for(int i = 0;i < arr.length; ++i)
points[i] = new Point(arr[i].getX() - x, arr[i].getY() - y);
return points;
}
最后在来一个排序点的方法
// file Util.java
// class Util
/**
* 极角排序
* 将传入的Point按照极角大小排序
* 若极角相同在按照极径大小排序
* @param points 要排序的点的数组
*/
public static void polarAngleSort(Point[] points) {
Arrays.sort(points, new Comparator<Point>() {
@Override
public int compare(Point o1, Point o2) {
double t1 = Math.atan2(o1.getY(), o1.getX());
double t2 = Math.atan2(o2.getY(), o2.getX());
if(Compare.isEqual(t1, t2)) {
if (Compare.isNotEqual(o1.getY(), o2.getY()))
return Double.compare(o1.getY(), o2.getY());
return Double.compare(o1.getX(), o2.getX());
}
return Double.compare(t1, t2);
}
});
}
有了上面的方法,就可以求相交多边形的面积了
// file Polygon.java
// class Polygon
/**
* 获得一个多边形和另一个多边形相交的多边形
* 如果多边形互相包含 返回面积小的多边形
* 如果多边形没有交集, 返回 null
* 否则返回相交的多边形
* @param polygon 需要相交的多边形
* @return 返回相交后的多边形
*/
public Polygon intersectionPolygon(Polygon polygon) {
if(polygon.isSeparate(this))
return null;
if(polygon.isContains(this) || isContains(polygon)) {
return Compare.isLess(polygon.getArea(), getArea()) ?
polygon : this;
}
ArrayList<Point> points = new ArrayList<>();
// 加入在另一个多边形内的点
points.addAll(pointsInPolygon(polygon));
// 加入相交的点
points.addAll(intersectionPoint(polygon));
Point[] arr = points.toArray(new Point[0]);
// 去重后平移并排序
arr = Utils.removeRepeatPoint(arr);
arr = translateOnePointToZero(arr);
Utils.polarAngleSort(arr);
return Utils.buildPolygon(arr);
}
要注意, 相交的多边形可能是六边形, 所以buildPolygon中要增加六边形的情况
6. 其他
这最后一个板块好像只剩下判断四边形的形状了。这个不会很难。直接看代码
// file Quadrilateral.java
// class Quadrilateral
/**
* 判断四边形是否是平行四边形
* 对边平行就是平行四边形
* @return 返回是否是平行四边形的 boolean 值
*/
public boolean isParallelismQuadrilateral() {
return this.sides[0].isParallelLine(this.sides[2])
&& this.sides[1].isParallelLine(this.sides[3]);
}
/**
* 判断四边形是否是菱形
* 是平行四边形且有一个临边长度相同就是菱形
* @return 返回是否是菱形的 boolean 值
*/
public boolean isLozenge() {
double[] distance = getDistance();
return isParallelismQuadrilateral()
&& Compare.isEqual(distance[0], distance[1]);
}
/**
* 判断四边形是否是长方形
* 不是菱形 且 相邻三边满足勾股定理
* @return 返回是否是矩形的 boolean 值
*/
public boolean isRectangle() {
if (!isLozenge()) return false;
double hypotenuse = powerOfDistance(0, 2);
double side1 = powerOfDistance(0, 1);
double side2 = powerOfDistance(1, 2);
return Compare.isEqual(
side2 + side1, hypotenuse);
}
/**
* 判断四边形是否是正方形
* 是长方形且有一个邻边长度相等
* @return 返回是否是正方形的 boolean 值
*/
public boolean isSquare() {
double[] distance = getDistance();
return isRectangle()
&& Compare.isEqual(distance[0], distance[3]);
}
总结
这一次的大作业虽然只有两题, 但是总题上的难度还是挺大的。不仅仅是难在算法方面, 如果前面的结构没有写好的话, 这两题写起来会十分的吃力。也多亏我在前几次的作业中有留意把框架搭好, 再加上老师在这两题的样例中放了水, 所以我没有考虑太多结构上的问题, 写的比较顺畅, 但还是卡在了算法的实现上。
总体上来讲, 这次大作业只要用好了继承和多态, 就不会很复杂。因为所有的题目写下来, 只要有一个父类几乎就解决了所有的问题。如果再向下出六边形, 七边形的题目斌且还是相同的要求的话, 几乎就不需要改动代码了。其实从出题的意图也可以看出来, 题目里面不同多边形互相组合的要求几乎就是在逼着我们使用继承和多态, 并且找到一种万全的算法。要不然只会越来越复杂, 最后越来越越混乱, 直到自己都没有办法控制自己写的代码。
这次的题目也是踩了不少坑。搞了最久的就是判断多边形是不是互相连接。因为一开始脑子一热, 没想清楚SAT不能区分连接和相交, 就把SAT实现了。到最后相连的点一直过不了。改了好久。最后冷静下来发现如果从一开始就用之前写的判断点是否在多边形内, 线相交的点是否在多边形上这些方法的话应该会更简单一点(这也可能就是老师出前面的题的目的啊!!但是写都写完了, 就没有改了)。不至于因为采用的特殊方法导致一两种特殊情况一直过不去。
最后看一下SourceMonitor的分析

之前复杂度最高的方法是三角形了类里面的splitByLine高达13。这一次统一了所有多边形的splitByLine函数。复杂度降下去了, 但是还超过了2-8为9。。总体上的问题应该是有的方法其实可以提炼出几个类, 从上面的类图就可以看出来, Polygon类有点太大了。但是不想写了。。还有为什么注释会算多啊。。

浙公网安备 33010602011771号