写这道题之前我们,先了解一下是什么是凸包问题。
凸包:在一个实数向量空间V中,对于给定集合X,所有包含X的凸集的交集S,被称为X的凸包。
简单的解释就是,给出一些点。找到从这些点中,选出一些点,让所有点都在这些点范围内。
在上面图中,p1,p2,p8,p7,p6组成的集合为这些点的凸包。
解决凸包问题常用的有两种算法:Graham扫描法和Jarvis步进法。
这篇文章我们先介绍 Jarvis步进法。
Jarvis步进法又称为卷包裹法。为什么这样叫呢?因为Jarvis步进法的原理和卷包裹这几个字很像。
卷包裹法的原理十分简单:首先先找到一个最边缘的点(一般是最下方的,如果有多个点,选择最右方的点)。我们假设有一个绳子,以最边缘的点向右逆时针旋转直到碰到另一个点为止,此时就找到了凸包的一条边,然后用新找到的点作为端点,继续旋转绳子,找到下一个点,重复这个过程直到围成一个凸多边形,即可得到这个点集的凸包。卷包裹法的时间复杂度为O(n²)。
原理十分简单,但是怎么实现是个问题。
我们先把原理的步骤写出来,然后再慢慢的转化成代码。
Step1:选择点集中最下面的点,如果有多个,则选择最下面的点中最左边的一个,所选择的点是凸包的第一个点。
Step2:以水平向右的方向作为初始射线方向,逆时针旋转,选择第一条初始射线之上的射线作为当前射线,当前射线经过凸包的第二个点。
Step3:以当前射线基准,继续逆时针旋转找到最靠近该射线的一条射线,从而找到凸包的另一个点。把这条射线作为当前射线,这个过程一直继续,知道回到第一个点,全程过程如下图所示:
如果把P1P2当成射线,那么”寻找到另一条射线“这一步骤,可以理解为以点P2为中心对P1P2进行逆时针旋转,直至碰到另一个点Pi,此时P2Pi为新的射线,也是凸多边形的一条边。但是程序中是不可能实现”旋转“这一操作。于是我们有如下几种操作实现旋转:
1,把每一个射线与其他 n - 2条射线比较,每步比较的效率为O(n²)
2,通过计算各射线与射线P1P2的夹角,效率为O(n)但是存在浮点误差
3,使用叉积来求各条射线斜率的相对关系,从而求得另一条射线,效率为O(n),不存在误差。
步骤写好了我们来考虑各步骤的代码实现。
先定义一下点这个结构体
struct pint{
int x,y;
}
第一步要取出最下边最左端的点,所以我们要对点集进行排序
bool cmp(pint a,pint b){
return (a.y < b.y || (a.y == b.y && a.x < b.x));
}
然后我们要进行旋转过程,我们旋转用叉积来判断各个射线的斜率相对关系。
若向量p1p3 × 向量 p1p2 > 0 则向量p1p3 在 向量 p1p2 的顺时针方向
若向量p1p3 × 向量 p1p2 > 0 则向量p1p3 在 向量 p1p2 的逆时针方向
若向量p1p3 × 向量 p1p2 == 0 则向量p1p3 和 向量 p1p2 共线。
bool CrossLeft(pint p1,pint p2,pint p3){
return ((p3.x - p1.x) * (p2.y - p1.y) - (p2.x - p1.x) * (p3.y - p1.y)) < 0;
}
CrossLeft这个函数的意思是若向量p1p3 在 向量p1p2逆时针方向 则返回真 ,否则返回假
然后就是 Jarvis步进法的核心代码,去实现旋转的过程。
void Jarvis(){
tail = cnt = 0;
sort(x,x+n,cmp);
sta[tail++] = 0;
sta[tail++] = 1;
for(int i = 2 ; i < n ; i ++){
while(tail > 1 && ! CrossLeft(x[sta[tail - 1]],x[sta[tail - 2]],x[i]))
tail --;
sta[tail++] = i;
}
for(int i = 0 ; i < tail ; i ++) ans[cnt++] = sta[i];
tail = 0 ;
sta[tail ++] = n - 1;
sta[tail ++] = n - 2;
for(int i = n - 3 ; i >= 0 ; i --){
while(tail > 1 && !CrossLeft(x[sta[tail - 1]],x[sta[tail - 2]],x[i]))
tail --;
sta[tail ++] = i;
}
for(int i = 0 ; i < tail ; i ++)
ans[cnt++] = sta[i];
}
来详细的分析一波,上述代码中 ans【】来存结果。sta【】数组表示凸包的一部分边。为什么是一部分呢?这里sta其实是一个栈。
首先在上述代码中我们先把第一个点 和第二个点放入 sta中,作为第一条射线。
然后判断 点i 和和当前线段的位置。若是最接近的当前射线的射线,那么其他射线就会全部在他的左边(即全部在这条射线的逆时针方向)。如果不是,那么总会存在一条射线在他顺时针方向。
进行两边的原因是,一次卷包裹只能覆盖一半,所以需要反过来,再卷一次。
不理解的话,大家可以画画图自己用笔模拟一下。
那现在看一下POJ 1113
题目链接:http://poj.org/problem?id=1113
题意:国王想把自己的城市围起来,但是城市到城墙的的最小距离为 l。求个城墙的最小周长。
题解:和裸的凸包问题不太一样。他需要让城市到城墙的距离为l,但是不难证明 这道题的答案是 凸包周长加上一个半径为l的圆的周长。因为要求的周长只是比凸包的周长多了几段圆弧,而所有圆弧的圆心角和为360°。
代码如下:
#include<iostream>
#include<algorithm>
#include<cmath>
#include<cstdio>
using namespace std;
#define size 1000
struct pint{
int x,y;
}x[size];
bool cmp(pint a,pint b){
return (a.y < b.y || (a.y == b.y && a.x < b.x));
}
int n,l,ans[size],cnt,sta[size],tail;
bool CrossLeft(pint p1,pint p2,pint p3){
return ((p3.x - p1.x) * (p2.y - p1.y) - (p2.x - p1.x) * (p3.y - p1.y)) < 0;
}
void Jarvis(){
tail = cnt = 0;
sort(x,x+n,cmp);
sta[tail++] = 0;
sta[tail++] = 1;
for(int i = 2 ; i < n ; i ++){
while(tail > 1 && ! CrossLeft(x[sta[tail - 1]],x[sta[tail - 2]],x[i]))
tail --;
sta[tail++] = i;
}
for(int i = 0 ; i < tail ; i ++) ans[cnt++] = sta[i];
tail = 0 ;
sta[tail ++] = n - 1;
sta[tail ++] = n - 2;
for(int i = n - 3 ; i >= 0 ; i --){
while(tail > 1 && !CrossLeft(x[sta[tail - 1]],x[sta[tail - 2]],x[i]))
tail --;
sta[tail ++] = i;
}
for(int i = 0 ; i < tail ; i ++)
ans[cnt++] = sta[i];
}
int main(){
scanf("%d %d",&n,&l);
for(int i = 0 ; i < n ;i ++) scanf("%d%d",&x[i].x,&x[i].y);
Jarvis();
double re = 4 * acos(0.0) * l;
for(int i = 0 ; i < cnt - 1 ; i ++){
re += sqrt((x[ans[i]].x - x[ans[i+1]].x) * (x[ans[i]].x - x[ans[i+1]].x) + (x[ans[i]].y - x[ans[i+1]].y) * (x[ans[i]].y - x[ans[i+1]].y)) *1.0;
}
printf("%.0f\n",re);
return 0 ;
}