一.問題描述
凸集(Convex Set): 任意兩點的連線都在這個集合內的集合就是一個凸集.
⒈對於一個集合D,D中任意有限個點的線性組合的全體稱為D的凸包。
⒉對於一個集合D,所有包含D的凸集之交稱為D的凸包(由此定義可以想到分治算法)。
可以證明,上述兩種定義是等價的。點集Q的凸包(convex hull)是指一個最小凸多邊形,滿足Q中的點或者在多邊形邊上或者在其內。下圖中由紅色線段表示的多邊形就是點集Q={p0,p1,...p12}的凸包。一組平面上的點,求一個包含所有點的最小的凸多邊形,這就是凸包問題了,這可以形象地想象成在地上放置一些不可移動的木樁,用一根繩子把他們盡量緊地圈起來,並且為凸邊形,這就是凸包了。二.GiftWrapping算法
又叫卷包裹算法,復雜度O(n*h),n表示共幾個點,h表示極點個數。
- 理論准備
向量叉積:也被稱為矢量積、叉積(即交叉乘積)、外積,是一種在向量空間中向量的二元運算。與點積不同,它的運算結果是一個偽向量而不是一個標量,並且兩個向量的叉積與這兩個向量垂直,方向由右手法則確定。兩個向量a和b的叉積寫作a×b(有時也被寫成a∧b,避免和字母x混淆)。
a×b=(aybz-azby)i+(azbx-axbz)j+(axby-aybx)k,為了幫助記憶,利用三階行列式,寫成
| i j k|
|ax ay az|
|bx by bz|。
b×a= -a×b,三角形ABC的面積=1/2*abs(AB×AC)(這個公式可以解決一些精度問題),不滿足結合律,但滿足雅可比恆等式:a× (b×c) +b× (c×a) +c× (a×b) =0和拉格朗日公式a× (b×c) =b(a·c) -c(a·b),可以簡單地記成“BAC - CAB”。這個公式在物理上簡化向量運算非常有效,需要注意的是,這個公式對微分算子不成立。
- 算法描述
卷包裹算法從一個必然在凸包上的點開始向着一個方向依次選擇最外側的點,當回到最初的點時,所選出的點集就是所要求的凸包。這里還有兩個問題不是很清楚:
1.怎么確定一個肯定在凸包上的點?
這個問題很好解決,取一個最左邊的也就是橫坐標最小的點(或最下邊的點,縱坐標最小的),如果有多個這樣的點, 就取這些點里縱坐標(橫坐標)最小的,這樣可以很好的處理共線的情況。
2.如何確定下一個點(即最外側的點)?向量的叉積是不滿足交換律的,向量A乘以向量B, 如果為正則為A逆時針旋轉向B,否則為順時針,當然這里A轉向B的角總是考慮一個小於180度以內的角。
三.算法的Java實現
看完這段代碼,直接把POJ1113AC了吧。
import java.util.*; class Point implements Comparable { double x; double y; public int compareTo(Object o) {// 按x升序排列,x相同按y升序 Point b = (Point) o; if (this.x > b.x) return 1; else if (this.x == b.x) { if (this.y > b.y) return 1; else if (this.y == b.y) return 0; else return -1; } else return -1; } } public class GiftWrap { Point[] point;// 已知的平面上的點集 boolean[] vis;// 標pointA[i]是否已在凸包中 Queue<Integer> queue = new LinkedList<Integer>();// 點集的凸包, int n; int l; public GiftWrap() { } // 向量ca與ba的叉積 double cross(Point c, Point a, Point b) { return (c.x - a.x) * (a.y - b.y) - (c.y - a.y) * (a.x - b.x); } // 求距離,主要是為了求極點 public double distance(Point p1, Point p2) { return (Math.hypot((p1.x - p2.x), (p1.y - p2.y))); } public void go() { Scanner sc = new Scanner(System.in); n = sc.nextInt(); point = new Point[n + 1]; vis = new boolean[n + 1]; for (int i = 1; i <= n; i++) {// 輸入從1開始 point[i] = new Point(); point[i].x = sc.nextDouble(); point[i].y = sc.nextDouble(); } Arrays.sort(point, 1, point.length - 1);// 注意這個排序從1開始 // 確定一個肯定在凸包上的點 vis[1] = true;// 注意這里將point[1]標記為放進凸包,不過並沒有真的放入隊列 int in = 1;//在凸包上的點 while (true) { int not = -1; for (int i = 1; i <= n; i++) { if (!vis[i]) {// 找一個不在凸包上的點 not = i; break; } } if (not == -1) break;// 找不到,結束 for (int i = 1; i <= n; i++) { /* * 遍歷所有點, 每個點都和現有最外側的點比較,得到新的最外側的點 * 第二個條件是找到極點,不包括共線點 */ if ((cross(point[in], point[i], point[not]) > 0) || (cross(point[in], point[i], point[not]) == 0) && (distance(point[in], point[i]) > distance( point[in], point[not]))) not = i; } if (vis[not]) break;// 找不到最外側的點了 queue.offer(not);// 最外側的點進凸包 vis[not] = true;// 標記這點已放進凸包了 in = not;//in始終表示一個必定在凸包里的點 } double ans = 0; in = 1; /* 最后將point[1]的下標放進凸包 不會重復放入,因為最開始只是把標記下標1在凸包 */ queue.offer(1); /* * 這里有個問題需要弄明白 * 算凸多邊形周長,需要連續的兩個點,下面的是連續的不? * 是的。 * 編號1插入到隊尾,temp先取出的是隊頭,即是最后一個進入凸多邊形的點,顯然和編號1是連接的 * 那會不會重復呢? * 當然不會,因為while循環最后一次是編號1和最先插入的點的距離,此時編號1已經沒了,隊列空了 */ while (!queue.isEmpty()) { int temp = queue.poll();//獲取並移除隊列的頭 ans += Math.hypot((point[in].x - point[temp].x), (point[in].y - point[temp].y)); in = temp; } System.out.println(ans); } public static void main(String args[]) { GiftWrap m = new GiftWrap(); m.go(); } }遺留問題:做完POJ1113時,想起了怎么求所得凸多邊形的外接圓,也就是最小覆蓋圓問題,那么又怎么求內切圓呢?