1. 程式人生 > >凸包的幾種演算法 主要Graham-Scan演算法的水平序法 另加poj113 wall的解題

凸包的幾種演算法 主要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演算法的前提是“

各個點有序”。所以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 潘金貴 顧鐵成等人譯