Andrew算法求二维凸包-学习笔记
凸包的概念
首先,引入凸包的概念:
(有点窄的时候...图片右边可能会被吞,拉开图片看就可以了)
大概长这个样子:
那么,给定一些散点,如何快速地求出凸包呢(用在凸包上的点来表示凸包)
Andrew算法流程和思想
常见的求凸包的算法有$Graham$和$Andrew$,$Andrew$是$Graham$扫描算法的变种,和$Graham$相比,$Andrew$更快,且更稳定,所以主要讲一下$Andrew$。
首先把所有点以$x$坐标为第一关键字,$y$坐标为第二关键字从小到大进行排序,可以肯定第一个点和最后一个点在答案中。
接下来用以下的例子来帮助理解算法流程:
第一次,把$1$和$2$加入答案中
尝试把$3$加进去,发现凹进去了,所以把$2$丢掉,把$3$放进去
我们来看看$2$被丢掉,$3$成功上位的原因(凹进去的原因):
发现是斜率(或者...可以总结成叉积?
在下图中$1->2$的斜率大于$1->3$的斜率,又因为之前按$x$递增排序,所以可以说明$2$在$3$的左上,所以是凹进去的。
换句话说,如果加进去这个点(即当前点,记为$i$)和$i-2$号点的斜率小于$i$号点和$i-1$号点的斜率,那么就要把$i-1$号点去掉并加入$i$号点来维护凸包的性质(不让它凹进去)
接下来加入$4$,$1->4$斜率大于$1->3$斜率,所以$3$不用被丢掉。
加入$5$,$3->5$斜率小于$3->4$斜率,所以丢掉$4$,加入$5$
加入$6$,一样的理由,一样的操作。($3->6$斜率小于$3->5$斜率,丢掉$5$,加入$6$)
然后发现$3$那个地方也凹进去了($1->6$的斜率小于$1->3$的斜率)
所以$3$也要被丢掉,然后只剩下两个点:
(所以写代码的时候要用$while$)
接着来,加入$7$:
然后是$8$,发现...斜率只小一点点(图没画好,这***钻的角度,将就看一下吧...),所以$7$要删掉
不过也顺便解决一个共线的问题,共线嘛,很好解决,反正两个点都在凸包上,都不丢就可以了,后面如果那一条线不属于凸包的话,用$while$丢点的时候两个点斜率是一样的,总会被丢出去的。
然后是$9$,发现斜率小,所以丢掉$8$:
啊哈,然后发现所有点都已经遍历完了,成功达到了$9$,可是凸包还有一半呢。
倒着再来一次就可以求出上面那个盖盖了:
(下面放流程图,不一一解说了(好累),操作是一样的)
(把$7$悄悄地挪了一下位置)
(丢掉$6$,发现斜率的关系和正着的那一次都一样,都是小于)
这样, 凸包就求出来啦!
按照以上的思路写代码就可以啦。
例题& 板子
1 /* 2 ID: Starry21 3 LANG: C++ 4 TASK: fc 5 */ 6 #include<cstdio> 7 #include<algorithm> 8 #include<vector> 9 #include<cstring> 10 #include<queue> 11 #include<map> 12 #include<iostream> 13 #include<cmath> 14 using namespace std; 15 #define ll long long 16 #define INF 0x3f3f3f3f 17 #define N 10005 18 struct node{ 19 double x,y; 20 }; 21 node p[N],s[N]/*凸包上的点*/; 22 int n; 23 double dis(node a,node b) 24 { 25 return sqrt(((a.x-b.x)*(a.x-b.x))+((a.y-b.y)*(a.y-b.y))); 26 } 27 bool cmp(node a,node b) 28 { 29 if(a.x==b.x) return a.y<b.y; 30 return a.x<b.x; 31 } 32 double getk(node a,node b) 33 { 34 if(a.x==b.x) return INF;//在一条竖线上 斜率看成无限大 35 return (b.y-a.y)/(b.x-a.x); 36 } 37 double Andrew() 38 { 39 sort(p+1,p+n+1,cmp); 40 int cnt=0,tot=0; 41 double sum=0.0; 42 for(int i=1;i<=n;i++) 43 { 44 s[++cnt]=p[i]; 45 while(cnt>=3&&getk(s[cnt-2],s[cnt])<getk(s[cnt-2],s[cnt-1])) 46 s[cnt-1]=s[cnt],cnt--; 47 } 48 for(int i=1;i<=cnt-1;i++) 49 sum+=dis(s[i],s[i+1]); 50 tot=cnt; 51 cnt=0; 52 for(int i=n;i>=1;i--) 53 { 54 s[++cnt]=p[i]; 55 while(cnt>=3&&getk(s[cnt-2],s[cnt])<getk(s[cnt-2],s[cnt-1])) 56 s[cnt-1]=s[cnt],cnt--; 57 } 58 for(int i=1;i<=cnt-1;i++) 59 sum+=dis(s[i],s[i+1]); 60 tot+=cnt; 61 tot-=2;//tot是凸包上点的个数 62 //printf("%d\n",tot); 63 return sum; 64 } 65 int main() 66 { 67 //freopen("fc.in","r",stdin); 68 //freopen("fc.out","w",stdout); 69 scanf("%d",&n); 70 for(int i=1;i<=n;i++) 71 scanf("%lf %lf",&p[i].x,&p[i].y); 72 printf("%.2lf\n",Andrew()); 73 return 0; 74 }
还有一个用叉积写的,原理都是一样的, 不过我自己不是很喜欢这种写法:
1 /* 2 ID: Starry21 3 LANG: C++ 4 TASK: shuttle 5 */ 6 #include<cstdio> 7 #include<algorithm> 8 #include<vector> 9 #include<cstring> 10 #include<queue> 11 #include<map> 12 #include<iostream> 13 #include<cmath> 14 using namespace std; 15 #define ll long long 16 #define INF 0x3f3f3f3f 17 #define N 10005 18 struct node{ 19 double x,y; 20 }; 21 node p[N],s[N]/*凸包上的点*/; 22 int n; 23 double dis(node a,node b) 24 { 25 return sqrt(((a.x-b.x)*(a.x-b.x))+((a.y-b.y)*(a.y-b.y))); 26 } 27 bool cmp(node a,node b) 28 { 29 if(a.x==b.x) return a.y<b.y; 30 return a.x<b.x; 31 } 32 bool Cross(node a,node b,node c) 33 { 34 double x1=a.x-b.x,y1=a.y-b.y; 35 double x2=c.x-b.x,y2=c.y-b.y; 36 if((x1*y2-x2*y1)<=0) return 0; 37 //如果不希望在凸包的边上有输入点。把<=改成< 38 return 1; 39 } 40 int Andrew() 41 { 42 sort(p+1,p+n+1,cmp); 43 int num=0; 44 for(int i=1;i<=n;i++) 45 { 46 while(num>1&&!Cross(s[num-1],s[num-2],p[i])) 47 num--; 48 s[num++]=p[i]; 49 } 50 int tmp=num; 51 for(int i=n-1;i>=1;i--) 52 { 53 while(num>tmp&&!Cross(s[num-1],s[num-2],p[i])) 54 num--; 55 s[num++]=p[i]; 56 } 57 if(n>1) num--; 58 return num; 59 } 60 int main() 61 { 62 //freopen("shuttle.in","r",stdin); 63 //freopen("shuttle.out","w",stdout); 64 scanf("%d",&n); 65 for(int i=1;i<=n;i++) 66 scanf("%lf %lf",&p[i].x,&p[i].y); 67 int num=Andrew(); 68 double sum=0; 69 for(int i=1;i<=num-1;i++) 70 sum+=dis(s[i],s[i+1]); 71 sum+=dis(s[num],s[1]);//还有第n个点到第1个点的距离 72 printf("%.2lf",sum); 73 return 0; 74 }