凸包算法詳解(Graham掃描法)

什麼是凸包?

來打一個比方,假設我現在拿了一塊木板,然後在上面無規則的隨意釘上了幾個釘子,並給他們命名爲P0-P12,如下圖所示:

現在我拿了一根繩子,打了一個結,讓繩子變成一個圓套,放在木板上並且讓所有釘子都處在圓套內,如下圖所示:

現在我要收縮圓套,讓所有釘子都在圓套內且繩子的長度最短,大家覺得該如何去做才能實現上述所說呢?是的,就是把釘子中最外圍的點連接成一條線,就能讓所有的釘子在圓套內,並且繩子所需要的長度最短。如下圖所示:

這類求解最外圍的點集問題,我們稱之爲凸包問題,光光是用肉眼去觀察的話,這種問題我們很快就能得出答案,並且能馬上說出哪幾個點是解,但是如果讓你敲代碼,去解決這類的問題,可能很多人會不知道如何去下手。

在講解凸包這類問題的解法前,我們首先要先講下向量積這個數學小知識。

瞭解了上面這個數學小知識後,我們現在可以正式開始着手去解決凸包問題了,首先我們先思考下,如果我們要去解決凸包問題,我們就必須要一個個去尋找最外圍的點,萬事開頭難,第一個點該從哪裏找起呢?

所有的點都在一個二維的平面上,細想一下,其中y軸(縱座標)最小的點是不是我們要找的最外圍的點之一呢?答案是肯定的,如果縱座標最小的點有多個,那麼我們就選取x軸(橫座標)最小的一個,如果這樣的點也有多個也就是重合,也不影響解題。以此類推,其實也可以找縱座標最大的點,還有橫座標最小的點,或者是橫座標最大的點作爲基準點。這裏呢,我們就以縱座標最小的點作爲我們的基準點,可以把它看作是原點。然後把剩下的點(除原點)進行排序,排序的方法爲:把每個點與原點間進行連線,如果和水平線(x軸正半軸)的夾角越小,則排在越前面,我們叫這種排序法爲極角排序

排完序後如同上圖所示,我們命名爲p0~p8,p0就是我們一開始就找到的原點(縱座標最小的點),p1是與p0連線與水平線(x軸正半軸)夾角最小的,往後p2,p3分別是夾角第二小,夾角第三小……。既然p0-p1這條直線是夾角最小的,那麼p1點就是處於靠下的位置了,也就是我們要找的最外圍的點之一了,從圖中也能觀察出這一點,反過來說,夾角最大的點p8也是最外圍的點之一了,大家也可以嘗試自己畫出幾個點來,也會得出這樣一個結論。

觀察上面這張圖,當我們連接了p0和p1後,是不是所有符合凸包算法要找的點往左邊進行了不同程度的拐彎,p1-p3直線相對於p0-p1直線往左邊拐了點,p3-p4直線相對於p1-p3直線向左邊拐了點……,由此觀察出,我們要找的點都必須向左拐彎,而不能向右拐彎,如果是向右拐彎,就說明這個點(兩條線的連接點)應該在圓套內是被包圍的點,而不是最外圍的點。

知道了這個信息後,我們代碼的編寫就知道思路了。首先,剛開始的時候我們知道p0,p1這兩個點,連接後,我們去找下一個點p2,如果p2是向左拐的我們就暫時把它算進我們最外圍的點之一,然後去找p3這個點,然後發現p2-p3是在p1-p2的基礎上向右轉的,這說明中間這個點不是我們要找的點,我們就把存進去的p2刪去倒回到p1這個點讓p1去連接p3,如果p1-p3是在p0-p1基礎上向左邊拐,我們就把p3暫時算作我們要尋找的點之一,然後去尋找p4;如果不是,則把中間的點再次刪去,倒回前一個點,如此往復,循環一邊所有的點後得出的點集就是這個凸包問題的解

如何判斷是向右轉還是向左轉,就要用到我們剛剛教的數學小知識:向量積了。

上圖這個p1-p2直線是滿足向左轉的條件的(相對於p0-p1直線),那麼既然是向左轉那麼這個p2這個點就一定在p0-p1直線的左邊,也就有:

p0-p1直線的左邊是p0-p2直線,所以現在我們只需要判斷p0-p1和p0-p2這兩條直線的向量積,如果是>0,p0-p2這條直線在p0-p1直線的逆時針方向(圍繞p0點),也就是向左轉;如果是<0,p0-p2這條直線則是在p0-p1直線的順時針方向,也就是向右轉;如果是=0,則是在同一條直線上。

好了,現在我們來練練手,來一道凸包算法的題目:(代碼解釋也會給出,在題目後面)

原題網址:http://acm.hdu.edu.cn/showproblem.php?pid=1392

 

Surround the Trees

Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 65536/32768 K (Java/Others)
Total Submission(s): 10299    Accepted Submission(s): 3991


 

Problem Description

There are a lot of trees in an area. A peasant wants to buy a rope to surround all these trees. So at first he must know the minimal required length of the rope. However, he does not know how to calculate it. Can you help him? 
The diameter and length of the trees are omitted, which means a tree can be seen as a point. The thickness of the rope is also omitted which means a rope can be seen as a line.

There are no more than 100 trees.

Input

The input contains one or more data sets. At first line of each input data set is number of trees in this data set, it is followed by series of coordinates of the trees. Each coordinate is a positive integer pair, and each integer is less than 32767. Each pair is separated by blank.

Zero at line for number of trees terminates the input for your program.

 

Output

The minimal length of the rope. The precision should be 10^-2.

 

Sample Input

9

12 7

24 9

30 5

41 9

80 7

50 87

22 9

45 1

50 7

0

 

Sample Output

243.06

 

代碼編寫:

 

struct Knight{
	int x;
	int y;
}p[maxn],s[maxn];

我們首先創建一個結構體數組p[maxn],來存放題目輸入的數據:x和y座標。至於s[maxn]是用來存放我們最外圍點集的。

int cross_product(Knight a,Knight b,Knight c){
	return (b.x-a.x)*(c.y-a.y)-(c.x-a.x)*(b.y-a.y);
}

接下來這個函數要傳進3個形參,也就是三個座標,然後通過向量積來判斷向左還是向右轉,忘記了公式的童鞋可以回去再看看上面向量積的介紹哦。

double dis(Knight a,Knight b){
	return sqrt((a.x-b.x)*(a.x-b.x)*1.0+(a.y-b.y)*(a.y-b.y)*1.0);
}

這個函數是計算兩點之間的距離。

int cmp1(Knight a,Knight b){
	if(a.y==b.y)
		return a.x<b.x;
	return a.y<b.y;
}

這個大家都經常寫到的啦,用在排序裏,意思是返回縱座標較小的值,如果縱座標相等,則返回橫座標較小的值。

int cmp2(Knight a,Knight b){
	int m = cross_product(s[0],a,b);
	if(m>0){
		return 1;
	}else if(m==0&&dis(s[0],a)-dis(s[0],b)<=0){
		return 1;
	}else{
		return 0;
	}
}

這個cmp2就是極角排序了,網上還有一種更快的排序方法,但是上面這個,我認爲相對來說好理解點,大家可以先理解上面這個排序,理解完後,再理解下面這個,我也給出來了:

//x和y爲找到的縱座標最小座標,即基準點(原點) 
int cmp2(Knight a,Knight b)
{
    if(atan2(a.y-y,a.x-x)!=atan2(b.y-y,b.x-x))
        return (atan2(a.y-y,a.x-x))<(atan2(b.y-y,b.x-x));
    return a.x<b.x;
}

附上這道題的AC代碼:算法複雜度O(nlogn)

#include <bits/stdc++.h>
#define ll long long 
using namespace std;
const int maxn = 1000;
struct Knight{
	int x;
	int y;
}p[maxn],s[maxn];

int cross_product(Knight a,Knight b,Knight c){
	return (b.x-a.x)*(c.y-a.y)-(c.x-a.x)*(b.y-a.y);
}

double dis(Knight a,Knight b){
	return sqrt((a.x-b.x)*(a.x-b.x)*1.0+(a.y-b.y)*(a.y-b.y)*1.0);
}

int cmp1(Knight a,Knight b){
	if(a.y==b.y)
		return a.x<b.x;
	return a.y<b.y;
}

int cmp2(Knight a,Knight b){
	int m = cross_product(s[0],a,b);
	if(m>0){
		return 1;
	}else if(m==0&&dis(s[0],a)-dis(s[0],b)<=0){
		return 1;
	}else{
		return 0;
	}
}
/*
//x和y爲找到的縱座標最小座標,即基準點(原點) 
int cmp2(Knight a,Knight b)
{
    if(atan2(a.y-y,a.x-x)!=atan2(b.y-y,b.x-x))
        return (atan2(a.y-y,a.x-x))<(atan2(b.y-y,b.x-x));
    return a.x<b.x;
}
*/


int main(){
	int n;
	while(scanf("%d",&n)!=EOF && n){
		for(int i=0;i<n;i++){
			scanf("%d%d",&p[i].x,&p[i].y);
		}
		if(n==1){//只有一個點的時候,就沒有周長啦 
			printf("0.00\n");
		}else if(n==2){//兩個點就可以直接計算出答案啦 
			printf("%.2lf\n",dis(p[0],p[1]));
		}else{
			memset(s,0,sizeof(s));
			sort(p,p+n,cmp1);//排序找出縱座標最小的值 
			s[0] = p[0];
			sort(p+1,p+n,cmp2);//剩下的點進行極角排序
			s[1] = p[1];//這是找到的p1點 
			int top = 1;
			for(int i=2;i<n;i++){
				while(cross_product(s[top-1],s[top],p[i])<0){
					top--;//如果是向右轉,這個中間點就不是我們要找的點 
				}
				s[++top]=p[i];//如果是向左轉,就加進來 
			}
			double ans = 0;
			for(int i=0;i<top;i++){//計算兩點之間的距離 
				ans += dis(s[i],s[i+1]);
			}
			ans += dis(s[0],s[top]);//別忘記把最後一個點和第一個點連起來 
			printf("%.2lf\n",ans);
		}
		
	}
}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章