計算幾何入門 1.2:凸包的構造——極點法和極邊法

一、極點(extreme point)

繼續考慮釘子與橡皮筋的例子。我們可以發現邊緣上的釘子是對範圍圈定“有貢獻”的,而範內部的的釘子對範圍圈定是“沒有貢獻的”。這只是直觀的結論,嚴謹考慮我們將其抽象爲極性與極點的概念。

 

簡單的數學前提:過一個點有無數直線;有向直線可以將平面劃分確定的左右兩部分

可以在圖像上表示爲:

可以發現,邊緣上“有貢獻”的釘子,總可以找到一條穿過它的有向直線,使得其餘是有點都處於直線的同一邊;而範圍內“沒有貢獻”的釘子則無法找到這種有向直線。這正是兩種點的本質區別:極性。我們將這些“有貢獻”的點就稱爲:極點(extreme point)。那麼如何將極點在點集中篩選出來呢?也就是給定一個多邊形,如何判斷它是凸的呢?

 

二、凸包構造方法

考慮冒泡排序的原理:序列是有序的當且僅當它的每個子序列都是有序的。類比冒泡排序的原理來考察凸包問題,如果一個多邊形是對應點集的凸包,當且僅當多邊形各點都是極點,或者說多邊形各點都是有“有貢獻”的。即:凸包由極點集確定。

 

那麼如何確定某個點是否爲極點呢?再考慮顏色勾兌的例子,某種顏色能被其他顏色勾兌出來,當且僅當該顏色對應的點包含於另外三個點的範圍內。因此可以將其餘點的所有三角形組合與待定點做判斷,若待定點不在任何一個三角形的範圍內,則說明待定點是一個極點。所有極點就能構成凸包,這就是凸包的基本構造步驟。

上圖中處於某三角形內部的兩點都判定爲非極點。

 

通過上述論證,我們的計算任務劃分爲子任務

判斷某待定點是否位於某個三角形的內部(in-triangle test)

通過反覆進行in-triangle test,我們就能將各個非極點排除,最終得到極點集合,構成凸包。而進行in-triangle test的基礎就是to-left test

 

三、to-left test

上述篩選極點的算法僞代碼描述爲:

Mark all points of S as EXTREME    //首先將集合S的所有點標記爲極點

for each triangle △(p, q, r)    //對於每個三角形pqr(枚舉每個三角形)

        for each s ∈ S\{p, q, r}    //若某個集合S內非pqr的點s

                if s ∈ △(p, q, r) then    //若s在三角形pqr內(in-triangle test)

                mark s as NON_EXTREME    //則將s標記爲非極點

這個算法看似非常直白,但是其中第二行:枚舉每個三角形,需要三重循環,加上第3句枚舉每個點s,整個算法至少要O(n^4)的複雜度。

 

爲了解決基礎算法複雜度過高的問題,我們引入to-left test算法。

首先還是來看in-triangle test:如何判斷待定是否落在某三角形內部。將in-triangle test劃分爲子任務:三次to-left test,每條邊對應一次to-left test,且三次測試返回相同結果(true/false)。

我們知道,每兩個點能確定一條直線,而對於每個有向直線能將平面分爲左右兩部分。

按照慣例約定逆時針來表示三角形,因此三角形pqr對應三個有向直線:pq,qr,rp。而待定點s若在三角形pqr內部,當且僅當s對於三個有向直線都在左側。即:

ToLeft(p, q, s) == true;

ToLeft(q, r, s) == true;

ToLeft(r, p, s) == true;

實際上三條直線能將平面最多切分爲7塊,每一塊都對應三個to-left測試,而只有三角形內部的區域中三個to-left測試結果全爲真。

 

至此,我們將in-triangle test分解爲三個to-left test,問題就進一步歸結爲:如何用算法高效的表示to-left test。

 

考慮如下一般情況:

如何判斷待定點s位於有向直線pq哪一側呢?

 

當然可以通過解析幾何的方式實現:每個直線對應一個解析方程,點到直線的距離能夠通過公式計算。但是這種方式並不是最好的,其缺陷很明顯。我們先看更通用的方法:計算三角形pqs的面積S。S可由以下行列式計算(算出的是兩倍面積):

注意此處計算出的面積是由正負的,面積爲正,則s位於有向直線pq的左側,面積爲負,則s位於有向直線pq的右側。因此得到了to-left test的算法:

bool ToLeft(Point p, Point q, Point s)
{
	int area2 =   p.x * q.y - p.y * q.x 
				+ q.x * s.y - q.y * s.x
				+ s.x * p.y - s.y * p.x;
	
	return area2 > 0;
}

這種基於行列式的方法比解析幾何的優勢不僅在於更加簡單明確,更重要在於這種方法有效避免了除法和三角函數,完全消除了誤差。

 

四、極邊(extreme edge)

通過極點的引入並沒有降低凸包構造算法O(n^4)的複雜度,爲此我們轉換視角,從邊的角度考慮問題。

 

類比極點,引入極邊(extreme edge)的概念,如下圖中:

所謂極邊就是點集S中過某兩點的有向直線,使得S中其餘點都落在此直線的一側,而非極邊則兩側總是都有點。與極點類似,所有極邊圈出的區域構成了凸包。依然選擇逆時針順序來看,對於任何一個極邊,其餘點的to-left測試都是true。

 

因此,構造凸包的問題轉化爲如何篩選極邊的問題。算法僞碼描述如下:

for each directed segment pq    //對於每個有向邊pq

        if points in S\{p, q} lie to the same side of pq then    //如果S中除了pq外所有點都在有向邊pq同側

                let EE = EE ∪ {pq}    //則邊pq是極邊

再來分析一下複雜度。首先枚舉每條邊需要n^2複雜度,接下來判斷某個邊是不是極邊僅需要線性複雜度,因此算法複雜度爲O(n^3),比極點方法提高了一個量級。

 

然後將上述僞碼用C++表述出來:

bool ToLeft(Point p, Point q, Point s)
{
	int area2 =   p.x * q.y - p.y * q.x 
				+ q.x * s.y - q.y * s.x
				+ s.x * p.y - s.y * p.x;
	
	return area2 > 0;	//左側爲真
}

void checkEdge(Point S[], int n, int p, int q)
{
	bool LEmpty = true, REmpty = true;	//先假定pq左右都無點
	for (int k = 0; k < n && (LEmpty || REmpty); k++)
		if (k != p && k != q)
			ToLeft(S[p], S[q], S[k]) : LEmpty = false ? REmpty = false;
	
	if (LEmpty || REmpty)	//若有一側爲空則爲極邊
		S[p].extreme = S[q].extreme = true;
}

void markEE(Point S[], int n)	//點集S共n個點,n>2
{
	for (int k = 0; k < n; k++)	//將所有點先標爲非極點
		S[k].extreme = false;
	
	for (int p = 0; p < n; p++)
		for (int q = p + 1; q < n; q++)
			checkEdge(S, n, p, q);	//枚舉所有邊並進行判斷
}


本文是學堂在線課程《計算幾何》的筆記,參考資料:《計算幾何——算法與應用》Mark de Berg等著,鄧俊輝譯;《計算幾何——算法設計與分析》 周培德著

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