0.前言:
本文將已詳細的配圖,帶您輕松入門平面凸包。
1.引入:
假設一個操場上有一些小朋友,下面是航拍視角:
現在他們要圍一個球場做游戲。
因為老師比較懶,所以就只能麻煩一些小朋友了(他們自己撐着繩子防止球滾出去)
而小朋友又不動腦子。所以就只能麻煩你來出主意了。
顯然,最簡單的方法是這樣:
先把一圈大繩子放在外面,然后往里縮,直到:
最外圈的小朋友撐起了繩子。
此時黑線圍成的多邊形的頂點就是小朋友所在的位置。
由此,我們就定義黑線圍成的圖形為一個平面凸包
那么,換一種定義方式,我們就定義:
平面凸包是指覆蓋平面上n個點的最小的凸多邊形。
當然,我們發現在程序中卻無法模擬此過程,於是有了下文的誕生。
2、斜率逼近法
其實這也是一種容易想到的算法,但是並不常用(代碼復雜度高),我們稍作了解
然后我們可以把這個思路具體化:
- (1)首先在所有點鍾找出一個y值最小的點,記為\(P_1\)
- (2)從\(P_1\)出發,剛開始k=0,即為水平狀態。然后按照上圖的示意沿逆時針方向尋找。即開始在\(k>0\)且\((x_2>x1,y_2>y_1)\)中找k最小的點\(P_2\),以此類推。
Q:如果過程中有多個點符合要求怎么辦?
A:那么就去距離\(P_1\)最遠的點,因為這樣能保證划定的范圍最大。
-
(3)從\(P_2\)出發,用(2)的方法找\(P_3\)
-
(4)最后直到\(P_m=P_1\)為止(已形成凸包)。
Q:為什么要剛開始找y值最小的點?
A:結合剛開始的小朋友拉繩子可知,我們在下面的繩子一定會被y值最小的小朋友擋住,即他一定在凸包上,於是就以他為基准來操作。
Q:萬一最后沒有一個\(P_m\)使得\(P_m=P_1\)呢?
A:易證必有,平面凸包總是存在的。
時間復雜度:O(nm)
n為所有小朋友的數量,m為舍己為人的小朋友的數量。
說到這里大家都明白了,一但凸包上的兩個點的斜率趨於無窮大,那么就無法解決了。
於是窩的日報又能進行下去了有人就提出了一種新的方法。
3、Jarvis算法
這其實是一種數學構造法
我們還是把那群小朋友騙聘過來:
我們考慮讓一個小朋友手里拿着一根棒子:
從外往里旋轉。
然后會撂倒碰到另一個小朋友:
然后我們讓被棒子碰到的小朋友再取一根棒子繼續打人,重復以上操作。
就是這樣。
但如果遇到以下情況:
有的小朋友在旋轉棍子時同時碰到了多於一個點(即三點共線),那么顯然我們需要選擇最遠的點。
不難證明,這樣下來也可以圍成一個平面凸包。
以上是定向的想象,那么下面就來嚴謹的說明一下
描述如下:
- 首先找到一條直線\(l\)過其中一點A,使得所有其他的點都在\(l\)的同一側。
這種直線顯然一定能找到。
由此也易證A一定為凸包上一點。
- 讓直線\(l\)以A為軸點沿順時針或逆時針方向旋轉,直到掄到除A以外的一點B
別忘了上面那個形象的講述,在遇到多於一個點時要取最遠的。
- 重復以上操作,直到l碰到A點。
在過程中受傷被碰到的點就構成了平面凸包的頂點序列。
在此過程中,雖然我們發現上述過程仍然不太好實現,但是我們還是可以通過一系列的玄學轉換得到
我們考慮到B點是最先碰到的,那么新的直線\(l'\)必然在A和除B及自身以外其他點的連線中與\(l\)的夾角最小
即紫∠比紅∠大
那么在下圖中:
\(if(\vec {AP}\times \vec {AP_i})z>0\)
則\(\vec {AP}\)到\(\vec {AP_i}\)的旋轉為逆時針旋轉。
顯然,\(\vec {AP_i}\)與l的夾角比\(\vec {AP}\)的更接近。為更好的答案。
\(else\)
\(if(\vec {AP}\times \vec {AP_i})z=0\)
那說明A,P,\(P_i\)三點共線,自然取最遠的。
我們按這個順序掃描所有的點,就能找到這個凸包上的一條邊。
顯而易見:此時間復雜度為\(O(nm)\),即每次掃n個點,一共m次可構成凸包。
但是。。。這個時間復雜度還是會涼。。。
假設就是這道題,那么我們觀察到\(n\leq 10000\),這是一道平面凸包的模板題,但是如果數據構造到m=n甚至和n相差不大的情況,那就會輕而易舉的超時。
可見,此算法僅僅適用於隨機點集,對於刻意構造的數據就會被卡成\(O(n^2)\)
而毒瘤的OI怎么會不卡呢?
連模板題都過不了,看來這個算法還是得優化,所以我們還是得用保險的算法,於是
4、Graham算法
本質:
Graham掃描算法維護一個凸殼 通過不斷在凸殼中加入新的點和去除影響凸性的點 最后形成凸包
至於凸殼: 就是凸包的一部分
算法主要由兩部分構成:
-
排序
-
掃描
(1)排序
我們的Graham算法的第一步就是對點集進行排序,這樣能保證其有序性,從而在后續的處理中達到更高效的效果,這也是Graham算法更優的原因。
開始操作:
-
我們還是選擇一個y值最小(如有相同選x最小)的點,記為\(P_1\)
-
剩下的點集中按照極角的大小逆時針排序,然后編號為\(P_2\)~\(P_m\)
達成成就:種草達人
- 我們按照排序結束時的順序枚舉每一個點,依次連線,這里可以使用一個棧來存儲,每次入棧,如果即將入棧的元素與棧頂兩個元素所構成了一個類似於凹殼的東西,那么顯然處於頂點的那個點一定不在這個點集的凸包上,而他正好在棧頂,所以把它彈出棧,新點入棧。
但是,新來的點有可能既踢走了棧頂,再連接新的棧頂元素后卻發現仍然可以踢出,此時就不能忘記判斷。
怎么樣,感覺這個算法如何?
如果您不想糾纏於繁雜的文字描述,那么下面就有精美圖片解說獻上。
(ps:下列解說中右轉左轉等是指以上一條連線為鉛垂線,新的連線偏移的方位)
剛開始,我們的點集是這樣的:
其中p1為起始點
然后p2准備入棧,由於棧中元素過少,所以檢驗合格,可直接進入。
之后因為p3仍為向左轉,符合凸包條件,所以暫時先讓它進去
p4出現了右轉現象,那么我們就把頂點p3舍去,在檢查p2的性質,合格
於是p3出棧,p4入棧
p5一切正常,入棧。
p6這里就要復雜一些
- 首先他往右轉,於是將p5彈出
- 又發現他相對於\(P_2P_4\)向右轉,於是將p4彈出
之后p6進棧。
p7一切正常(左轉),入棧
p8一切正常(左轉),入棧
所以說最后就連到了起點p1。
由此,我們的Graham算法的全過程就結束了。
凸包形成(即綠線所圍的多邊形)
掃描的時間復雜度:\(O(n)\)
但是顯然不可能做到這么優秀.
於是還有排序的時間復雜度:\(O(nlog_2n)\)
合起來總的時間復雜度:\(O(nlog_2n)\)
可見,我們在排序的幫助下省去了一些盲目的掃描,雖然排序作為一個預處理時間復雜度占據了總時間復雜度,但相比前一個算法還是更為優秀
現在我們到模板題上來。
P2742 【模板】二維凸包 / [USACO5.1]圈奶牛Fencing the Cows
題意簡敘:
求一個點集凸包的邊長和。
分析:
平面凸包模板題,注意浮點數之類的別弄丟精度就行,其他直接套模板,代碼里有注釋。
code:
#include<iostream>
#include<algorithm>
#include<cstdio>
#include<cmath>
using namespace std;
int n;
struct ben
{
double x,y;
}p[10005],s[10005];
double check(ben a1,ben a2,ben b1,ben b2)//檢查叉積是否大於0,如果是a就逆時針轉到b
{
return (a2.x-a1.x)*(b2.y-b1.y)-(b2.x-b1.x)*(a2.y-a1.y);
}
double d(ben p1,ben p2)//兩點間距離。。。
{
return sqrt((p2.y-p1.y)*(p2.y-p1.y)+(p2.x-p1.x)*(p2.x-p1.x));
}
bool cmp(ben p1,ben p2)//排序函數,這個函數別寫錯了,要不然功虧一簣
{
double tmp=check(p[1],p1,p[1],p2);
if(tmp>0)
return 1;
if(tmp==0&&d(p[0],p1)<d(p[0],p2))
return 1;
return 0;
}
int main()
{
scanf("%d",&n);
double mid;
for(int i=1;i<=n;i++)
{
scanf("%lf%lf",&p[i].x,&p[i].y);
if(i!=1&&p[i].y<p[1].y)//這是是去重
{
mid=p[1].y;p[1].y=p[i].y;p[i].y=mid;
mid=p[1].x;p[1].x=p[i].x;p[i].x=mid;
}
}
sort(p+2,p+1+n,cmp);//系統快排
s[1]=p[1];
int cnt=1;//最低點一定在凸包里
for(int i=2;i<=n;i++)
{
while(cnt>1&&check(s[cnt-1],s[cnt],s[cnt],p[i])<=0)
cnt--;
cnt++;
s[cnt]=p[i];
}
s[cnt+1]=p[1];
double ans=0;
for(int i=1;i<=cnt;i++)
ans+=d(s[i],s[i+1]);
printf("%.2lf\n",ans);
return 0;
}
4、例題:
1、信用卡凸包
是一道上海的省選題,不過並不難。
題意簡敘:
給你一堆如上圖所示的卡片,求其凸包周長(凸包可以包含圓弧)
分析:
我們可以先來考慮\(r=0\)的情況。
發現\(r=0\)即為信用卡為矩形,於是就按照正常的思路將點列出跑Graham算法即可。
然后開始想正解
因為樣例三是最普遍的情況,所以研究一下:
發現我們把每一個被磨圓的頂角往圓心里看,再重新構造凸包,然后發現黑色內圈與綠藍外圈有重疊部分。
然后分解一下,如紅筆。
發現恰好多出4個\(\frac{1}{4}\)圓弧,也就是一個圓
再驗證幾個發現也是對的。
於是這個問題就轉換為裸的凸包模板了。
5、后記:
不管怎樣,這一篇日報居然寫完了,雖然這種算法考察在noip中不常見,但最近風雲變幻,誰知道以后會出什么題,但現在把整個算法的各種變形都推得明明白白還不如復習好之前的算法,所以我們到目前把模板掌握,避免考試出板子時卻手足無措的情況發生就行。
- 配圖十分不易,講解努力詳細,望您不吝賜贊