在说这个题目之前,我想给大家介绍一些这几天我了解到的有关凸包的知识:
1、Gift-Wrapping(卷包裹算法)
这个算法在《算法艺术》上说的很清楚了(p391-393),如果理解的还不是很清楚,在这里讲解的特别好,特别清楚,由于这个很简单,所以就不谈论它了。我对这个算法的理解是:时间复杂度是O(N*H),N是点的个数,H是在凸包上点的个数。所以卷包裹算法很适合凸包上的点很少的时候,通常情况下,随机数据会很快。但是如果构造出的凸包上的点很多的时候,它就会很慢了,不如说,所有点都在一个园上的时候。
2、Graham-Scan算法
《算法艺术》(p393-396)。我觉得上面讲的很详细很详细了。同样,如果理解的不好可以看看这里。
这个算法是基于一个有序的点,所以必须要排序的。关键就在这里,排序分两种:极角排序和水品排序。估计大多数人都会第一种。
先说第一种。首先要找到一个一定在凸包上的点p0,这个点怎么找?自己想想吧。然后就以这个点为基点,对所有点按照极角大小排序。这里注意一下,如果极角相等,那么就按距离从小到大排序。这个是防止共线问题,关于这个下面好好讨论。然后把排序后的p0,p1,p2放入栈中,接着就是遍历每个点了,始终保证非“右手”方向就好了,具体实现去网上搜,很多。
然后就是第二种的水平序。排序准则是先按y大小排序,如果y相等,就按x排序。相对于第一种,这个排序简单的多。然后和上面的思想一样,只不过要分两步,右链和左链,先做右链,从0到排序最后点,然后再反过来,进行左链。具体的细节可以参考下面的代码。可能你在想这样有什么好处呢,我要说的是,这样可以很完美的解决共线问题。
我对这个算法的理解是:对于随机数据,可能没有卷包裹算法快。但是也是可以接受的。之所以喜欢的原因是,它可以完美的解决共线这个一直让人头大的问题。
3、Melkman算法
首先要说的是:很多人都认为这个是最好的算法。这个算法可以在个点有序的前提下,每次获得一个点就可以将先前的凸包改造成新的凸包,因此,这个是一个在线算法,它有着其他算法无法比拟的优势。1987年Melkman提出的的凸包算法,它不再使用堆栈了,转而使用双向表,这为凸包算法的历史掀开了崭新的一夜。
具体实现我就不说了,相信看过上面几句话的,现在都已经忍不住要学习了。
今天看了一下午《算法艺术》和网上的一些资料,终于又有了一些理解,那就补充一下吧。之前都是模模糊糊的,现在明白了,可能我现在理解的还是错误的,但是我还是要说出来:Melkman算法的前提是“各个点有序”。所以melkman是用来求简单多边形的凸包算法,可以在线性时间求出最小凸包。而其他两种则可以求点集的凸包的算法。如果要用Melkman算法来求点集的凸包,那么首先也是要排序的,通过排序可以形成一个简单的多边形,然后才可以在线性的时间求出最小凸包。
所以最终得到的结论是:求点集的凸包,时间复杂度的底线是O(nlogn)。
最后讨论一下共线的问题:
假设有这么几个点
0 0
0 1
0 2
2 2
1 1
和这几个点
1 1
2 2
3 3
4 4
对于上面的两组数据,如果用卷包裹算法和Graham-Scan的极角排序法做,要求只输出凸包上的定点,会出现什么样的问题??
如果用Graham-Scan的水平序来写,会不会出现同样的问题??
大家可以好好想想!!!!
还有很多凸包方面的算法,比如Jarvis步进法、增量、溶解、QuickHull等,这些多用于数学中,实践意义不大,所以就不说了。有兴趣了解凸包算法的发展史,可以看看蓝点大神的《漫话二维凸包》。。
现在说poj1113 wall:
题目:poj1113 wall
意思就是给一个城堡的墙角的坐标,让你用最短的围墙围起来,并且围墙离城堡的距离不能少于L。
说白了就是一个很裸的凸包问题。对于那个不能小于L,等作出了凸包,然后把边向外移动L,自己画画就看出来了,每个角处的弧,加起来刚好是一个半径为L的圆,所以最终结果就是圆的周长加上凸包的周长。
值得注意的是:由于精度问题,对于那些共线的点,应该取最远处那个计算长度,不然可能会一直WA,这个就体现了Graham-Scan算法的水平序的优势了。大家好好品味。。
比如上面提到的第一组数据,计算的顶点应该是0 0,0 2和2 2,而不是所有的点,即使所有的点都在凸包上,如果计算所有的点,误差就会大很多,肯能就是一直WA的。
代码如下:
//突然想用类写个模板,由于c++没学好,所以弄了一上午才弄万,不好的地方欢迎大家指点。。。。。 #include <stdio.h> #include <iostream> #include <algorithm> #include <string.h> #include <math.h> using namespace std; const int N=1100; struct Node{ int x,y; bool operator<(Node a)const{ return y<a.y||(y==a.y&&x<a.x); } }; class Graham_Scan{ public: Graham_Scan(int r){ num_node=r; for(int i=0;i<=num_node;i++) visit[i]=true; } bool judg(){//判断给的点是否符合条件能够成凸包 if(num_node<2) return 0; } void init(); void fun(); void print_node(bool jud);//输出凸包上的点,如果jud真就输出所有点,否则就输出定点 double print_per();//输出凸包的周长 private: int num_node,stack_all[N],stack[N],top_all,top; Node node[N]; double dis(Node a,Node b); void Graham_scan();//Graham_Scan算法水平序的实现 bool visit[N]; int turn(int a,int b,int c); }; void Graham_Scan::init() { for(int i=0;i<num_node;i++) scanf("%d%d",&node[i].x,&node[i].y); } double Graham_Scan::dis(Node a,Node b) { double c; c=(b.x-a.x)*(b.x-a.x)+(b.y-a.y)*(b.y-a.y); return sqrt(c); } int Graham_Scan::turn(int a,int b,int c) { return (node[b].x-node[a].x)*(node[c].y-node[a].y)-(node[b].y-node[a].y)*(node[c].x-node[a].x); } void Graham_Scan::fun() { Graham_scan(); } void Graham_Scan::Graham_scan() { int i; sort(node,node+num_node); stack_all[0]=stack[0]=0; stack_all[1]=stack[1]=1; top=top_all=1; visit[1]=false; //执行右链,同时标记已经在右链上的点 for(i=2;i<num_node;i++) { while(top>0&&turn(stack[top-1],stack[top],i)<=0) top--; stack[++top]=i; while(top_all>0&&turn(stack_all[top_all-1],stack_all[top_all],i)<0) { visit[stack_all[top_all]]=true; top_all--; } stack_all[++top_all]=i; visit[i]=false; } //现在的top,top_all点一定是最上边,最右边的那个点 //执行左链,逃过在右链上已经在凸包上的点,看别人写的时候没有跳过,不过算法书上说是要跳过的,所以还是跳过吧 int top1=top,top1_all=top_all; stack[++top]=num_node-2; stack_all[++top_all]=num_node-2; for(i=num_node-3;i>=0;i--) { while(visit[i]&&top>top1&&turn(stack[top-1],stack[top],i)<=0) top--; stack[++top]=i; while(visit[i]&&top_all>top1_all&&turn(stack_all[top_all-1],stack_all[top_all],i)<0) top_all--; stack_all[++top_all]=i; } //现在的top,top_all点一定是0点 } void Graham_Scan::print_node(bool jud) { if(jud) { for(int i=0;i<top_all;i++) cout<<node[stack_all[i]].x<<" "<<node[stack_all[i]].y<<endl; } else for(int i=0;i<top;i++) cout<<node[stack[i]].x<<" "<<node[stack[i]].y<<endl; } double Graham_Scan::print_per() { double ans=0.0; for(int i=1;i<=top;i++) { ans+=dis(node[stack[i]],node[stack[i-1]]); } return ans; } int main() { int n,l; while(~scanf("%d%d",&n,&l)) { double ans=acos(-1.0)*2*l; Graham_Scan solve(n); if(solve.judg()) { cout<<"No\n"<<endl; continue; } solve.init(); solve.fun(); ans+=solve.print_per(); printf("%0.lf\n",ans); } }
参考资料:
《算法艺术与信息学竞赛》 刘汝佳 黄亮
《算法导论》 Thomas H.Cormen、Charles E.Leiserson、Ronald L.Rivest、Clifford Stein 潘金贵 顾铁成等人译