凸包的幾種演算法 主要Graham-Scan演算法的水平序法 另加poj113 wall的解題
在說這個題目之前,我想給大家介紹一些這幾天我瞭解到的有關凸包的知識:
1、Gift-Wrapping(捲包裹演算法)
這個演算法在《演算法藝術》上說的很清楚了(p391-393),如果理解的還不是很清楚,在這裡講解的特別好,特別清楚,由於這個很簡單,所以就不談論它了。我對這個演算法的理解是:時間複雜度是O(N*H),N是點的個數,H是在凸包上點的個數。所以捲包裹演算法很適合凸包上的點很少的時候,通常情況下,隨機資料會很快。但是如果構造出的凸包上的點很多的時候,它就會很慢了,不如說,所有點都在一個園上的時候。
2、Graham-Scan演算法
《演算法藝術》(p393-396)。我覺得上面講的很詳細很詳細了。同樣,如果理解的不好可以看看
這個演算法是基於一個有序的點,所以必須要排序的。關鍵就在這裡,排序分兩種:極角排序和水品排序。估計大多數人都會第一種。
先說第一種。首先要找到一個一定在凸包上的點p0,這個點怎麼找?自己想想吧。然後就以這個點為基點,對所有點按照極角大小排序。這裡注意一下,如果極角相等,那麼就按距離從小到大排序。這個是防止共線問題,關於這個下面好好討論。然後把排序後的p0,p1,p2放入棧中,接著就是遍歷每個點了,始終保證非“右手”方向就好了,具體實現去網上搜,很多。
然後就是第二種的水平序。排序準則是先按y大小排序,如果y相等,就按x排序。相對於第一種,這個排序簡單的多。然後和上面的思想一樣,只不過要分兩步,右鏈和左鏈,先做右鏈,從0到排序最後點,然後再反過來,進行左鏈。具體的細節可以參考下面的程式碼。可能你在想這樣有什麼好處呢,我要說的是,這樣可以很完美的解決共線問題。
我對這個演算法的理解是:對於隨機資料,可能沒有捲包裹演算法快。但是也是可以接受的。之所以喜歡的原因是,它可以完美的解決共線這個一直讓人頭大的問題。
3、Melkman演算法
首先要說的是:很多人都認為這個是最好的演算法。這個演算法可以在個點有序的前提下,每次獲得一個點就可以將先前的凸包改造成新的凸包,因此,這個是一個線上演算法,它有著其他演算法無法比擬的優勢。1987年Melkman提出的的凸包演算法,它不再使用堆疊了,轉而使用雙向表,這為凸包演算法的歷史掀開了嶄新的一夜。
具體實現我就不說了,相信看過上面幾句話的,現在都已經忍不住要學習了。
今天看了一下午《演算法藝術》和網上的一些資料,終於又有了一些理解,那就補充一下吧。之前都是模模糊糊的,現在明白了,可能我現在理解的還是錯誤的,但是我還是要說出來:Melkman演算法的前提是“
所以最終得到的結論是:求點集的凸包,時間複雜度的底線是O(nlogn)。
最後討論一下共線的問題:
假設有這麼幾個點
0 0
0 1
0 2
2 2
1 1
和這幾個點
1 1
2 2
3 3
4 4
對於上面的兩組資料,如果用捲包裹演算法和Graham-Scan的極角排序法做,要求只輸出凸包上的定點,會出現什麼樣的問題??
如果用Graham-Scan的水平序來寫,會不會出現同樣的問題??
大家可以好好想想!!!!
還有很多凸包方面的演算法,比如Jarvis步進法、增量、溶解、QuickHull等,這些多用於數學中,實踐意義不大,所以就不說了。有興趣瞭解凸包演算法的發展史,可以看看藍點大神的《漫話二維凸包》。。
現在說poj1113 wall:
意思就是給一個城堡的牆角的座標,讓你用最短的圍牆圍起來,並且圍牆離城堡的距離不能少於L。
說白了就是一個很裸的凸包問題。對於那個不能小於L,等作出了凸包,然後把邊向外移動L,自己畫畫就看出來了,每個角處的弧,加起來剛好是一個半徑為L的圓,所以最終結果就是圓的周長加上凸包的周長。
值得注意的是:由於精度問題,對於那些共線的點,應該取最遠處那個計算長度,不然可能會一直WA,這個就體現了Graham-Scan演算法的水平序的優勢了。大家好好品味。。
比如上面提到的第一組資料,計算的頂點應該是0 0,0 2和2 2,而不是所有的點,即使所有的點都在凸包上,如果計算所有的點,誤差就會大很多,肯能就是一直WA的。
程式碼如下:
//突然想用類寫個模板,由於c++沒學好,所以弄了一上午才弄萬,不好的地方歡迎大家指點。。。。。
#include <stdio.h>
#include <iostream>
#include <algorithm>
#include <string.h>
#include <math.h>
using namespace std;
const int N=1100;
struct Node{
int x,y;
bool operator<(Node a)const{
return y<a.y||(y==a.y&&x<a.x);
}
};
class Graham_Scan{
public:
Graham_Scan(int r){
num_node=r;
for(int i=0;i<=num_node;i++)
visit[i]=true;
}
bool judg(){//判斷給的點是否符合條件能夠成凸包
if(num_node<2)
return 0;
}
void init();
void fun();
void print_node(bool jud);//輸出凸包上的點,如果jud真就輸出所有點,否則就輸出定點
double print_per();//輸出凸包的周長
private:
int num_node,stack_all[N],stack[N],top_all,top;
Node node[N];
double dis(Node a,Node b);
void Graham_scan();//Graham_Scan演算法水平序的實現
bool visit[N];
int turn(int a,int b,int c);
};
void Graham_Scan::init()
{
for(int i=0;i<num_node;i++)
scanf("%d%d",&node[i].x,&node[i].y);
}
double Graham_Scan::dis(Node a,Node b)
{
double c;
c=(b.x-a.x)*(b.x-a.x)+(b.y-a.y)*(b.y-a.y);
return sqrt(c);
}
int Graham_Scan::turn(int a,int b,int c)
{
return (node[b].x-node[a].x)*(node[c].y-node[a].y)-(node[b].y-node[a].y)*(node[c].x-node[a].x);
}
void Graham_Scan::fun()
{
Graham_scan();
}
void Graham_Scan::Graham_scan()
{
int i;
sort(node,node+num_node);
stack_all[0]=stack[0]=0;
stack_all[1]=stack[1]=1;
top=top_all=1;
visit[1]=false;
//執行右鏈,同時標記已經在右鏈上的點
for(i=2;i<num_node;i++)
{
while(top>0&&turn(stack[top-1],stack[top],i)<=0)
top--;
stack[++top]=i;
while(top_all>0&&turn(stack_all[top_all-1],stack_all[top_all],i)<0)
{
visit[stack_all[top_all]]=true;
top_all--;
}
stack_all[++top_all]=i;
visit[i]=false;
}
//現在的top,top_all點一定是最上邊,最右邊的那個點
//執行左鏈,逃過在右鏈上已經在凸包上的點,看別人寫的時候沒有跳過,不過演算法書上說是要跳過的,所以還是跳過吧
int top1=top,top1_all=top_all;
stack[++top]=num_node-2;
stack_all[++top_all]=num_node-2;
for(i=num_node-3;i>=0;i--)
{
while(visit[i]&&top>top1&&turn(stack[top-1],stack[top],i)<=0)
top--;
stack[++top]=i;
while(visit[i]&&top_all>top1_all&&turn(stack_all[top_all-1],stack_all[top_all],i)<0)
top_all--;
stack_all[++top_all]=i;
}
//現在的top,top_all點一定是0點
}
void Graham_Scan::print_node(bool jud)
{
if(jud)
{
for(int i=0;i<top_all;i++)
cout<<node[stack_all[i]].x<<" "<<node[stack_all[i]].y<<endl;
}
else
for(int i=0;i<top;i++)
cout<<node[stack[i]].x<<" "<<node[stack[i]].y<<endl;
}
double Graham_Scan::print_per()
{
double ans=0.0;
for(int i=1;i<=top;i++)
{
ans+=dis(node[stack[i]],node[stack[i-1]]);
}
return ans;
}
int main()
{
int n,l;
while(~scanf("%d%d",&n,&l))
{
double ans=acos(-1.0)*2*l;
Graham_Scan solve(n);
if(solve.judg())
{
cout<<"No\n"<<endl;
continue;
}
solve.init();
solve.fun();
ans+=solve.print_per();
printf("%0.lf\n",ans);
}
}
參考資料:
《演算法藝術與資訊學競賽》 劉汝佳 黃亮
《演算法導論》 Thomas H.Cormen、Charles E.Leiserson、Ronald L.Rivest、Clifford Stein 潘金貴 顧鐵成等人譯