(凸包问题)P2742 [USACO5.1] 圈奶牛Fencing the Cows /【模板】二维凸包

解题思路

这是一道二维凸包的模板问题

首先我们要做点准备工作

存点
struct Point
{
	double x,y;
	Point(){}
	Point(int x,int y): x(x) , y(y){}
	bool operator< (Point B){
		if(B.x == x) return B.y > y;
		return B.x > x;
	}
	Point operator- (Point B){return Point(x - B.x , y - B.y);}
};

注意一下重载运算符的部分,这里的<是为了排序使用,后续会说明原因
这里的减法运算是为了利用两个点来构造向量

判断
const int eps = 1e-6;
int sgn(double x)
{
	if(fabs(x) <= eps) return 0;
	return (x>=0 ? 1 : -1);
}

由于涉及到实数运算,并且计算机处理实数运算操作的时候可能会产生误差,所以不能单纯的通过它本身的运算来进行判断,要设置一个误差,具体是多少可以根据题目要求来定

叉乘
double Cross(Point a,Point b){return a.x * b.y - a.y * b.x;}

计算几何问题绕不开的就是这些关于向量的运算,其中叉乘也是非常基础且关键的运算,本题用到的算法就是以这个为基础的

距离
double dis(Point a,Point b){
	return sqrt((b.x - a.x) * (b.x - a.x) + (b.y - a.y) * (b.y - a.y));
}

本题的目标是围栏的长度

介绍一下核心算法

本题的核心算法叫做Andrew算法

主体思想: 把整个凸包分为上半部分和下半部分,遍历两次得到答案

首先我们要所有的点进行排序,按照x从小到大的顺序排列,如果x相同,按照y从小到大的顺序排序
简单一想你就会发现,这样做第一个肯定是凸包左下角的那个点,最后一个肯定是凸包右上角的那个点
那么聪明的你此时肯定能想到从头到尾遍历一遍,从左下角到右上角,这样下凸包就有了,再从后往前遍历一遍,从右上角到左下角,一个上凸包就有了,啊哈,一整个凸包就出来了

当想到这的时候已经兴致冲冲的开始准备写了,我觉得应该不只有这一种方法判断,但是接下来要讲的就是这个Andrew算法怎么构造上下凸包

这就要用到叉乘的性质
比如说Cross(Point a, Point b) //解释一下这里的Point就是一个向量了
b向量如果在a向量左边,那么Cross的结果就大于0
反之则小于0

结合代码可能更好理解

	v[k++] = p[0];
	for(int i=1;i<n;i++){
		while (k > 1 && sgn(Cross(v[k-1] - v[k-2] , p[i] - v[k-1])) <= 0)
			k--;
		v[k++] = p[i];
	}

在这里插入图片描述

以图片为例,我们想要找到下凸包的所有点,就要从左下角的点开始
不断加入点,假设此时加入了1和2点,判断3点的时候,向量1->2 和向量2->3的叉乘应该是大于0的,这就表明加上这个3点,前进方向是往上的,一直这样下去就可以到右上角就可以出现一个下凸包
接下来到4点问题就出现了
向量3->4和向量2->3的叉乘就小于0了,前进方向就变得很奇怪了,因为如果出现了这样的情况,说明前面一定有点和这个4点构成的前进方向是正常的,所以我们就要把3去掉了,再去判断向量2->4和向量1->2,还是一样,所以继续去掉2点,最后就留下了1和4点,从下凸包的角度看,2和3就在包围圈里了
简而言之,就是通过前进方向来判断这个点是不是凸包上的点

上凸包一个原理,这里不过多赘述了,看代码即可

最后求出记录下的点构成的多边形的周长就行了

总代码

代码看着很长,是我想写的更有条理一些,实际上已经被分解出来解读完了

#include<bits/stdc++.h>
using namespace std;
const int eps = 1e-6;
const int N = 1e5 + 5;
struct Point
{
	double x,y;
	Point(){}
	Point(int x,int y): x(x) , y(y){}
	bool operator< (Point B){
		if(B.x == x) return B.y > y;
		return B.x > x;
	}
	Point operator- (Point B){return Point(x - B.x , y - B.y);}
}v[N],p[N];
int sgn(double x)
{
	if(fabs(x) <= eps) return 0;
	return (x>=0 ? 1 : -1);
}
double Cross(Point a,Point b){return a.x * b.y - a.y * b.x;}
double dis(Point a,Point b){return sqrt((b.x - a.x) * (b.x - a.x) + (b.y - a.y) * (b.y - a.y));}
int n,k;
double ans;
void Andrew()
{
	v[k++] = p[0];
	for(int i=1;i<n;i++){
		while (k > 1 && sgn(Cross(v[k-1] - v[k-2] , p[i] - v[k-1])) <= 0)
			k--;
		v[k++] = p[i];
	}
	
	int t = k;
	for(int i=n-2;i>=0;i--){
		while (k > t && sgn(Cross(v[k-1] - v[k-2] , p[i] - v[k-1])) <= 0)
			k--;
		v[k++] = p[i];
	}
	if(n > 1) k--;
}
void Distance()
{
	for(int i=0;i<k;i++){
		ans += dis(v[i],v[(i+1)%k]);
	}
}
void solve()
{
	Andrew();
	Distance();
	printf("%.2f",ans);
}
int main()
{
	cin >> n;
	for(int i=0;i<n;i++) cin >> p[i].x >> p[i].y;
	sort(p,p+n);
	// for(int i=0;i<n;i++) cout << p[i].x << " " << p[i].y << endl;
	solve();
}
  • 36
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值