转载:凸壳算法集及描述(繁体中文)
2015-03-05 02:57
197 查看
来源:http://acm.nudt.edu.cn/~twcourse/ConvexHull.html#a11
中文譯做「凸包」,能包住物品的最小的凸外殼,也就是能將全部東西包進去的最小凸多邊形。凸的定義是圖形內任兩點的連線不會經過圖形外部:http://mathworld.wolfram.com/Convex.html。這裡我們只討論:從二維平面上散佈的點當中找出凸包。
在所有點的外圍繞一圈可得一凸多邊形,即是凸包。
凸包所包住的區域,為各點之間做線性內插後的範圍。
UVa
109
132
218
361
681
811
819
10002
10065
10078
10135
10173
10256
11626
Convex Hull: Jarvis' March
Jarvis' March ( Gift Wrapping Algorithm)
從一個凸包上的頂點開始,順著外圍繞一圈,順時針或逆時針都可以。
當要尋找下一個被包覆的點時,則窮舉平面上所有點,找出位於最外圍的一點來包覆即可(可利用外積運算來做判斷)。時間複雜度為 O(N*M), M
為凸包的頂點數目。
// P為平面上的那些點。這裡設定為剛好100點。
// CH為凸包上的點。這裡設定為照逆時針順序排列。
struct Point {int x, y;} P[100], CH[100];
// 小於。用以找出最低最左邊的點。
bool compare(Point& a, Point& b)
{
return (a.y < b.y) || (a.y == b.y && a.x < b.x);
}
// 向量OA cross 向量OB。大於零表示從OA到OB為順時針旋轉。
double cross(Point& o, Point& a, Point& b)
{
return (a.x - o.x) * (b.y - o.y) - (a.y - o.y) * (b.x - o.x);
}
void findConvexHull()
{
/* 用最低最左邊的點當作是起點。起點可以用凸包上面任意一個點。 */
int s = 0;
for (int i=0; i<100; ++i)
if (compare(P[i], P[s]))
s = i;
/* 包禮物,逆時針方向。 */
CH[0] = P[s]; // 紀錄起點
for (int m=1; true; ++m) // m 為凸包頂點數目
{
/* 開始窮舉所有點,找出位於最外圍的一點 */
int next = s;
if (m == 1) next = !s; // 找第一點時,next預設為起點以外的點,
// 否則cross會一直等於零。
for (int i=0; i<100; ++i)
if (cross(CH[m], P[i], P[next]) < 0)
next = i;
if (next == s) break; // 繞一圈後回到起點了
CH[m] = P[next]; // 紀錄方才所找到的點
}
}
Convex Hull: Graham's Scan
Graham's Scan
由前面段落可知:凸包上的頂點們有順序的沿著外圍繞行一圈。若能照此順序來包,就不必以窮舉所有點的方式來尋找最外圍的點。 Graham's Scan即是嘗試將所有點按照順序排好,再來做繞一圈的動作。
順序該如何決定呢?只要能確保凸包各頂點的前後順序是正確的,那麼便不會包錯。一個簡單的想法是依角度排序──只要將中心點設定在凸包內部或設定在凸包上面,便可以確保凸包各頂點的前後順序必定正確(讀者可自行証明此說)。
除了凸包各頂點的前後順序要正確,另外還要限制所有點依照前後順序連線起來後,不會繞成超過一個的圈圈,也不會有任何邊重疊。更精準的說法是:會形成簡單多邊形( simple polygon),不會有邊相交。如此一來,便不必理會那些不在凸包上面的點的前後順序,因為那些點會在找最外圍的點的時候被淘汰掉(讀者可自行証明此說)。
一般來說,選擇凸包上面的端點當作排序角度時的中心點是比較好的,因為最大的夾角必會小於 180 度,而可以使用外積運算來排序。(外積在大於 180度時會得負值、等於 180
度時會等於零,導致排序錯誤。)
如果凸包各頂點的前後順序是錯誤的,或者所有點依照前後順序連線後產生了很多圈圈,就會發生慘劇。有時甚至會找出凹的形狀。
其他細節在演算法書籍上面皆可找到,故不細講。時間複雜度為 O(NlogN) ,主要取決於排序的時間;若用 Counting Sort之類的排序方法便可達到 O(N)
;若已知這些點構成的簡單多邊形之後,便不需排序,就只需 O(N)。
// P為平面上的那些點。這裡設定為剛好100點。
// CH為凸包上的點。這裡設定為照逆時針順序排列。
struct Point {int x, y;} P[100+1], CH[100+1];
// 向量OA cross 向量OB。大於零表示從OA到OB為順時針旋轉。
double cross(Point& o, Point& a, Point& b)
{
return (a.x - o.x) * (b.y - o.y) - (a.y - o.y) * (b.x - o.x);
}
// 小於。用以找出最低最左邊的點。
bool compare_position(Point& a, Point& b)
{
return (a.y < b.y) || (a.y == b.y && a.x < b.x);
}
// 小於。以P[0]當中心點做角度排序,以逆時針方向排序。
// 若角度一樣,則順序隨便。
bool compare_angle(Point& a, Point& b)
{
return cross(P[0], a, b) > 0;
}
void findConvexHull()
{
/* 用最低最左邊的點當作是起點。起點必須是凸包的端點。 */
// 將端點換到第一點。O(N)
swap(P[0], *min_element(P, P+100, compare_position));
// 其餘各點照角度排序,並以第一點當中心點。O(NlogN)
sort(P+1, P+100, compare_angle);
/* 包,逆時針方向。O(N) */
P[N] = P[0];
int m = 0; // m 為凸包頂點數目
for (int i=0; i<=100; ++i) {
while (m >= 2 && cross(CH[m-2], CH[m-1], P[i]) < 0) m--;
CH[m++] = P[i];
}
m--; // 最後一個點是重複出現兩次的起點,故要減一。
}
若要連凸包上面共線的點都找出來,便要小心處理剛開始包、快要包好時產生共線的情形,這些點的先後順序決不能亂。
有一個解決方法是分做左右兩邊包,當排序時遇到角度相同的情況時,令距離離中心點較短的順序較高。總之相當麻煩,就不細講了。下面這段程式碼寫出一些特別要注意的地方:
// 小於。以P[0]當中心點做角度排序,以逆時針方向排序。
// 若角度一樣,則距離較離中心點較短的順序較高。
bool compare_angle(Point& a, Point& b)
{
// 加入角度相同時,距離長度的判斷。
int c = cross(P[0], a, b);
return (c > 0) || (c == 0 && len2(P[0], a) < len2(P[0], b));
}
void findConvexHull()
{
......
// 這邊的判斷記得要改成小於等於零,以包含共線情形。
while (m >= 2 && cross(CH[m-2], CH[m-1], P[i]) <= 0) m--;
......
}
Convex Hull: Andrew's Monotone Chain
Andrew's Monotone Chain
排順序時改為依座標大小排序。這個方法非常優美,而且能處理共線的情形: http://www.algorithmist.com/index.php/Monotone_Chain_Convex_Hull 。我也找到了有趣的 Applet:
http://wind.lcs.mit.edu/~aklmiu/6.838/convexhull/index.html 。
時間複雜度為下述兩項總和:一、一次排序,通常為 O(NlogN) ;二、掃描 2N個點,為 O(N)
。
// P為平面上的那些點。這裡設定為剛好100點。
// CH為凸包上的點。這裡設定為照逆時針順序排列。
struct Point {int x, y;} P[100], CH[100];
// 向量OA cross 向量OB。大於零表示從OA到OB為順時針旋轉。
double cross(Point& o, Point& a, Point& b)
{
return (a.x - o.x) * (b.y - o.y) - (a.y - o.y) * (b.x - o.x);
}
// 小於。依座標大小排序,先排 x 再排 y。
bool compare(Point& a, Point& b)
{
return (a.x < b.x) || (a.x == b.x && a.y < b.y);
}
void findConvexHull()
{
// 將所有點依照座標大小排序
sort(P, P+100, compare);
int m = 0; // m 為凸包頂點數目
// 包下半部
for (int i=0; i<100; ++i) {
while (m >= 2 && cross(CH[m-2], CH[m-1], P[i]) <= 0) m--;
CH[m++] = P[i];
}
// 包上半部,不用再包入方才包過的終點,但會再包一次起點
for (int i=100-2, t=m+1; i>=0; --i) {
while (m >= t && cross(CH[m-2], CH[m-1], P[i]) <= 0) m--;
CH[m++] = P[i];
}
}
Convex Hull: Quick Hull Algorithm
演算法
這是一個運用 Divide and Conquer 的演算法。
時間複雜度為下述兩項總和:一、一次排序的時間,通常為 O(NlogN) ;二、 Divide and Conquer向下遞迴 O(logN)
深度,合併凸包要 O(N) 時間,總共需時 O(NlogN)
。
Convex Hull: Melkman's Algorithm
演算法
求出一簡單多邊形的凸包。
http://cgm.cs.mcgill.ca/~athens/cs601/Melkman.html
時間複雜度為 O(N) 。是相當優美的演算法。
中文譯做「凸包」,能包住物品的最小的凸外殼,也就是能將全部東西包進去的最小凸多邊形。凸的定義是圖形內任兩點的連線不會經過圖形外部:http://mathworld.wolfram.com/Convex.html。這裡我們只討論:從二維平面上散佈的點當中找出凸包。
在所有點的外圍繞一圈可得一凸多邊形,即是凸包。
凸包所包住的區域,為各點之間做線性內插後的範圍。
UVa
109
132
218
361
681
811
819
10002
10065
10078
10135
10173
10256
11626
Convex Hull: Jarvis' March
Jarvis' March ( Gift Wrapping Algorithm)
從一個凸包上的頂點開始,順著外圍繞一圈,順時針或逆時針都可以。
當要尋找下一個被包覆的點時,則窮舉平面上所有點,找出位於最外圍的一點來包覆即可(可利用外積運算來做判斷)。時間複雜度為 O(N*M), M
為凸包的頂點數目。
// P為平面上的那些點。這裡設定為剛好100點。
// CH為凸包上的點。這裡設定為照逆時針順序排列。
struct Point {int x, y;} P[100], CH[100];
// 小於。用以找出最低最左邊的點。
bool compare(Point& a, Point& b)
{
return (a.y < b.y) || (a.y == b.y && a.x < b.x);
}
// 向量OA cross 向量OB。大於零表示從OA到OB為順時針旋轉。
double cross(Point& o, Point& a, Point& b)
{
return (a.x - o.x) * (b.y - o.y) - (a.y - o.y) * (b.x - o.x);
}
void findConvexHull()
{
/* 用最低最左邊的點當作是起點。起點可以用凸包上面任意一個點。 */
int s = 0;
for (int i=0; i<100; ++i)
if (compare(P[i], P[s]))
s = i;
/* 包禮物,逆時針方向。 */
CH[0] = P[s]; // 紀錄起點
for (int m=1; true; ++m) // m 為凸包頂點數目
{
/* 開始窮舉所有點,找出位於最外圍的一點 */
int next = s;
if (m == 1) next = !s; // 找第一點時,next預設為起點以外的點,
// 否則cross會一直等於零。
for (int i=0; i<100; ++i)
if (cross(CH[m], P[i], P[next]) < 0)
next = i;
if (next == s) break; // 繞一圈後回到起點了
CH[m] = P[next]; // 紀錄方才所找到的點
}
}
Convex Hull: Graham's Scan
Graham's Scan
由前面段落可知:凸包上的頂點們有順序的沿著外圍繞行一圈。若能照此順序來包,就不必以窮舉所有點的方式來尋找最外圍的點。 Graham's Scan即是嘗試將所有點按照順序排好,再來做繞一圈的動作。
順序該如何決定呢?只要能確保凸包各頂點的前後順序是正確的,那麼便不會包錯。一個簡單的想法是依角度排序──只要將中心點設定在凸包內部或設定在凸包上面,便可以確保凸包各頂點的前後順序必定正確(讀者可自行証明此說)。
除了凸包各頂點的前後順序要正確,另外還要限制所有點依照前後順序連線起來後,不會繞成超過一個的圈圈,也不會有任何邊重疊。更精準的說法是:會形成簡單多邊形( simple polygon),不會有邊相交。如此一來,便不必理會那些不在凸包上面的點的前後順序,因為那些點會在找最外圍的點的時候被淘汰掉(讀者可自行証明此說)。
一般來說,選擇凸包上面的端點當作排序角度時的中心點是比較好的,因為最大的夾角必會小於 180 度,而可以使用外積運算來排序。(外積在大於 180度時會得負值、等於 180
度時會等於零,導致排序錯誤。)
如果凸包各頂點的前後順序是錯誤的,或者所有點依照前後順序連線後產生了很多圈圈,就會發生慘劇。有時甚至會找出凹的形狀。
其他細節在演算法書籍上面皆可找到,故不細講。時間複雜度為 O(NlogN) ,主要取決於排序的時間;若用 Counting Sort之類的排序方法便可達到 O(N)
;若已知這些點構成的簡單多邊形之後,便不需排序,就只需 O(N)。
// P為平面上的那些點。這裡設定為剛好100點。
// CH為凸包上的點。這裡設定為照逆時針順序排列。
struct Point {int x, y;} P[100+1], CH[100+1];
// 向量OA cross 向量OB。大於零表示從OA到OB為順時針旋轉。
double cross(Point& o, Point& a, Point& b)
{
return (a.x - o.x) * (b.y - o.y) - (a.y - o.y) * (b.x - o.x);
}
// 小於。用以找出最低最左邊的點。
bool compare_position(Point& a, Point& b)
{
return (a.y < b.y) || (a.y == b.y && a.x < b.x);
}
// 小於。以P[0]當中心點做角度排序,以逆時針方向排序。
// 若角度一樣,則順序隨便。
bool compare_angle(Point& a, Point& b)
{
return cross(P[0], a, b) > 0;
}
void findConvexHull()
{
/* 用最低最左邊的點當作是起點。起點必須是凸包的端點。 */
// 將端點換到第一點。O(N)
swap(P[0], *min_element(P, P+100, compare_position));
// 其餘各點照角度排序,並以第一點當中心點。O(NlogN)
sort(P+1, P+100, compare_angle);
/* 包,逆時針方向。O(N) */
P[N] = P[0];
int m = 0; // m 為凸包頂點數目
for (int i=0; i<=100; ++i) {
while (m >= 2 && cross(CH[m-2], CH[m-1], P[i]) < 0) m--;
CH[m++] = P[i];
}
m--; // 最後一個點是重複出現兩次的起點,故要減一。
}
若要連凸包上面共線的點都找出來,便要小心處理剛開始包、快要包好時產生共線的情形,這些點的先後順序決不能亂。
有一個解決方法是分做左右兩邊包,當排序時遇到角度相同的情況時,令距離離中心點較短的順序較高。總之相當麻煩,就不細講了。下面這段程式碼寫出一些特別要注意的地方:
// 小於。以P[0]當中心點做角度排序,以逆時針方向排序。
// 若角度一樣,則距離較離中心點較短的順序較高。
bool compare_angle(Point& a, Point& b)
{
// 加入角度相同時,距離長度的判斷。
int c = cross(P[0], a, b);
return (c > 0) || (c == 0 && len2(P[0], a) < len2(P[0], b));
}
void findConvexHull()
{
......
// 這邊的判斷記得要改成小於等於零,以包含共線情形。
while (m >= 2 && cross(CH[m-2], CH[m-1], P[i]) <= 0) m--;
......
}
Convex Hull: Andrew's Monotone Chain
Andrew's Monotone Chain
排順序時改為依座標大小排序。這個方法非常優美,而且能處理共線的情形: http://www.algorithmist.com/index.php/Monotone_Chain_Convex_Hull 。我也找到了有趣的 Applet:
http://wind.lcs.mit.edu/~aklmiu/6.838/convexhull/index.html 。
時間複雜度為下述兩項總和:一、一次排序,通常為 O(NlogN) ;二、掃描 2N個點,為 O(N)
。
// P為平面上的那些點。這裡設定為剛好100點。
// CH為凸包上的點。這裡設定為照逆時針順序排列。
struct Point {int x, y;} P[100], CH[100];
// 向量OA cross 向量OB。大於零表示從OA到OB為順時針旋轉。
double cross(Point& o, Point& a, Point& b)
{
return (a.x - o.x) * (b.y - o.y) - (a.y - o.y) * (b.x - o.x);
}
// 小於。依座標大小排序,先排 x 再排 y。
bool compare(Point& a, Point& b)
{
return (a.x < b.x) || (a.x == b.x && a.y < b.y);
}
void findConvexHull()
{
// 將所有點依照座標大小排序
sort(P, P+100, compare);
int m = 0; // m 為凸包頂點數目
// 包下半部
for (int i=0; i<100; ++i) {
while (m >= 2 && cross(CH[m-2], CH[m-1], P[i]) <= 0) m--;
CH[m++] = P[i];
}
// 包上半部,不用再包入方才包過的終點,但會再包一次起點
for (int i=100-2, t=m+1; i>=0; --i) {
while (m >= t && cross(CH[m-2], CH[m-1], P[i]) <= 0) m--;
CH[m++] = P[i];
}
}
Convex Hull: Quick Hull Algorithm
演算法
這是一個運用 Divide and Conquer 的演算法。
一開始將所有點以X座標位置排序。 Divide:將所有點分成左半部和右半部。 Conquer:左半部和右半部分別求凸包。 Merge:將兩個凸包合併成一個凸包。
在兩凸包頂端最凸處加一條邊, 然後在兩凸包底部最凸處加一條邊, 就變成一個凸包。 令左半部凸包最左端的點為p點,令右半部凸包最右端的點為q點。 要找上方的邊, 讓p點為基準,然後移動q點在凸包上往逆時針方向走, 讓直線pq持續往逆時針方向轉,轉到底為止。 接著讓q點為基準,然後移動q點在凸包上往逆時針方向走, 讓直線pq持續往逆時針方向轉,轉到底為止。 此時的邊pq就是上方的邊。 要找下方的邊,也可以如法炮製。 另外一種比較麻煩一點的找法是, 令左半部凸包最高的點為p點,令右半部凸包最低的點為q點。 讓p點為基準,然後移動q點在凸包上往逆時針、也往順時針方向走, 總之就是讓直線pq持續往逆時針方向轉。 接著讓q點為基準做類似的事情。 要找下方的邊,也可以如法炮製。
時間複雜度為下述兩項總和:一、一次排序的時間,通常為 O(NlogN) ;二、 Divide and Conquer向下遞迴 O(logN)
深度,合併凸包要 O(N) 時間,總共需時 O(NlogN)
。
Convex Hull: Melkman's Algorithm
演算法
求出一簡單多邊形的凸包。
http://cgm.cs.mcgill.ca/~athens/cs601/Melkman.html
時間複雜度為 O(N) 。是相當優美的演算法。
相关文章推荐
- 文章转载:在简体中文Office2003下OCR繁体中文、日文、韩文
- (转载)动态规划:从新手到专家(关于动态规划算法最精彩的中文描述,没有之一)
- Cisco 怎么做繁体中文描述
- 转载:搭建Silverlight 5中文开发环境
- linux man使用方法和centos安装中文man包- 转载
- 中文MSDN上一篇关于第三方ORM的描述文章,其ORM解决方案用起来居然跟Courser十分相似
- [原创软件]中文简体、繁体、BIG5互转
- jsp中文编码问题与编码问题的一些资料(转载)
- 【转载】tomcat5中文问题解决之道
- 中文分词-转载3_一个北京程序员
- 关于WebWork2中的中文问题 选择自 chenyun2000 的 Blog (转载)
- 取汉字的拼音首字母方法(不支持繁体中文)
- jsp中文乱码问题整理(部分转载他人)
- 转载QT中文论坛的一个帖子,版本比较老了,但是说的比较全,有了框架再干活比较顺。
- 【转载】Ubuntu 12.04 LTS 中文输入法的安装
- 转载:Matplotlib 中文用户指南 3.5 密致布局指南
- rtnetlink 中文描述
- 【转载】isspace函数的debug版本对中文处理有问题
- ASP.NET中同时支持简体和繁体中文
- java获取系统语言(区分简体中文和繁体中文)