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

二维凸包

假设平面上有 n 个点,需要找到一个周长最小的图形来覆盖所有的点,这个图形就被定义作凸包。

形象地来说,就是在这 n 个点的外侧放上一圈的绳子,然后不断收缩,最终这条绳子会被最外围的点卡住,此时的绳子就是这 n 个点的凸包。

下面,结合模板题,将会讲解求二维凸包的一种常用做法。

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

Luogu P2742

题目背景

upd: 新增一组 hack 数据。

题目描述

农夫约翰想要建造一个围栏用来围住他的奶牛,可是他资金匮乏。他建造的围栏必须包括他的奶牛喜欢吃草的所有地点。对于给出的这些地点的坐标,计算最短的能够围住这些点的围栏的长度。

输入格式

输入数据的第一行是一个整数。表示农夫约翰想要围住的放牧点的数目 n

2 到第 (n+1) 行,每行两个实数,第 (i+1) 行的实数 xi,yi 分别代表第 i 个放牧点的横纵坐标。

输出格式

输出输出一行一个四舍五入保留两位小数的实数,代表围栏的长度。

样例 #1

样例输入 #1

4
4 8
4 12
5 9.3
7 8

样例输出 #1

12.00

提示

数据规模与约定

对于 100% 的数据,保证 1n105106xi,yi106。小数点后最多有 2 位数字。

Solution

求凸包的算法很多,这里只介绍一种:Graham 算法。这种算法的时间复杂度为 O(nlogn),并且瓶颈在于预处理时对点的排序是 O(nlogn) 的,算法主体是 O(n) 的。

Graham 算法首先将会取出一个 y 最小的点来作为凸包的起始点(容易知道这个点一定会在最终的凸包上),然后将其余的点按照相对于起始点的极角从小到大排序。如果有两个点的极角相等,那么将距离近的点排在前面,这样可以让距离远的点在加入栈的时候能够将近的点踢出。

按照排序后的顺序逐个将点加入一个栈中,这个栈存储了目前凸包上所有的点。

加入一个点的时候,先判断当前点加入栈顶后会不会将凸包变成凹包,换句话说,凸包上的点应该满足每两个点之间应该是向左转而非向右转。也就是说将当前点与栈顶的点组成一个向量 vec1,再将栈顶与栈顶的第二个元素组成向量 vec2,如果这两个向量是在右转则就可以踢出栈顶元素,否则就可以加入当前元素了。判断左右转用向量的叉乘即可,当 vec1×vec2>0,则证明是在左转,如果 vec1×vec2<0,则是右转,如果 vec1×vec2=0,则是两向量共线,根据排序规则,共线的时候需要将栈顶踢出来保证最优答案。

这样这个算法流程就很清晰了,可以结合代码理解:

Code

为了方便,我将栈封装了一个结构体,支持 push()pop()size()empty()这些 STL 栈有的操作,并且重载了 [] 运算符,使得可以访问栈中的任意元素,栈顶则是 s[1],栈顶的第二个元素是 s[2]

因为点和向量的表示方法是一致的,所以就使用了同一个结构体,并且重载了 *- 运算,表示向量叉乘和向量减。

#include<bits/stdc++.h>
#define mem(a,b) memset(a,b,sizeof a)
using namespace std;
const int _SIZE=1e5;
struct VEC{
	double x,y;
	double operator* (const VEC &a) const {return x*a.y-a.x*y;}
	VEC operator- (const VEC &a) const {return (VEC){x-a.x,y-a.y};}
}p[_SIZE+5];
int n;
double dis(VEC a,VEC b) {return sqrt(pow(a.x-b.x,2)+pow(a.y-b.y,2));}
double check(VEC a,VEC b) {return a*b;}
struct STACK{
	VEC s[_SIZE+5];int cnt=0;
	void push(VEC x) {s[++cnt]=x;}
	void pop() {cnt--;}
	VEC operator[] (const int &x) {return s[cnt-x+1];}
	bool empty() {return !cnt;}
	void calc(double &ans)
	{
		for (int i=2;i<=cnt;i++)
			ans+=dis(s[i],s[i-1]);
	}
	int size() {return cnt;}
	void print() {for (int i=1;i<=cnt;i++) printf("%.2lf %.2lf\n",s[i].x,s[i].y);puts("");}
};
STACK s;
bool cmp(VEC a,VEC b)
{
	double temp=check(a-p[1],b-p[1]);
	if (temp!=0) return temp>0;
	return dis(a,p[1])<dis(b,p[1]);
}
int main() 
{
	scanf("%d",&n);
	for (int i=1;i<=n;i++)
	{
		scanf("%lf%lf",&p[i].x,&p[i].y);
		if (i!=1 && (p[i].y<p[1].y || (p[i].y==p[1].y && p[i].x<p[1].x))) swap(p[i],p[1]);
	}
	sort(p+2,p+n+1,cmp);
	s.push(p[1]);
	for (int i=2;i<=n;i++)
	{
		while (s.size()>1 && check(s[1]-s[2],p[i]-s[1])<=0) s.pop();
		s.push(p[i]);
	}
	s.push(p[1]);//计算答案的时候记得将第一个点作为凸包终点加入栈中进行统计
	double ans=0;
	s.calc(ans);
	printf("%.2lf\n",ans);
	return 0;
}
posted @   Hanx16Msgr  阅读(60)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
点击右上角即可分享
微信分享提示