一些几何学的东西

http://www.csie.ntnu.edu.tw/~u91029/PointLinePlane2.html

Sweep Line

程度★ 難度★★

Sweep Line

「掃描線」是計算幾何領域的基礎手法,可以用來解決許多計算幾何的問題,有如圖論中的 BFS 與 DFS 一樣經典。它並不是一個物品,而是一個概念。

平移的掃描線

一條(或兩條)無限長平行線,沿其垂直方向不斷移動,從畫面一端移動到另一端,只在頂點處停留。

實作時,通常是先按座標大小排序所有頂點,然後以兩索引值,紀錄平行線的位置在哪個頂點上面。兩條平行線,一條為主,穿越整個畫面;一條為輔,跟著主線的狀況進行平移。這是陰陽的道理。

UVa 920 972

旋轉的掃描線

一條(或兩條)從原點射出的射線,做 360° 旋轉,只在頂點處停留。

實作時,通常是先按極角大小排序所有頂點,然後以兩索引值,紀錄射線的位置在哪個頂點上面。兩條射線,一條為主,轉過整個畫面;一條為輔,跟著主線的狀況進行平移。這是陰陽的道理。

UVa 270 10691 10927 11529 11696 11704 ICPC 3259 4064

用 Sweep Line 設計演算法時的步驟

說穿了,掃描線的基本精神就是:先排序、再搜尋。

在二維平面上,有一個重要的特性就是「區域性」。比如說,兩點之間,會被距離更近的點隔開。

排序的目的,也就是建立「區域性」。有了「區域性」,搜尋的條件就更精確了,搜尋的速度也就更快了。

觀察問題是否有「不重疊」、「不相交」、「間隔」、「相聚」之類的性質。
然後選定平移的或者旋轉的掃描線,進行掃描。
換句話說,排序所有的頂點,進行搜尋。

Sweep Line 可以解決的問題

平移的掃描線
Closest Pair             找出所有最近點對
Segment Intersections    找出所有線段的所有交點
Polygon Intersection     找出兩個多邊形的交集、聯集、差集
Voronoi Diagram
Delaunay Triangulation

旋轉的掃描線
Convex Hull              找出凸包

旋轉的掃描線:極角排序( Sorting Points by Polar Angle )

 
  1. typedef complex<doublePoint;
  2. #define x real()
  3. #define y imag()
  4.  
  5. double cross(const Pointaconst Pointb)
  6. {
  7.     return a.x * b.y - a.y * b.x;
  8. }
  9.  
  10. // 用 complex 的內建函數,算出極角大小。
  11. bool cmp(const Pointp1const Pointp2)
  12. {
  13.     return arg(p1) < arg(p2);
  14. }
  15.  
  16. // 用 arctan 計算極角大小。
  17. // 注意到大小範圍是(-180°, +180°]。
  18. bool cmp(const Pointp1const Pointp2)
  19. {
  20.     return atan2(p1.yp1.x) < atan2(p2.yp2.x);
  21. }
  22.  
  23. // 先判斷象限,再用外積判斷順序。
  24. // 若極角相同,會再以長度排序。
  25. bool cmp(const Pointp1const Pointp2)
  26. {
  27.     if (p1.y == 0 && p2.y == 0 && p1.x * p2.x <= 0return p1.x > p2.x;
  28.     if (p1.y == 0 && p1.x >= 0 && p2.y != 0return true;
  29.     if (p2.y == 0 && p2.x >= 0 && p1.y != 0return false;
  30.     if (p1.y * p2.y < 0return p1.y > p2.y;
  31.     double c = cross(p1p2);
  32.     return c > 0 || c == 0 && fabs(p1.x) < fabs(p2.x);
  33. }
  34.  
  35. // 先判斷象限,再用外積判斷順序。
  36. // 適用於每個點的極角皆不同。
  37. bool cmp(const Pointp1const Pointp2)
  38. {
  39.     if (p1.y > 0 && p2.y > 0)
  40.         return p2.x * p1.y < p2.y * p1.x;
  41.     else if (p1.y < 0 && p2.y < 0)
  42.         return p2.x * p1.y < p2.y * p1.x;
  43.     else if (p1.y == 0)
  44.         if (p1.x > 0)
  45.             return true;
  46.         else
  47.             return p2.y < 0;
  48.     else if (p2.y == 0)
  49.         if (p2.x > 0)
  50.             return false;
  51.         else
  52.             return p1.y > 0;
  53.     else
  54.         return p1.y > 0;
  55. }
  56.  
  57. void sort_points_by_polar_angle()
  58. {
  59.     Point p[100];
  60.     sort(pp+Ncmp);
  61. }

Rotating Caliper

程度★ 難度★★

Rotating Caliper

「旋轉卡尺」也是計算幾何領域的基礎手法,它並不是一個物品,而是一個概念。參考資料: http://cgm.cs.mcgill.ca/~orm/rotcal.html ,中文翻譯: http://blog.csdn.net/ACMaker/archive/2008/10.aspx

平面上的圖形做 360° 旋轉,並以兩條垂直線,隨時從左右夾緊圖形,量測寬度、找到極端頂點。

切換視點,就變成:兩條無限長平行線,做 360° 旋轉,嘗試夾住平面上的圖形。

實作時,通常是以兩索引值,紀錄平行線的位置在哪個頂點上面。兩條平行線,一條為主,轉過整個畫面;一條為輔,跟著主線的狀況進行鬆緊。這是陰陽的道理。

實作時,只需轉 180° 即可。轉半圈,兩條平行線對調,效果同 360° 。

說穿了,旋轉卡尺的基本精神就是:窮舉斜率,判斷目標對象有多斜。

UVa 303 10173 10416 11243

Rotating Caliper 可以解決的問題

Farthest Pair    找出所有最遠點對
Convex Polygon   凸多邊形的各種問題,例如求直徑、合併、相交等等
Bounding Box

事實上旋轉卡尺與平移的掃描線互為點線對偶,所以平移的掃描線能解決的問題,旋轉卡尺也能解決,反之亦然。

旋轉卡尺:凸包( Convex Hull )

使用旋轉卡尺夾住圖形,卡尺夾不進的地方,剛好是該圖形的凸包。因此,旋轉卡尺適合用於凸包、凸多邊形的相關問題。

旋轉卡尺夾到的頂點順序,就是凸包的頂點順序。

 
  1. struct Point {dobule xy;} P[10];
  2. Point L[10+1], U[10];   // 下凸包、上凸包
  3.  
  4. bool cmp(const Pointaconst Pointb)
  5. {
  6.     return a.x < b.x || a.x == b.x && a.y < b.y;
  7. }
  8.  
  9. void rotating_cliper()
  10. {
  11.     /* 尋找凸包,Andrew's Monotone Chain。 */
  12.  
  13.     sort(PP + 10cmp);   // 先排X座標、再排Y座標
  14.  
  15.     int l = 0u = 0;       // 上凸包的點數、下凸包的點數
  16.     for (int i=0i<10; ++i)
  17.     {
  18.         while (l >= 2 && cross(L[l-2], L[l-1], P[i]) <= 0l--;
  19.         while (u >= 2 && cross(U[u-2], U[u-1], P[i]) >= 0u--;
  20.         L[l++] = P[i];
  21.         U[u++] = P[i];
  22.     }
  23.  
  24.     /* 旋轉卡尺 */
  25.  
  26.     // 下凸包額外補上一個凸包頂點,以便操作。
  27.     if (u-2 >= 0L[l] = U[u-2];
  28.  
  29.     for (int i=0j=u-1i<l && j>0; )
  30.     {
  31.         cout << "夾到了頂點 L[i] 與頂點 U[j]";
  32.  
  33.         // 下方邊與上方邊的張開角度:
  34.         // 小於180°,則上方前進。想像成下方很平、上方變凸。
  35.         // 大於180°,則下方前進。想像成上方很平、下方變凸。
  36.         // 等於180°,則同時前進,亦可擇一前進。
  37.         if (cross(L[i+1] - L[i], U[j-1] - U[j]) < 0)
  38.             i++;    // 下方前進
  39.         else
  40.             j--;    // 上方前進
  41.     }
  42. }

迴圈部分亦可採一主一副的形式:每窮舉一個頂點,就立即找出對頂的點。

 
  1.     for (int i=0j=u-1i<l && j>0i++)   // i++挪入迴圈
  2.     {
  3.         cout << "夾到了頂點 L[i] 與頂點 U[j]";
  4.         if (cross(L[i+1] - L[i], U[j-1] - U[j]) > 0j--;
  5.     }

Closest Pair

程度★ 難度★

Closest Pair

一群點當中,距離最近的兩個點叫作「最近點對」。

演算法( Enumerative Method )

窮舉法的時間複雜度是 O(N^2) ,可以找出所有的最近點對。

 
  1. struct Point {double xy;} p[10];
  2.  
  3. double closest_pair()
  4. {
  5.     double d = 1e9// 最近點對的距離
  6.     Segment cp[10]; // 最近點對
  7.     int n = 0;      // 最近點對的數目
  8.  
  9.     for (int i=0i<N; ++i)         // 窮舉a點
  10.         for (int j=i+1j<N; ++j)   // 窮舉b點
  11.         {
  12.             double dij = distance(p[i], p[j]);
  13.             if (dij < d)
  14.                 d = dijn = 0cp[n++] = Segment(ij);
  15.             else (dij == d)
  16.                 cp[n++] = Segment(ij);
  17.         }
  18.  
  19.     return d;
  20. }

演算法( Enumerative Method )

此處再介紹另外一種窮舉法,時間複雜度仍是 O(N^2) ,不過會比直接窮舉全部點對的方法快上許多,也比後面要談的分治法快上許多。

1. 先排序所有點,以X座標為主,Y座標無所謂。
2. 由左往右掃,依序窮舉各點作為左端點。
 甲、由左端點開始向右掃,窮舉所有右端點。
 乙、若左右兩點距離差太遠,就可以停止窮舉右端點。

實作時,減少 sqrt 函式的呼叫次數,盡量紀錄距離的平方,可以增進執行速度。避免直接排序原資料,複製一份指標或索引值來排序,也可以增進執行速度。

 
  1. struct Point {double xy;} p[10];
  2. bool cmp(const Pointiconst Pointj) {return i.x < j.x;}
  3.  
  4. double closest_pair()
  5. {
  6.     sort(pp+10cmp); // 依X座標排序
  7.  
  8.     double d = 1e9;     // 最近點對的距離
  9.     Segment cp[10];     // 最近點對
  10.     int n = 0;          // 最近點對的數目
  11.  
  12.     for (int i=0i<N; ++i)
  13.         for (int j=i+1j<N; ++j)
  14.         {
  15.             if (p[i].x + d < p[j].xbreak;
  16.             double dij = distance(p[i], p[j]);
  17.             if (dij < d)
  18.                 d = dijn = 0cp[n++] = Segment(ij);
  19.             else (dij == d)
  20.                 cp[n++] = Segment(ij);
  21.         }
  22.  
  23.     return d;
  24. }

UVa 10245 10750 11378

演算法( Divide and Conquer )

時間複雜度是 O(N * (logN)^2) ,可以找出所有的最近點對。原理是把平面切割成左右兩側,答案分為「點對在左側」、「點對在右側」、「點對橫跨兩側」等三種情形。先將左側與右側分別遞迴求解之後,再利用左側與右側的答案,快速的算出橫跨兩側的答案。

Preprocessing:
1. 排序所有點,以X座標為主,Y座標無所謂。O(NlogN)
2. 檢查是否有共點。如果有,就找出所有共點,演算法結束。O(N)
   (只想找出其中一組最近點對時,可省略此步驟。)

Divide:
把所有點分為左右兩側,左側、右側的點數盡量一樣多。

Conquer:
左側、右側分別遞迴求解。

Merge:
1. 利用左側、右側的最佳解d,求出橫跨兩側的解:
 甲、首先找出靠近中線的點,水平距離小於d、包含d。O(N)
   (小於d、不包含d,則只找出其中一組最近點對。)
 乙、排序這些點,以Y座標為主,X座標無所謂。O(NlogN)
 丙、每一個點都找鄰近的點,看看是不是解。
   鄰近、不共點,並且偏高、等高的點,最多只有四個點。O(N*4) = O(N)
   (如果只想找其中一組最近點對,最多只有兩個點。O(N*2) = O(N))
2. 答案為左側、右側、橫跨兩側,三者當中的最佳解。

以下提供釋例。畫面上有一些點。

把點分為左側與右側,點數盡量一樣多。左側與右側的交界處可能會銜接,也可能不會銜接。左右兩側通常是不一樣寬的。

左側、右側分別遞迴求解,最後求得這兩種情況下的最近點對。最近點對的距離為 d 。

可以發現,左側的每一個點,半徑 d 以內的範圍(不包含邊界),不會有其他左側的點存在,只可能出現另一側的點。右側亦如是。

要找出橫跨兩側的點對,可以從左側的右邊線,往左距離 d 以內的範圍裡(包含邊界)的這些點,有可能與右側的點形成更近點對(一樣近點對)。

也可以以右側為主。

從左側的右邊線,往右距離 d 以內的範圍裡的這些點,則是可能的另一頭端點的範圍。

要找出橫跨兩側的最近點對,只要依序窮舉左側右邊界距離 d 以內、位於左側的點,作為左端點;然後再找左側右邊界距離 d 以內、位於右側的點,作為右端點。相距太遠的點對沒必要去找。

這種方式雖然直覺,但是不容易實作。

一個比較容易實作的方式,是把範圍內的這些點全部混在一起,不分左右,然後由下往上掃描。

對於一個點來說,只要找距離小於等於 d 的端點就行了,也就是半徑 d 以內的範圍(包含邊界)。

當運氣不好,左側、右側的邊界相互銜接的時候,又恰巧這一個點就在邊界上,此時距離小於等於 d 的端點,最多會有九個點:有四個點是同側的,位於同側的半圓周上,角度相離 60° 的位置;有五個點是異側的,其中四個點位於異側的半圓周上,角度相離 60° 的位置,另有一點與原點重疊。這是從邊邊角角瘋狂硬塞的結果。當我們由下往上掃描的時候,僅需檢查其後五點;當我們一開始有處理掉共點的問題,僅需檢查其後四點。

如果只想找出其中一組最近點對,那麼只要找距離小於 d 的端點就行了,不必找距離等於 d 的端點。距離小於 d 的端點,最多只有三個點:這三個點都是異側的,自由的在異側的半圓之內移動。從邊邊角角瘋狂硬塞,塞不進四點,必有一點會觸到圓周。當我們由下往上掃描的時候,僅需檢查其後兩點。

大功告成。

實作時,當問題被分割成剩下兩個點、三個點時,可以直接求解,不要再遞迴下去,如此可以增進執行速度。

只找出最近點對的距離
  1. struct Point {double xy;} p[10], t[10];
  2. bool cmpx(const Pointiconst Pointj) {return i.x < j.x;}
  3. bool cmpy(const Pointiconst Pointj) {return i.y < j.y;}
  4.  
  5. double DnC(int Lint R)
  6. {
  7.     if (L >= Rreturn 1e9// 沒有點、只有一個點。
  8.  
  9.     /* Divide:把所有點分成左右兩側,點數盡量一樣多。 */
  10.  
  11.     int M = (L + R) / 2;
  12.  
  13.     /* Conquer:左側、右側分別遞迴求解。 */
  14.  
  15.     double d = min(DnC(L,M), DnC(M+1,R));
  16. //  if (d == 0.0) return d; // 提早結束
  17.  
  18.     /* Merge:尋找靠近中線的點,並依Y座標排序。O(NlogN)。 */
  19.  
  20.     int N = 0;  // 靠近中線的點數目
  21.     for (int i=M;   i>=L && p[M].x - p[i].x < d; --it[N++] = p[i];
  22.     for (int i=M+1i<=R && p[i].x - p[M].x < d; ++it[N++] = p[i];
  23.     sort(tt+Ncmpy); // O(NlogN)
  24.  
  25.     /* Merge:尋找橫跨兩側的最近點對。O(N)。 */
  26.  
  27.     for (int i=0i<N-1; ++i)
  28.         for (int j=1j<=2 && i+j<N; ++j)
  29.             d = min(ddistance(t[i], t[i+j]));
  30.  
  31.     return d;
  32. }
  33.  
  34. double closest_pair()
  35. {
  36.     sort(pp+10cmpx);
  37.     return DnC(0N-1);
  38. }

如果在 Merge 階段,以 Merge Sort 將所有點重新依照 Y 座標排序,時間複雜度可以降到 O(NlogN) 。然而執行 Merge Sort 的額外負擔實在太大,通常會比原來的方法慢上許多。

只找出最近點對的距離
  1. struct Point {double xy;} p[10], t[10];
  2. bool cmpx(const Pointiconst Pointj) {return i.x < j.x;}
  3. bool cmpy(const Pointiconst Pointj) {return i.y < j.y;}
  4.  
  5. double DnC(int Lint R)
  6. {
  7.     if (L >= Rreturn 1e9// 沒有點、只有一個點。
  8.  
  9.     /* Divide:把所有點分成左右兩側,點數盡量一樣多。 */
  10.  
  11.     int M = (L + R) / 2;
  12.  
  13.     // 先把中線的X座標記起來,因為待會重新排序之後會跑掉。
  14.     double x = p[M].x;
  15.  
  16.     /* Conquer:左側、右側分別遞迴求解。 */
  17.  
  18.     // 遞迴求解,並且依照Y座標重新排序。
  19.     double d = min(DnC(L,M), DnC(M+1,R));
  20. //  if (d == 0.0) return d; // 提早結束
  21.  
  22.     /* Merge:尋找靠近中線的點,並依Y座標排序。O(N)。 */
  23.  
  24.     // 尋找靠近中線的點,先找左側。各點已照Y座標排序了。
  25.     int N = 0;  // 靠近中線的點數目
  26.     for (int i=0i<=M; ++i)
  27.         if (x - p[i].x < d)
  28.             t[N++] = p[i];
  29.  
  30.     // 尋找靠近中線的點,再找右側。各點已照Y座標排序了。
  31.     int P = N;  // P為分隔位置
  32.     for (int i=M+1i<=R; ++i)
  33.         if (p[i].x - x < d)
  34.             t[N++] = p[i];
  35.  
  36.     // 以Y座標排序。使用Merge Sort方式,合併已排序的兩陣列。
  37.     inplace_merge(tt+Pt+Ncmpy);
  38.  
  39.     /* Merge:尋找橫跨兩側的最近點對。O(N)。 */
  40.  
  41.     for (int i=0i<N; ++i)
  42.         for (int j=1j<=2 && i+j<N; ++j)
  43.             d = min(ddistance(t[i], t[i+j]));
  44.  
  45.     /* Merge:重新以Y座標排序所有點。O(N)。 */
  46.  
  47.     // 如此一來,更大的子問題就可以直接使用Merge Sort。
  48.     inplace_merge(p+Lp+M+1p+R+1cmpy);
  49.  
  50.     return d;
  51. }
  52.  
  53. double closest_pair()
  54. {
  55.     sort(pp+10cmpx);
  56.     return DnC(0N-1);
  57. }

如果一開始將所有點複製一份,先以 Y 座標排序,那麼在遞迴途中就不用每次都排序。如此會使時間複雜度暴增為 O(N^2) ,甚至比窮舉法還慢。

只找出最近點對的距離
  1. struct Point {double xy;} px[10], py[10], t[10];
  2. bool cmpx(const Pointiconst Pointj) {return i.x < j.x;}
  3. bool cmpy(const Pointiconst Pointj) {return i.y < j.y;}
  4.  
  5. double DnC(int Lint R)
  6. {
  7.     if (L >= Rreturn 1e9;
  8.  
  9.     int M = (L + R) / 2;
  10.     double d = min(DnC(L,M), DnC(M+1,R));
  11. //  if (d == 0.0) return d; // 提早結束
  12.  
  13.     /* Merge:依照Y座標順序,來尋找靠近中線的點。O(N)。 */
  14.  
  15.     int N = 0;  // 靠近中線的點數目
  16.     for (int i=0i<10; ++i)    // 全部的點都要找過一遍
  17.         if (py[i].x - px[M].x < d)
  18.             t[N++] = py[i];
  19.  
  20.     for (int i=0i<N-1; ++i)
  21.         for (int j=1j<=2 && i+j<N; ++j)
  22.             d = min(ddistance(t[i], t[i+j]));
  23.  
  24.     return d;
  25. }
  26.  
  27. double closest_pair()
  28. {
  29.     sort(pxpx+10cmpx);
  30.     copy(pxpx+10py);    // 先複製一份
  31.     sort(pypy+10cmpy);  // 然後依照Y座標排序
  32.     return DnC(0N-1);
  33. }

Farthest Pair

程度★ 難度★★★

Farthest Pair

一群點當中,距離最遠的兩個點叫作「最遠點對」。

窮舉法的時間複雜度是 O(N^2) ,可以找出所有的最遠點對。

使用旋轉卡尺,時間複雜度是 O(NlogN) 。

想法

距離變遠,就是擴張。無論是擴張兩點連線,或者是擴張邊界,距離都是變遠。

要找最遠點對,可以使用擴張邊界的概念。擴張邊界直到極限,邊界便會碰觸到最偏遠的點,最後形成凸包。

因此,最遠點對一定是凸包的對角線。就如同日常生活中,邊邊角角的寬度是最寬的、最容易卡到。

也許這段論述太過抽象、不夠嚴謹。來詳細推敲一番吧!

凸多邊形範圍內,最遠的距離是對角線距離。

換句話說,凸多邊形範圍內任一線段,必定短於、等長於某一條對角線。此處改用擴張兩點連線的概念進行說明。

一、凸多邊形範圍內,任取兩點。

二、延長兩點連線,直到邊界。

三、挪動一點至頂點,盡可能遠離垂足。

四、挪動另一點至頂點,同上。

藉由此性質,以旋轉卡尺,窮舉最長對角線的斜率,量測最長對角線的長度,就能輕鬆找到最遠點對。

凸多邊形的最長對角線們,斜率皆不同。

同一個斜率,如果有兩條以上的最長對角線,就會產生矛盾──以兩條最長對角線描出平行四邊形,可以發現平行四邊形的對角線更長。凸多邊形範圍內一定可以順利描出平行四邊形,請參考凸的定義。

凸多邊形範圍內,同一個斜率,最多只有一條最長對角線。因此,同一個斜率,旋轉卡尺只能夾到一條最長對角線。

凸多邊形的最長對角線數目,不超過頂點數目。

平面上 N 個點,其凸包最多有 N 個頂點。隨著卡尺旋轉,每一個頂點,都可能作為下一條最長對角線的端點,可以推理出凸包最多有 N 條最長對角線,此時形成正多角星。

平面上 N 個點的最遠點對,最多只有 N 組。

演算法

一、求凸包,過濾出可能是最遠點對的點。O(NlogN)
二、旋轉卡尺,找出最長對角線,即是最遠點對。O(N)

PKU 2187 UVa 11012

Segment Intersection
( Under Construction! )

程度★ 難度★★★

Segment Intersection

使用窮舉法窮舉兩線段,時間複雜度是 O(N^2) ,可以求出所有交點。

這裡要介紹的演算法,是先將線段排序,再由左到右依序窮舉各線段,判斷相交,時間複雜度為 O((N+K)*logN) 。類似於最近點對的窮舉法。

一、排序所有端點:
 甲、X座標,由小到大。
 乙、Y座標,由小到大。
 丙、左端點先於右端點。
 丁、下端點先於上端點。下可當作左,上可當作右。
二、依序掃描各端點:
 甲、若為左端點,把線段放入二元樹。並判斷是否相交,求交點。
 乙、若為右端點,從二元樹拿出線段。
 丙、若為交點,顛倒對應線段在二元樹當中的順序。

若只是要判斷是否有交點,而不是要找出所有交點,那麼可以把演算法改成更容易實作的形式,時間複雜度是 O(NlogN) 。請參考 CLRS 。

Shamos-Hoey              O(NlogN)
Bentley-Ottmann          O(NlogN + KlogK) = O((N+K)*logN)   K < C(N,2) < N^2
Chazelle & Edelsbrunner  O(NlogN + K)
Balaban                  O(NloglogN + K)

ICPC 4125

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值