計算幾何---凸包教學 (附帶POJ 1113
寫這道題之前我們,先了解一下是什麼是凸包問題。
凸包:在一個實數向量空間V中,對於給定集合X,所有包含X的凸集的交集S,被稱為X的凸包。
簡單的解釋就是,給出一些點。找到從這些點中,選出一些點,讓所有點都在這些點範圍內。
在上面圖中,p1,p2,p8,p7,p6組成的集合為這些點的凸包。
解決凸包問題常用的有兩種演算法:Graham掃描法和Jarvis步進法。
這篇文章我們先介紹 Jarvis步進法。
Jarvis步進法又稱為捲包裹法。為什麼這樣叫呢?因為Jarvis步進法的原理和捲包裹這幾個字很像。
捲包裹法的原理十分簡單:首先先找到一個最邊緣的點(一般是最下方的,如果有多個點,選擇最右方的點)。我們假設有一個繩子,以最邊緣的點向右逆時針旋轉直到碰到另一個點為止,此時就找到了凸包的一條邊,然後用新找到的點作為端點,繼續旋轉繩子,找到下一個點,重複這個過程直到圍成一個凸多邊形,即可得到這個點集的凸包。捲包裹法的時間複雜度為O(n²)。
原理十分簡單,但是怎麼實現是個問題。
我們先把原理的步驟寫出來,然後再慢慢的轉化成程式碼。
Step1:選擇點集中最下面的點,如果有多個,則選擇最下面的點中最左邊的一個,所選擇的點是凸包的第一個點。
Step2:以水平向右的方向作為初始射線方向,逆時針旋轉,選擇第一條初始射線之上的射線作為當前射線,當前射線經過凸包的第二個點。
Step3:以當前射線基準,繼續逆時針旋轉找到最靠近該射線的一條射線,從而找到凸包的另一個點。把這條射線作為當前射線,這個過程一直繼續,知道回到第一個點,全程過程如下圖所示:
如果把P1P2當成射線,那麼”尋找到另一條射線“這一步驟,可以理解為以點P2為中心對P1P2進行逆時針旋轉,直至碰到另一個點Pi,此時P2Pi為新的射線,也是凸多邊形的一條邊。但是程式中是不可能實現”旋轉“這一操作。於是我們有如下幾種操作實現旋轉:
1,把每一個射線與其他 n - 2條射線比較,每步比較的效率為O(n²)
2,通過計算各射線與射線P1P2的夾角,效率為O(n)但是存在浮點誤差
3,使用叉積來求各條射線斜率的相對關係,從而求得另一條射線,效率為O(n),不存在誤差。
步驟寫好了我們來考慮各步驟的程式碼實現。
先定義一下點這個結構體
struct pint{
int x,y;
}
第一步要取出最下邊最左端的點,所以我們要對點集進行排序
bool cmp(pint a,pint b){ return (a.y < b.y || (a.y == b.y && a.x < b.x)); }
然後我們要進行旋轉過程,我們旋轉用叉積來判斷各個射線的斜率相對關係。
若向量p1p3 × 向量 p1p2 > 0 則向量p1p3 在 向量 p1p2 的順時針方向
若向量p1p3 × 向量 p1p2 > 0 則向量p1p3 在 向量 p1p2 的逆時針方向
若向量p1p3 × 向量 p1p2 == 0 則向量p1p3 和 向量 p1p2 共線。
bool CrossLeft(pint p1,pint p2,pint p3){
return ((p3.x - p1.x) * (p2.y - p1.y) - (p2.x - p1.x) * (p3.y - p1.y)) < 0;
}
CrossLeft這個函式的意思是若向量p1p3 在 向量p1p2逆時針方向 則返回真 ,否則返回假
然後就是 Jarvis步進法的核心程式碼,去實現旋轉的過程。
void Jarvis(){
tail = cnt = 0;
sort(x,x+n,cmp);
sta[tail++] = 0;
sta[tail++] = 1;
for(int i = 2 ; i < n ; i ++){
while(tail > 1 && ! CrossLeft(x[sta[tail - 1]],x[sta[tail - 2]],x[i]))
tail --;
sta[tail++] = i;
}
for(int i = 0 ; i < tail ; i ++) ans[cnt++] = sta[i];
tail = 0 ;
sta[tail ++] = n - 1;
sta[tail ++] = n - 2;
for(int i = n - 3 ; i >= 0 ; i --){
while(tail > 1 && !CrossLeft(x[sta[tail - 1]],x[sta[tail - 2]],x[i]))
tail --;
sta[tail ++] = i;
}
for(int i = 0 ; i < tail ; i ++)
ans[cnt++] = sta[i];
}
來詳細的分析一波,上述程式碼中 ans【】來存結果。sta【】陣列表示凸包的一部分邊。為什麼是一部分呢?這裡sta其實是一個棧。
首先在上述程式碼中我們先把第一個點 和第二個點放入 sta中,作為第一條射線。
然後判斷 點i 和和當前線段的位置。若是最接近的當前射線的射線,那麼其他射線就會全部在他的左邊(即全部在這條射線的逆時針方向)。如果不是,那麼總會存在一條射線在他順時針方向。
進行兩邊的原因是,一次捲包裹只能覆蓋一半,所以需要反過來,再卷一次。
不理解的話,大家可以畫畫圖自己用筆模擬一下。
那現在看一下POJ 1113
題意:國王想把自己的城市圍起來,但是城市到城牆的的最小距離為 l。求個城牆的最小周長。
題解:和裸的凸包問題不太一樣。他需要讓城市到城牆的距離為l,但是不難證明 這道題的答案是 凸包周長加上一個半徑為l的圓的周長。因為要求的周長只是比凸包的周長多了幾段圓弧,而所有圓弧的圓心角和為360°。
程式碼如下:
#include<iostream>
#include<algorithm>
#include<cmath>
#include<cstdio>
using namespace std;
#define size 1000
struct pint{
int x,y;
}x[size];
bool cmp(pint a,pint b){
return (a.y < b.y || (a.y == b.y && a.x < b.x));
}
int n,l,ans[size],cnt,sta[size],tail;
bool CrossLeft(pint p1,pint p2,pint p3){
return ((p3.x - p1.x) * (p2.y - p1.y) - (p2.x - p1.x) * (p3.y - p1.y)) < 0;
}
void Jarvis(){
tail = cnt = 0;
sort(x,x+n,cmp);
sta[tail++] = 0;
sta[tail++] = 1;
for(int i = 2 ; i < n ; i ++){
while(tail > 1 && ! CrossLeft(x[sta[tail - 1]],x[sta[tail - 2]],x[i]))
tail --;
sta[tail++] = i;
}
for(int i = 0 ; i < tail ; i ++) ans[cnt++] = sta[i];
tail = 0 ;
sta[tail ++] = n - 1;
sta[tail ++] = n - 2;
for(int i = n - 3 ; i >= 0 ; i --){
while(tail > 1 && !CrossLeft(x[sta[tail - 1]],x[sta[tail - 2]],x[i]))
tail --;
sta[tail ++] = i;
}
for(int i = 0 ; i < tail ; i ++)
ans[cnt++] = sta[i];
}
int main(){
scanf("%d %d",&n,&l);
for(int i = 0 ; i < n ;i ++) scanf("%d%d",&x[i].x,&x[i].y);
Jarvis();
double re = 4 * acos(0.0) * l;
for(int i = 0 ; i < cnt - 1 ; i ++){
re += sqrt((x[ans[i]].x - x[ans[i+1]].x) * (x[ans[i]].x - x[ans[i+1]].x) + (x[ans[i]].y - x[ans[i+1]].y) * (x[ans[i]].y - x[ans[i+1]].y)) *1.0;
}
printf("%.0f\n",re);
return 0 ;
}