<!-- /* Font Definitions */ @font-face {font-family:宋体; panose-1:2 1 6 0 3 1 1 1 1 1; mso-font-alt:SimSun; mso-font-charset:134; mso-generic-font-family:auto; mso-font-pitch:variable; mso-font-signature:3 135135232 16 0 262145 0;} @font-face {font-family:ˎ̥; panose-1:0 0 0 0 0 0 0 0 0 0; mso-font-alt:"Times New Roman"; mso-font-charset:0; mso-generic-font-family:roman; mso-font-format:other; mso-font-pitch:auto; mso-font-signature:0 0 0 0 0 0;} @font-face {font-family:Tahoma; panose-1:2 11 6 4 3 5 4 4 2 4; mso-font-charset:0; mso-generic-font-family:swiss; mso-font-pitch:variable; mso-font-signature:553679495 -2147483648 8 0 66047 0;} @font-face {font-family:"/@宋体"; panose-1:2 1 6 0 3 1 1 1 1 1; mso-font-charset:134; mso-generic-font-family:auto; mso-font-pitch:variable; mso-font-signature:3 135135232 16 0 262145 0;} /* Style Definitions */ p.MsoNormal, li.MsoNormal, div.MsoNormal {mso-style-parent:""; margin:0cm; margin-bottom:.0001pt; text-align:justify; text-justify:inter-ideograph; mso-pagination:none; font-size:10.5pt; mso-bidi-font-size:12.0pt; font-family:"Times New Roman"; mso-fareast-font-family:宋体; mso-font-kerning:1.0pt;} /* Page Definitions */ @page {mso-page-border-surround-header:no; mso-page-border-surround-footer:no;} @page Section1 {size:595.3pt 841.9pt; margin:72.0pt 90.0pt 72.0pt 90.0pt; mso-header-margin:42.55pt; mso-footer-margin:49.6pt; mso-paper-source:0; layout-grid:15.6pt;} div.Section1 {page:Section1;} -->
|
| ||||
计算几何常用算法概览
一、引言 计算机的出现使得很多原本十分繁琐的工作得以大幅度简化,但是也有一些在人们直观看来很容易的问题却需要拿出一套并不简单的通用解决方案,比如几何问题。作为计算机科学的一个分支,计算几何主要研究解决几何问题的算法。在现代工程和数学领域,计算几何在图形学、机器人技术、超大规模集成电路设计和统计等诸多领域有着十分重要的应用。在本文中,我们将对计算几何常用的基本算法做一个全面的介绍,希望对您了解并应用计算几何的知识解决问题起到帮助。 二、目录 本文整理的计算几何基本概念和常用算法包括如下内容: 三、算法介绍 如果一条线段的端点是有次序之分的,我们把这种线段成为有向线段 (directed segment) 。如果有向线段 p1p2 的起点 p1 在坐标原点,我们可以把它称为矢量 (vector)p2 。 设二维矢量 P = ( x1,y1 ) , Q = ( x2 , y2 ) ,则矢量加法定义为: P + Q = ( x1 + x2 , y1 + y2 ) ,同样的,矢量减法定义为: P - Q = ( x1 - x2 , y1 - y2 ) 。显然有性质 P + Q = Q + P , P - Q = - ( Q - P ) 。 计算矢量叉积是与直线和线段相关算法的核心部分。设矢量 P = ( x1,y1 ) , Q = (x2,y2) ,则矢量叉积定义为由 (0,0) 、 p1 、 p2 和 p1+p2 所组成的平行四边形的带符号的面积,即: P × Q = x1*y2 - x2*y1 ,其结果是一个标量。显然有性质 P × Q = - ( Q × P ) 和 P × ( - Q ) = - ( P × Q ) 。一般在不加说明的情况下,本文下述算法中所有的点都看作矢量,两点的加减法就是矢量相加减,而点的乘法则看作矢量叉积。 叉积的一个非常重要性质是可以通过它的符号判断两矢量相互之间的顺逆时针关系: 若 P × Q > 0 , 则 P 在 Q 的顺时针方向。 折线段的拐向判断方法可以直接由矢量叉积的性质推出。对于有公共端点的线段 p0p1 和 p1p2 ,通过计算 (p2 - p0) × (p1 - p0) 的符号便可以确定折线段的拐向: 若 (p2 - p0) × (p1 - p0) > 0, 则 p0p1 在 p1 点拐向右侧后得到 p1p2 。 若 (p2 - p0) × (p1 - p0) < 0, 则 p0p1 在 p1 点拐向左侧后得到 p1p2 。 若 (p2 - p0) × (p1 - p0) = 0, 则 p0 、 p1 、 p2 三点共线。 具体情况可参照下图:
设点为 Q ,线段为 P1P2 ,判断点 Q 在该线段上的依据是: ( Q - P1 ) × ( P2 - P1 ) = 0 且 Q 在以 P1 , P2 为对角顶点的矩形内。前者保证 Q 点在直线 P1P2 上,后者是保证 Q 点不在线段 P1P2 的延长线或反向延长线上,对于这一步骤的判断可以用以下过程实现: ON-SEGMENT(pi,pj,pk) if min(xi,xj)<=xk<=max(xi,xj) and min(yi,yj)<=yk<=max(yi,yj) then return true; else return false; 特别要注意的是,由于需要考虑水平线段和垂直线段两种特殊情况, min(xi,xj)<=xk<=max(xi,xj) 和 min(yi,yj)<=yk<=max(yi,yj) 两个条件必须同时满足才能返回真值。 我们分两步确定两条线段是否相交: (1) 快速排斥试验 设以线段 P1P2 为对角线的矩形为 R , 设以线段 Q1Q2 为对角线的矩形为 T ,如果 R 和 T 不相交,显然两线段不会相交。 (2) 跨立试验
在相同的原理下,对此算法的具体的实现细节可能会与此有所不同,除了这种过程外,大家也可以参考《算法导论》上的实现。 有了上面的基础,这个算法就很容易了。如果线段 P1P2 和直线 Q1Q2 相交,则 P1P2 跨立 Q1Q2 ,即: ( P1 - Q1 ) × ( Q2 - Q1 ) * ( Q2 - Q1 ) × ( P2 - Q1 ) >= 0 。 只要判断该点的横坐标和纵坐标是否夹在矩形的左右边和上下边之间。 因为矩形是个凸集,所以只要判断所有端点是否都在矩形中就可以了。 只要比较左右边界和上下边界就可以了。 很容易证明,圆在矩形中的充要条件是:圆心在矩形中且圆的半径小于等于圆心到矩形四边的距离的最小值。 判断点 P 是否在多边形中是计算几何中一个非常基本但是十分重要的算法。以点 P 为端点,向左方作射线 L ,由于多边形是有界的,所以射线 L 的左端一定在多边形外,考虑沿着 L 从无穷远处开始自左向右移动,遇到和多边形的第一个交点的时候,进入到了多边形的内部,遇到第二个交点的时候,离开了多边形, …… 所以很容易看出当 L 和多边形的交点数目 C 是奇数的时候, P 在多边形内,是偶数的话 P 在多边形外。 但是有些特殊情况要加以考虑。如图下图 (a)(b)(c)(d) 所示。在图 (a) 中, L 和多边形的顶点相交,这时候交点只能计算一个;在图 (b) 中, L 和多边形顶点的交点不应被计算;在图 (c) 和 (d) 中, L 和多边形的一条边重合,这条边应该被忽略不计。如果 L 和多边形的一条边重合,这条边应该被忽略不计。
为了统一起见,我们在计算射线 L 和多边形的交点的时候, 1 。对于多边形的水平边不作考虑; 2 。对于多边形的顶点和 L 相交的情况,如果该顶点是其所属的边上纵坐标较大的顶点,则计数,否则忽略; 3 。对于 P 在多边形边上的情形,直接可判断 P 属于多边行。由此得出算法的伪代码如下: 判断点是否在多边形中的这个算法的时间复杂度为 O(n) 。 另外还有一种算法是用带符号的三角形面积之和与多边形面积进行比较,这种算法由于使用浮点数运算所以会带来一定误差,不推荐大家使用。 线段在多边形内的一个必要条件是线段的两个端点都在多边形内,但由于多边形可能为凹,所以这不能成为判断的充分条件。如果线段和多边形的某条边内交(两线段内交是指两线段相交且交点不在两线段的端点),因为多边形的边的左右两侧分属多边形内外不同部分,所以线段一定会有一部分在多边形外 ( 见图 a) 。于是我们得到线段在多边形内的第二个必要条件:线段和多边形的所有边都不内交。 线段和多边形交于线段的两端点并不会影响线段是否在多边形内;但是如果多边形的某个顶点和线段相交,还必须判断两相邻交点之间的线段是否包含于多边形内部(反例见图 b) 。
因此我们可以先求出所有和线段相交的多边形的顶点,然后按照 X-Y 坐标排序 (X 坐标小的排在前面,对于 X 坐标相同的点, Y 坐标小的排在前面,这种排序准则也是为了保证水平和垂直情况的判断正确 ) ,这样相邻的两个点就是在线段上相邻的两交点,如果任意相邻两点的中点也在多边形内,则该线段一定在多边形内。 证明如下: 命题 1 : 证明: 由命题 1 直接可得出推论: 只要判断折线的每条线段是否都在多边形内即可。设折线有 m 条线段,多边形有 n 个顶点,则该算法的时间复杂度为 O(m*n) 。 只要判断多边形的每条边是否都在多边形内即可。判断一个有 m 个顶点的多边形是否在一个有 n 个顶点的多边形内复杂度为 O(m*n) 。 将矩形转化为多边形,然后再判断是否在多边形内。 只要计算圆心到多边形的每条边的最短距离,如果该距离大于等于圆半径则该圆在多边形内。计算圆心到多边形每条边最短距离的算法在后文阐述。 计算圆心到该点的距离,如果小于等于半径则该点在圆内。 因为圆是凸集,所以只要判断是否每个顶点都在圆内即可。 设两圆为 O1,O2 ,半径分别为 r1, r2 ,要判断 O2 是否在 O1 内。先比较 r1 , r2 的大小,如果 r1<r2 则 O2 不可能在 O1 内;否则如果两圆心的距离大于 r1 - r2 ,则 O2 不在 O1 内;否则 O2 在 O1 内。 如果该线段平行于 X 轴( Y 轴),则过点 point 作该线段所在直线的垂线,垂足很容易求得,然后计算出垂足,如果垂足在线段上则返回垂足,否则返回离垂足近的端点;如果该线段不平行于 X 轴也不平行于 Y 轴,则斜率存在且不为 0 。设线段的两端点为 pt1 和 pt2 ,斜率为: k = ( pt2.y - pt1. y ) / (pt2.x - pt1.x ); 该直线方程为: y = k* ( x - pt1.x) + pt1.y 。其垂线的斜率为 - 1 / k ,垂线方程为: y = (-1/k) * (x - point.x) + point.y 。 联立两直线方程解得: x = ( k^2 * pt1.x + k * (point.y - pt1.y ) + point.x ) / ( k^2 + 1) , y = k * ( x - pt1.x) + pt1.y; 然后再判断垂足是否在线段上,如果在线段上则返回垂足;如果不在则计算两端点到垂足的距离,选择距离垂足较近的端点返回。 只要分别计算点到每条线段的最近点,记录最近距离,取其中最近距离最小的点即可。 如果该点在圆心,因为圆心到圆周任一点的距离相等,返回 UNDEFINED 。 连接点 P 和圆心 O ,如果 PO 平行于 X 轴,则根据 P 在 O 的左边还是右边计算出最近点的横坐标为 centerPoint.x - radius 或 centerPoint.x + radius 。如果 PO 平行于 Y 轴,则根据 P 在 O 的上边还是下边计算出最近点的纵坐标为 centerPoint.y -+radius 或 centerPoint.y - radius 。如果 PO 不平行于 X 轴和 Y 轴,则 PO 的斜率存在且不为 0 ,这时直线 PO 斜率为 k = ( P.y - O.y ) / ( P.x - O.x ) 。直线 PO 的方程为: y = k * ( x - P.x) + P.y 。设圆方程为 :(x - O.x ) ^2 + ( y - O.y ) ^2 = r ^2 ,联立两方程组可以解出直线 PO 和圆的交点,取其中离 P 点较近的交点即可。 对于两条共线的线段,它们之间的位置关系有下图所示的几种情况。图 (a) 中两条线段没有交点;图 (b) 和 (d) 中两条线段有无穷焦点;图 (c) 中两条线段有一个交点。设 line1 是两条线段中较长的一条, line2 是较短的一条,如果 line1 包含了 line2 的两个端点,则是图 (d) 的情况,两线段有无穷交点;如果 line1 只包含 line2 的一个端点,那么如果 line1 的某个端点等于被 line1 包含的 line2 的那个端点,则是图 (c) 的情况,这时两线段只有一个交点,否则就是图 (b) 的情况,两线段也是有无穷的交点;如果 line1 不包含 line2 的任何端点,则是图 (a) 的情况,这时两线段没有交点。
设一条线段为 L0 = P1P2 ,另一条线段或直线为 L1 = Q1Q2 ,要计算的就是 L0 和 L1 的交点。 2 . 如果 P1 和 P2 横坐标相同,即 L0 平行于 Y 轴 a) 若 L1 也平行于 Y 轴, i. 若 P1 的纵坐标和 Q1 的纵坐标相同,说明 L0 和 L1 共线,假如 L1 是直线的话他们有无穷的交点,假如 L1 是线段的话可用 " 计算两条共线线段的交点 " 的算法求他们的交点(该方法在前文已讨论过); b) 若 L1 不平行于 Y 轴,则交点横坐标为 P1 的横坐标,代入到 L1 的直线方程中可以计算出交点纵坐标; 3 . 如果 P1 和 P2 横坐标不同,但是 Q1 和 Q2 横坐标相同,即 L1 平行于 Y 轴,则交点横坐标为 Q1 的横坐标,代入到 L0 的直线方程中可以计算出交点纵坐标; 4 . 如果 P1 和 P2 纵坐标相同,即 L0 平行于 X 轴 a) 若 L1 也平行于 X 轴, i. 若 P1 的横坐标和 Q1 的横坐标相同,说明 L0 和 L1 共线,假如 L1 是直线的话他们有无穷的交点,假如 L1 是线段的话可用 " 计算两条共线线段的交点 " 的算法求他们的交点(该方法在前文已讨论过); b) 若 L1 不平行于 X 轴,则交点纵坐标为 P1 的纵坐标,代入到 L1 的直线方程中可以计算出交点横坐标; 5 . 如果 P1 和 P2 纵坐标不同,但是 Q1 和 Q2 纵坐标相同,即 L1 平行于 X 轴,则交点纵坐标为 Q1 的纵坐标,代入到 L0 的直线方程中可以计算出交点横坐标; 6 . 剩下的情况就是 L1 和 L0 的斜率均存在且不为 0 的情况 a) 计算出 L0 的斜率 K0 , L1 的斜率 K1 ; b) 如果 K1 = K2 i. 如果 Q1 在 L0 上,则说明 L0 和 L1 共线,假如 L1 是直线的话有无穷交点,假如 L1 是线段的话可用 " 计算两条共线线段的交点 " 的算法求他们的交点(该方法在前文已讨论过); 分别求与每条边的交点即可。 设圆心为 O ,圆半径为 r ,直线(或线段) L 上的两点为 P1,P2 。 1. 如果 L 是线段且 P1 , P2 都包含在圆 O 内,则没有交点;否则进行下一步。 2. 如果 L 平行于 Y 轴, a) 计算圆心到 L 的距离 dis ; 4. 如果 L 既不平行 X 轴也不平行 Y 轴,可以求出 L 的斜率 K ,然后列出 L 的点斜式方程,和圆方程联立即可求解出 L 和圆的两个交点; 5. 如果 L 是线段,对于 2 , 3 , 4 中求出的交点还要分别判断是否属于该线段的范围内。 点集 Q 的凸包 (convex hull) 是指一个最小凸多边形,满足 Q 中的点或者在多边形边上或者在其内。下图中由红色线段表示的多边形就是点集 Q={p0,p1,...p12} 的凸包。
现在已经证明了凸包算法的时间复杂度下界是 O(n*logn), 但是当凸包的顶点数 h 也被考虑进去的话, Krikpatrick 和 Seidel 的剪枝搜索算法可以达到 O(n*logh) ,在渐进意义下达到最优。最常用的凸包算法是 Graham 扫描法和 Jarvis 步进法。本文只简单介绍一下 Graham 扫描法,其正确性的证明和 Jarvis 步进法的过程大家可以参考《算法导论》。 对于一个有三个或以上点的点集 Q , Graham 扫描法的过程如下: 令 p0 为 Q 中 Y-X 坐标排序下最小的点 此过程执行后,栈 S 由底至顶的元素就是 Q 的凸包顶点按逆时针排列的点序列。需要注意的是,我们对点按极角逆时针排序时,并不需要真正求出极角,只需要求出任意两点的次序就可以了。而这个步骤可以用前述的矢量叉积性质实现。 四、结语 尽管人类对几何学的研究从古代起便没有中断过,但是具体到借助计算机来解决几何问题的研究,还只是停留在一个初级阶段,无论从应用领域还是发展前景来看,计算几何学都值得我们认真学习、加以运用,希望这篇文章能带你走进这个丰富多彩的世界。 |