算法作业-凸包问题-Graham方法

给定平面上n(n≥3)个点的集合P,求P的一个最小子集Q,使得Q中的点能构成的一个包围P中所有点的多边形。请设计一种贪心算法求解此问题,并证明你所设计的贪心策略的正确性,分析算法的时间复杂度。

解:Graham算法基本思路:

1,选择P中y坐标最小的点为起始点p0,若有多个这样的点则进一步选取其中x坐标最小的点为p0;

2,设<p1,p2,……,pm>是P中剩余的点,对其按逆时针方向相对p0的极角进行从小到大排序,若有多个点有相同的极角,则去掉其余的点,只留下一个与p0距离最远的那个点;

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<Point> outerTrees(Point[] points) {
        return GrahamScan(points);
    }

    private static List<Point> GrahamScan(Point[] points){
        int n = points.length;
        if (n <= 2) return Arrays.asList(points);
        //排序
        Arrays.sort(points,new Comparator<Point>(){
            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<Point> ret = new ArrayList<Point>();
        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 与其他点的极坐标,按照极角的从小到大来排序其他点,排序后可写作<p1, p2 ,…, pm>。这些点中可能没有完全包含原来给出的除了p0 之外的点,那些没有包含进来的点与p0 形成的极角同p0 与这m个点中的某一个点px形成的极角相同,并且到点p0的距离小于px到点p0之间的距离,所以那些没有包含进来的点不会出现在最终的凸多边形的顶点集合中。

下面我们给出一些形式化的定义,以便于证明。令原始的所有的点集用Q 表示,CH(Q)表示GRHAM-SCAN(Q)的结果,即由点集Q 形成的最终的凸多边形的顶点集。令Qi=< p0,p1 ,…, pi >,所以Qm=<p0, p1 ,…, pm>,那么根据上述说明就有:CH(Qm)=CH(Q)。于是,我们只需要证明当该算法结束时,栈S 中包含了CH(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),按逆时针从栈底到栈顶存放顶点。证毕!


好的,以下是 Graham-Scan 算法的 C++ 实现,用于求解凸包问题: ```cpp #include <bits/stdc++.h> using namespace std; struct Point { int x, y; }; // 按照 x 坐标从小到大排序,若 x 坐标相等,则按照 y 坐标从小到大排序。 bool cmp(Point a, Point b) { if (a.x == b.x) return a.y < b.y; return a.x < b.x; } // 计算叉积。 int cross(Point a, Point b, Point c) { return (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x); } // Graham-Scan 算法求解凸包。 vector<Point> grahamScan(vector<Point> &points) { int n = points.size(); if (n <= 1) return points; sort(points.begin(), points.end(), cmp); vector<Point> hull(2 * n); int k = 0; // 构建下凸壳。 for (int i = 0; i < n; ++i) { while (k >= 2 && cross(hull[k - 2], hull[k - 1], points[i]) <= 0) k--; hull[k++] = points[i]; } // 构建上凸壳。 for (int i = n - 2, t = k + 1; i >= 0; --i) { while (k >= t && cross(hull[k - 2], hull[k - 1], points[i]) <= 0) k--; hull[k++] = points[i]; } // 去除重复点。 hull.resize(k - 1); return hull; } int main() { // 测试数据。 vector<Point> points = {{0, 3}, {1, 1}, {2, 2}, {4, 4}, {0, 0}, {1, 2}, {3, 1}, {3, 3}}; vector<Point> hull = grahamScan(points); // 输出凸包的顶点。 for (int i = 0; i < hull.size(); ++i) { cout << "(" << hull[i].x << ", " << hull[i].y << ")" << endl; } return 0; } ``` 注意点: 1. 为了方便起见,我直接使用了 C++11 的新特性,使用 vector 存储点集,如果你使用的是较老的编译器,可以使用数组代替 vector。 2. 实现中为了方便起见,我使用了三个点 $A(a_x,a_y)$、$B(b_x,b_y)$、$C(c_x,c_y)$ 的叉积 $cross(A,B,C)$ 表示向量 $\vec{AB}$ 和 $\vec{AC}$ 的叉积。当叉积 $cross(A,B,C)>0$ 时,表示 $\vec{AB}$ 在 $\vec{AC}$ 的逆时针方向;当叉积 $cross(A,B,C)<0$ 时,表示 $\vec{AB}$ 在 $\vec{AC}$ 的顺时针方向;当叉积 $cross(A,B,C)=0$ 时,表示 $\vec{AB}$ 和 $\vec{AC}$ 共线。 3. 为了避免精度误差,最好使用整数类型存储坐标,如 int 类型。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值