给定平面上n(n≥3)个点的集合P,求P的一个最小子集Q,使得Q中的点能构成的一个包围P中所有点的多边形。请设计一种贪心算法求解此问题,并证明你所设计的贪心策略的正确性,分析算法的时间复杂度。
解:Graham算法基本思路:
1,选择P中y坐标最小的点为起始点p0,若有多个这样的点则进一步选取其中x坐标最小的点为p0;
2,设
3,设排序后的点的顺序为p1,p2,……,pm,以向量p0p1与p1p2的叉积方向为z轴正方向,依次判断向量pipi+1与向量pi+1pi+2(1<=i<=m-1)的叉积方向是否为正方向。若为正方向,则pi+1为凸包上的点,否则,pi+1是凸包内的点。(这个是我自己的理解,其他博客里大多数写的是判断向量的左转,其实是一个道理,感觉自己的更好理解)
算法的时间复杂度为O(nlogn),n是点的总个数。
Java代码如下:
package convex_hull;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
public class Graham {
public static void main(String[] args) {
// TODO Auto-generated method stub
Point p1 = new Point(5,1,"A");
Point p2 = new Point(1,1,"B");
Point p3 = new Point(2,0,"C");
Point p4 = new Point(3,1,"D");
Point p5 = new Point(4,2,"E");
Point p6 = new Point(3,3,"F");
Point p7 = new Point(2,4,"G");
Point p8 = new Point(2,2,"H");
Point[] points = new Point[]{p1,p2,p3,p4,p5,p6,p7,p8};
System.out.println(outerTrees(points));
}
public static List outerTrees(Point[] points) {
return GrahamScan(points);
}
private static List GrahamScan(Point[] points){
int n = points.length;
if (n <= 2) return Arrays.asList(points);
//排序
Arrays.sort(points,new Comparator(){
public int compare(Point o1, Point o2) {
return o1.y != o2.y ? o1.y - o2.y : o1.x - o2.x;
}
});
int[] stack = new int[n+2];
int p = 0;
//一个O(n)的循环
for (int i = 0; i < n; i++) {
while (p >= 2 && cross(points[stack[p - 2]], points[i], points[stack[p - 1]]) > 0)
p--;
stack[p++] = i;
}
int inf = p + 1;
for (int i = n -2; i >= 0; i--){
if (equal(points[stack[p-2]], points[i])) continue;
while (p >= inf && cross(points[stack[p-2]], points[i], points[stack[p-1]]) > 0)
p--;
stack[p++] = i;
}
int len = Math.max(p - 1, 1);
List ret = new ArrayList();
for (int i = 0; i < len; i++){
ret.add(points[stack[i]]);
}
return ret;
}
private static int cross(Point o, Point a, Point b){
return (a.x-o.x)*(b.y-o.y) - (a.y - o.y) * (b.x - o.x);
}
private static boolean equal(Point a, Point b){
return a.x == b.x && a.y == b.y;
}
}
package convex_hull;
public class Point {
public String name;
public int x;
public int y;
public Point(int i, int j, String s) {
// TODO Auto-generated constructor stub
x = i;
y = j;
name = s;
}
@Override
public String toString() {
// TODO Auto-generated method stub
return name;
}
}
随机输入8个点{(5,1,"A"),(1,1,"B"),(2,0,"C"),(3,1,"D"),(4,2,"E"),(3,3,"F"),(2,4,"G"),(2,2,"H")}对代码进行实例测试,得出的凸包上的点输出如下:
形状如下图所示:
(A,E,F,G四个点在一条直线上)
证明:在GRAHAM_SCAN 算法中,第一步是先找y 坐标最小的点p0,如果有多个点的y坐标并列最小,则取最左边的点记为p0。在第二步中,计算点p0 与其他点的极坐标,按照极角的从小到大来排序其他点,排序后可写作
下面我们给出一些形式化的定义,以便于证明。令原始的所有的点集用Q 表示,CH(Q)表示GRHAM-SCAN(Q)的结果,即由点集Q 形成的最终的凸多边形的顶点集。令Qi=< p0,p1 ,…, pi >,所以Qm=
我们采用循环不变量的方式来证明,我们给出循环不变式:在执行程序的for 循环之前,栈S 恰好包含了CH(Qi-1),并且按逆时针顺序从栈底到栈顶存放。
初始:该算法在for 循环之前,将p0,p1,p2 依次压栈,这三个顶点形成了它们自己的凸多边形,栈S 中恰好存放了CH(Q2)中的顶点,并且按逆时针顺序从栈底到栈顶存放,所以初始时刻,循环不变式是保持的。
保持:在新的循环开始时,栈顶存放的顶点是pi-1。在for 循环中的while 循环进行之后,pi 压栈之前,栈顶为pj,pk 为紧靠栈顶的下一点,则此时栈S 与第j 次循环结束后的情况一样,所以按照循环不变式,此时栈S 中恰好包含了CH(Qj)中的顶点,并且按逆时针顺序从栈底到栈顶存放。
在点pi 未压栈之前:pi 与p0 形成的极角比pj 与p0 形成的极角大,并且∠pkpjpi 满足向左转的条件,否则点pj 就被弹栈了,并且栈S 中此时为CH(Qj)。所以当压栈pi 后,此时栈S 中即为CH(Qj∪{pi}),并且是按逆时针从栈底到栈顶存放的。
下面我们来证明CH(Qj∪{pi})=CH(Qi)。假设pt 是在第i 次for 循环中被弹出的某一点,在栈中紧靠pt 的下一点假设为pr,则∠prptpi 是非左转的,而且pt 与p0 所形成的极角介于pr 与pi 之间,所以pt 在△p0prpi 中或者是边prpi 上,所以pt 是位于由Qi 其他的点形成的三角形或三角形的边上的,所以它不可能出现在CH(Qi)中,所以CH(Qi-{pt})=CH(Qi)。假设Pi 为第i次for 循环所弹出的所有点的集合,则把式CH(Qi-{pt})=CH(Qi)不断的应用于Pi 的每一个点,就有CH(Qi-Pi)=CH(Qi)。而Qi-Pi = Qj∪{pi},所以CH(Qj∪{pi})=CH(Qi)。我们已经证明了当pi 压入栈S 中时,S 中按逆时针从栈底到栈顶存放了CH(Qi)。当i 增加时会进行下一次的for 循环,而且循环不变式仍成立。
终止:当i=m+1 时,循环终止,此时栈S 中的所有点为CH(Qm),即为CH(Q),按逆时针从栈底到栈顶存放顶点。证毕!