计算几何---凸包教学 (附带POJ 1113 - Wall 题解)

写这道题之前我们,先了解一下是什么是凸包问题。

凸包:在一个实数向量空间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 ;
}

 

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值