計算機幾何算法概覽

一、引言

  計算機的出現使得很多原本十分繁瑣的工作得以大幅度簡化,但是也有一些在人們直觀看來很容易的問題卻需要拿出一套並不簡單的通用解決方案,比如幾何問題。作爲計算機科學的一個分支,計算幾何主要研究解決幾何問題的算法。在現代工程和數學領域,計算幾何在圖形學、機器人技術、超大規模集成電路設計和統計等諸多領域有着十分重要的應用。在本文中,我們將對計算幾何常用的基本算法做一個全面的介紹,希望對您瞭解並應用計算幾何的知識解決問題起到幫助。

二、目錄

本文整理的計算幾何基本概念和常用算法包括如下內容:

  • 矢量的概念
  • 矢量加減法
  • 矢量叉積
  • 折線段的拐向判斷
  • 判斷點是否在線段上
  • 判斷兩線段是否相交
  • 判斷線段和直線是否相交
  • 判斷矩形是否包含點
  • 判斷線段、折線、多邊形是否在矩形中
  • 判斷矩形是否在矩形中
  • 判斷圓是否在矩形中
  • 判斷點是否在多邊形中
  • 判斷線段是否在多邊形內
  • 判斷折線是否在多邊形內
  • 判斷多邊形是否在多邊形內
  • 判斷矩形是否在多邊形內
  • 判斷圓是否在多邊形內
  • 判斷點是否在圓內
  • 判斷線段、折線、矩形、多邊形是否在圓內
  • 判斷圓是否在圓內
  • 計算點到線段的最近點
  • 計算點到折線、矩形、多邊形的最近點
  • 計算點到圓的最近距離及交點座標
  • 計算兩條共線的線段的交點
  • 計算線段或直線與線段的交點
  • 求線段或直線與折線、矩形、多邊形的交點
  • 求線段或直線與圓的交點
  • 凸包的概念
  • 凸包的求法

三、算法介紹

 矢量的概念:

 如果一條線段的端點是有次序之分的,我們把這種線段成爲有向線段(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的順時針方向。
  若 P × Q < 0 , 則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 ,則矢量 ( P1 - Q1 ) 和( P2 - Q1 )位於矢量( Q2 - Q1 ) 的兩側,即( P1 - Q1 ) × ( Q2 - Q1 ) * ( P2 - Q1 ) × ( Q2 - Q1 ) < 0。上式可改寫成( P1 - Q1 ) × ( Q2 - Q1 ) * ( Q2 - Q1 ) × ( P2 - Q1 ) > 0。當 ( P1 - Q1 ) × ( Q2 - Q1 ) = 0 時,說明 ( P1 - Q1 ) 和 ( Q2 - Q1 )共線,但是因爲已經通過快速排斥試驗,所以 P1 一定在線段 Q1Q2上;同理,( Q2 - Q1 ) ×(P2 - Q1 ) = 0 說明 P2 一定在線段 Q1Q2上。所以判斷P1P2跨立Q1Q2的依據是:( P1 - Q1 ) × ( Q2 - Q1 ) * ( Q2 - Q1 ) × ( P2 - Q1 ) >= 0。同理判斷Q1Q2跨立P1P2的依據是:( Q1 - P1 ) × ( P2 - P1 ) * ( P2 - P1 ) × ( Q2 - P1 ) >= 0。具體情況如下圖所示:

                                          

 在相同的原理下,對此算法的具體的實現細節可能會與此有所不同,除了這種過程外,大家也可以參考《算法導論》上的實現。

 判斷線段和直線是否相交:

 有了上面的基礎,這個算法就很容易了。如果線段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屬於多邊行。由此得出算法的僞代碼如下:    

	count ← 0;
    以P爲端點,作從右向左的射線L; 
    for 多邊形的每條邊s
     do if P在邊s上 
          then return true;
        if s不是水平的
          then if s的一個端點在L上
                 if 該端點是s兩端點中縱座標較大的端點
                   then count ← count+1
               else if s和L相交
                 then count ← count+1;
    if count mod 2 = 1 
      then return true;
    else return false;

  其中做射線L的方法是:設P'的縱座標和P相同,橫座標爲正無窮大(很大的一個正數),則P和P'就確定了射線L。

 判斷點是否在多邊形中的這個算法的時間複雜度爲O(n)。

 另外還有一種算法是用帶符號的三角形面積之和與多邊形面積進行比較,這種算法由於使用浮點數運算所以會帶來一定誤差,不推薦大家使用。

 判斷線段是否在多邊形內:

 線段在多邊形內的一個必要條件是線段的兩個端點都在多邊形內,但由於多邊形可能爲凹,所以這不能成爲判斷的充分條件。如果線段和多邊形的某條邊內交(兩線段內交是指兩線段相交且交點不在兩線段的端點),因爲多邊形的邊的左右兩側分屬多邊形內外不同部分,所以線段一定會有一部分在多邊形外(見圖a)。於是我們得到線段在多邊形內的第二個必要條件:線段和多邊形的所有邊都不內交。

 線段和多邊形交於線段的兩端點並不會影響線段是否在多邊形內;但是如果多邊形的某個頂點和線段相交,還必須判斷兩相鄰交點之間的線段是否包含於多邊形內部(反例見圖b)。

                                                       

 因此我們可以先求出所有和線段相交的多邊形的頂點,然後按照X-Y座標排序(X座標小的排在前面,對於X座標相同的點,Y座標小的排在前面,這種排序準則也是爲了保證水平和垂直情況的判斷正確),這樣相鄰的兩個點就是在線段上相鄰的兩交點,如果任意相鄰兩點的中點也在多邊形內,則該線段一定在多邊形內。

 證明如下:
  命題1:
    如果線段和多邊形的兩相鄰交點P1 ,P2的中點P' 也在多邊形內,則P1, P2之間的所有點都在多邊形內。
     證明:
    假設P1,P2之間含有不在多邊形內的點,不妨設該點爲Q,在P1, P'之間,因爲多邊形是閉合曲線,所以其內外部之間有界,而P1屬於多邊行內部,Q屬於多邊性外部,P'屬於多邊性內部,P1-Q-P'完全連續,所以P1Q和QP'一定跨越多邊形的邊界,因此在P1,P'之間至少還有兩個該線段和多邊形的交點,這和P1P2是相鄰兩交點矛盾,故命題成立。證畢。

 由命題1直接可得出推論:
  推論2:
    設多邊形和線段PQ的交點依次爲P1,P2,……Pn,其中Pi和Pi+1是相鄰兩交點,線段PQ在多邊形內的充要條件是:P,Q在多邊形內且對於i =1, 2,……, n-1,Pi ,Pi+1的中點也在多邊形內。

  在實際編程中,沒有必要計算所有的交點,首先應判斷線段和多邊形的邊是否內交,倘若線段和多邊形的某條邊內交則線段一定在多邊形外;如果線段和多邊形的每一條邊都不內交,則線段和多邊形的交點一定是線段的端點或者多邊形的頂點,只要判斷點是否在線段上就可以了。
  至此我們得出算法如下:    

if 線端PQ的端點不都在多邊形內 
      then return false;
    點集pointSet初始化爲空;
    for 多邊形的每條邊s
      do if 線段的某個端點在s上
           then 將該端點加入pointSet;
         else if s的某個端點在線段PQ上
           then 將該端點加入pointSet;
         else if s和線段PQ相交 // 這時候已經可以肯定是內交了
           then return false;
    將pointSet中的點按照X-Y座標排序;
    for pointSet中每兩個相鄰點 pointSet[i] , pointSet[ i+1]
      do if pointSet[i] , pointSet[ i+1] 的中點不在多邊形中
           then return false;
    return true;

  這個過程中的排序因爲交點數目肯定遠小於多邊形的頂點數目n,所以最多是常數級的複雜度,幾乎可以忽略不計。因此算法的時間複雜度也是O(n)。

 判斷折線是否在多邊形內:

 只要判斷折線的每條線段是否都在多邊形內即可。設折線有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的交點。
 1. 首先判斷L0和L1是否相交(方法已在前文討論過),如果不相交則沒有交點,否則說明L0和L1一定有交點,下面就將L0和L1都看作直線來考慮。

 2. 如果P1和P2橫座標相同,即L0平行於Y軸

 a) 若L1也平行於Y軸,

   i. 若P1的縱座標和Q1的縱座標相同,說明L0和L1共線,假如L1是直線的話他們有無窮的交點,假如L1是線段的話可用"計算兩條共線線段的交點"的算法求他們的交點(該方法在前文已討論過);
    ii. 否則說明L0和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是線段的話可用"計算兩條共線線段的交點"的算法求他們的交點(該方法在前文已討論過);
    ii. 否則說明L0和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是線段的話可用"計算兩條共線線段的交點"的算法求他們的交點(該方法在前文已討論過);
    ii. 如果Q1不在L0上,則說明L0和L1平行,他們沒有交點。
  c) 聯立兩直線的方程組可以解出交點來
  這個算法並不複雜,但是要分情況討論清楚,尤其是當兩條線段共線的情況需要單獨考慮,所以在前文將求兩條共線線段的算法單獨寫出來。另外,一開始就先利用矢量叉乘判斷線段與線段(或直線)是否相交,如果結果是相交,那麼在後面就可以將線段全部看作直線來考慮。需要注意的是,我們可以將直線或線段方程改寫爲ax+by+c=0的形式,這樣一來上述過程的部分步驟可以合併,縮短了代碼長度,但是由於先要求出參數,這種算法將花費更多的時間。

 求線段或直線與折線、矩形、多邊形的交點:

 分別求與每條邊的交點即可。

 求線段或直線與圓的交點:

 設圓心爲O,圓半徑爲r,直線(或線段)L上的兩點爲P1,P2。

 1. 如果L是線段且P1,P2都包含在圓O內,則沒有交點;否則進行下一步。

 2. 如果L平行於Y軸,

  a) 計算圓心到L的距離dis;
   b) 如果dis > r 則L和圓沒有交點;
   c) 利用勾股定理,可以求出兩交點座標,但要注意考慮L和圓的相切情況。

  3. 如果L平行於X軸,做法與L平行於Y軸的情況類似;

 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座標排序下最小的點 
  設<p1,p2,...pm>爲對其餘點按以p0爲中心的極角逆時針排序所得的點集(如果有多個點有相同的極角,除了距p0最遠的點外全部移除
  壓p0進棧S
  壓p1進棧S
  壓p2進棧S
    for i ← 3 to m
      do while 由S的棧頂元素的下一個元素、S的棧頂元素以及pi構成的折線段不拐向左側
        對S彈棧
      壓pi進棧S
    return S;

 此過程執行後,棧S由底至頂的元素就是Q的凸包頂點按逆時針排列的點序列。需要注意的是,我們對點按極角逆時針排序時,並不需要真正求出極角,只需要求出任意兩點的次序就可以了。而這個步驟可以用前述的矢量叉積性質實現。

四、結語

 儘管人類對幾何學的研究從古代起便沒有中斷過,但是具體到藉助計算機來解決幾何問題的研究,還只是停留在一個初級階段,無論從應用領域還是發展前景來看,計算幾何學都值得我們認真學習、加以運用,希望這篇文章能帶你走進這個豐富多彩的世界。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章