1. 程式人生 > >數論小白都能看懂的平面凸包詳解

數論小白都能看懂的平面凸包詳解

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、信用卡凸包

P3829 [SHOI2012]信用卡凸包

是一道上海的省選題,不過並不難。

題意簡敘:

給你一堆如上圖所示的卡片,求其凸包周長(凸包可以包含圓弧)

分析:

我們可以先來考慮\(r=0\)的情況。

發現\(r=0\)即為信用卡為矩形,於是就按照正常的思路將點列出跑Graham演算法即可。


然後開始想正解

因為樣例三是最普遍的情況,所以研究一下:

發現我們把每一個被磨圓的頂角往圓心裡看,再重新構造凸包,然後發現黑色內圈與綠藍外圈有重疊部分。

然後分解一下,如紅筆。

發現恰好多出4個\(\frac{1}{4}\)圓弧,也就是一個圓

再驗證幾個發現也是對的。

於是這個問題就轉換為裸的凸包模板了。

5、後記:

不管怎樣,這一篇日報居然寫完了,雖然這種演算法考察在noip中不常見,但最近風雲變幻,誰知道以後會出什麼題,但現在把整個演算法的各種變形都推得明明白白還不如複習好之前的演算法,所以我們到目前把模板掌握,避免考試出板子時卻手足無措的情況發生就行。

  • 配圖十分不易,講解努力詳細,望您不吝賜贊