1. 基本概念
简单多边形的 耳朵(ear) :是指由连续顶点v 0 v_0 v 0 ,v 1 v_1 v 1 和 v 2 v_2 v 2 组成的内部不包含其他任意顶点的三角形。
在计算机几何术语中,v 0 v_0 v 0 与 v 2 v_2 v 2 之间的连线称之为多边形的对角线 ,点 v 1 v_1 v 1 称之为耳尖 。
虽然你可以将耳尖放到三角形的任意一个顶点上,但是我们认为三角形包含一个耳尖。
一个由四个顶点(或者更多)组成的多边形至少有两个不重叠的耳尖。
这个特性提供了一个通过递归来解决三角化分割的方法。
针对由 n 个顶点组成的多边形,找到其耳尖,移除唯一耳尖上的顶点,此时剩余顶点组成了一个 n-1 个顶点的简单多边形。
我们重复这个操作直到剩余三个顶点。这样的话会产生一个复杂度为 O ( n 3 ) O(n^3) O ( n 3 ) 的算法
随着一些细节改进,耳朵消除可以在 O ( n 2 ) O(n^2) O ( n 2 ) 的时间来完成。
2. 算法思路
1. 先将多边形使用双向链表存储,这样可以快速的移除耳朵,列表的构建复杂度是 O ( n ) O(n) O ( n ) ;
2. 遍历顶点寻找耳朵,对于每一个顶点 V i V_i V i 和围绕该顶点的三角形< v i − 1 , v i , v i + 1 > <v_{i-1}, v_i, v_{i+1}> < v i − 1 , v i , v i + 1 > ,(总长度为 N N N ,所以 v n = v 0 v_n = v_0 v n = v 0 ,并且 v − 1 = v n − 1 v - 1 = v_n - 1 v − 1 = v n − 1 ),测试其他顶点是否在当前三角形中,如果有一个顶点在三角形里面,则不是耳朵,只有都不在的情况,才算是找到一个耳朵。
具体实现的时候我们可以考虑以下因素让这个算法更为高效。
当发现有一个点在三角形里面的时候便可以开始放弃当前测试。
一个凹拐角其两边的夹角大于180°,而一个凸拐角两边夹角小于180°。
存储多边形的数据结构使用四个链表,具体使用数组而不是标准的动态需要分配合释放存储器的链表。
多边形的顶点存储在在一个循环链表 vertices
中,凹顶点和凸顶点存储在线性表中,耳尖存储在一个循环列表 ears
中。
3. 一旦凸顶点和耳朵的链表构建成功,每次遍历都会移除一个耳朵。
假设当前 v i v_i v i 是个耳朵并且被移除掉,那么边结构的相邻点 v i − 1 , v i + 1 v_{i-1}, v_{i+1} v i − 1 , v i + 1 则会发生变化:
如果相邻点是 凸顶点 ,那么依旧保持凸点;
如果相邻点是 耳朵 ,那么当 v i v_i v i 被移除后则不一定能保持耳朵的状态;
如果相邻点是 凹顶点 ,那么他则有可能变为一个凸点甚至是耳朵。
因此当移除顶点 v i v_i v i 后,如果相邻点是凸点,则必须遍历相关顶点,通过遍历查看是否包含其他点,来测试它是否是一个耳朵。我们有 n n n 个耳朵,每一次更新都会触发一个耳朵检测,每次过程中更新 O ( n ) O(n) O ( n ) ,所以移除进程的复杂度是 O ( n 2 ) O(n^2) O ( n 2 ) 。
3. 示例代码
3.1 耳切法算法
计算给定多边形的梵高(耳切)三角剖分。
这个实现在 O ( n 2 ) O(n^2) O ( n 2 ) 中运行,因为 ear
同时存储在一个单独的链表 ears
中,该列表每个步骤更新一次。
1 2 3 4 5 6 7 8 9 10 11 12 public static ArrayList<Triangle> fast (Polygon p) { ArrayList<Triangle> result = new ArrayList<Triangle>(); VanGoghPolygon ap = new VanGoghPolygon(p); for (int i = 0 ; i < p.size() - 2 ; i++) { result.add(ap.removeEar()); } return result; }
3.2 梵高多边形类 VanGoghPolygon.class
3.2.1 import
1 2 3 import ru.dubov.primitives.Point;import ru.dubov.primitives.Polygon;import ru.dubov.primitives.Triangle;
3.2.2 成员
1 2 3 4 5 6 7 8 9 10 private DoublyLinkedCyclicList<Point> vertices;private DoublyLinkedCyclicList<DoublyLinkedCyclicList<Point>.Node> ears;private boolean isClockwise;
3.2.3 构造方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public VanGoghPolygon () { vertices = new DoublyLinkedCyclicList<Point>(); ears = new DoublyLinkedCyclicList<DoublyLinkedCyclicList<Point>.Node>(); } public VanGoghPolygon (Polygon p) { this (); for (int i = p.size()-1 ; i >= 0 ; i--) { vertices.insert(p.get(i)); } constructEarList(); }
3.2.4 移除耳朵
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 public Triangle removeEar () { DoublyLinkedCyclicList<Point>.Node adjLeft = ears.head().value.prev; DoublyLinkedCyclicList<Point>.Node adjRight = ears.head().value.next; Triangle ear = new Triangle(adjLeft.value, ears.head().value.value, adjRight.value); ears.head().value.delete(); ears.head().delete(); if (isConvex(adjLeft, isClockwise)) { if (adjLeft == ears.head().prev.value) { if (! isEar(ears.head().prev.value, isClockwise)) { ears.head().prev.delete(); } } else if (isEar(adjLeft, isClockwise)) { ears.insert(adjLeft); ears.next(); } } if (isConvex(ears.head().value, isClockwise)) { if (adjRight == ears.head().value) { if (! isEar(ears.head().value, isClockwise)) { ears.head().delete(); } } else if (isEar(adjRight, isClockwise)) { ears.insert(adjRight); } } return ear; }
3.2.5 创建耳朵链表
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public final void constructEarList () { DoublyLinkedCyclicList<Point>.Node cur = vertices.head(); if (cur == null ) return ; isClockwise = isClockwise(); cur = vertices.head().prev; do { if (isEar(cur, isClockwise)) { ears.insert(cur); } cur = cur.prev; } while (cur != vertices.head().prev); }
3.2.6 判断凸点
1 2 3 4 private boolean isConvex (DoublyLinkedCyclicList<Point>.Node node, boolean isClockwise) { return Point.isLeftTurn(node.prev.value, node.value, node.next.value) ^ isClockwise; }
3.2.7 判断耳朵
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 private boolean isEar (DoublyLinkedCyclicList<Point>.Node node, boolean isClockwise) { if (! isConvex(node, isClockwise)) { return false ; } Triangle tr = new Triangle(node.prev.value, node.value, node.next.value); DoublyLinkedCyclicList<Point>.Node cur = vertices.head(); do { if (tr.pointInside(cur.value)) return false ; cur = cur.next; } while (cur != vertices.head()); return true ; }
3.2.8 判断顺时针
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public boolean isClockwise () { double sum = 0 ; DoublyLinkedCyclicList<Point>.Node cur = vertices.head(); do { sum += (cur.next.value.getX() - cur.value.getX())* (cur.next.value.getY() + cur.value.getY()); cur = cur.next; } while (cur != vertices.head()); return (sum > 0 ); }
3.3 双链接循环链表 DoublyLinkedCyclicList < T >
3.3.1 成员
3.3.2 构造方法
1 2 3 public DoublyLinkedCyclicList () { head = null ; }
3.3.3 操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 public void insert (T value) { Node node = new Node(value); if (head == null ) { head = node; head.next = head; head.prev = head; return ; } head.prev.next = node; node.prev = head.prev; node.next = head; head.prev = node; head = node; } public void next () { head = head.next; } public void prev () { head = head.prev; } public Node head () { return head; }
3.3.4 Node 类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 public class Node { public Node () { next = prev = null ; } public Node (T val) { next = prev = null ; value = val; } public void delete () { if (this .prev != null ) { this .prev.next = this .next; } if (this .next != null ) { this .next.prev = this .prev; } if (head == this ) { head = this .next; } } public Node next, prev; public T value; }
参考资料
参考文章: 耳切法处理多边形三角划分 (有示例)