洛谷P2742 二维凸包

题面

有N个点,求凸包长度,0<=N<=10000

分析

前置知识:向量的外积/叉积,用于判断新加入的点能否与原本构成凸壳。
a = ( x 1 , y 1 ) , b = ( x 2 , y 2 ) \vec{a}=(x1,y1),\vec{b}=(x2,y2)
a × b = x 1 y 2 y 1 x 2 \vec{a}×\vec{b}=x1y2-y1x2
结果的正负性取决于右手定则,垂直纸面向外为正。

凸包的定义是包含所有点的最小凸多边形,可以考虑点都是平面上的钉子,然后用一个有弹性的皮筋从外部围住它们,皮筋会缩紧,使多边形最小,并且最“外”的钉子集合确定了这个图形是凸的。

用的是Andrew算法,求凸壳分为了上下两部分。每新加入一个点,就判断和前两个点的延伸方向是否能构成凸壳,若能则加入;若不能则前两个点后退,直到能将这个新点加入为止(后退而不计算的点被包含在了内部

算法首先将所有点从小到大排序,x为第一关键字,y为第二关键字
从左到右求下凸壳
先加入序列中的前两个点,开始递推。
下面是一般性的图解,如果新加入的点是这样:在这里插入图片描述
下面应该加入P13了,经过 P 12 P 13 × P 11 P 12 < = 0 \vec{P_{12}P_{13}}×\vec{P_{11}P_{12}}<=0 的判定,加入这个点可以构成凸包。(等于0是共线,也满足条件可加入)

加入P14: P 13 P 14 × P 12 P 13 > 0 \vec{P_{13}P_{14}}×\vec{P_{12}P_{13}}>0 ,不符合凸包条件,凸包后退到P11和P12
但是 P 12 P 14 × P 11 P 12 > 0 \vec{P_{12}P_{14}}×\vec{P_{11}P_{12}}>0 仍然成立,继续后退
P 11 P 14 × P 10 P 11 < 0 \vec{P_{11}P_{14}}×\vec{P_{10}P_{11}}<0 ,加入14
在这里插入图片描述
同色的表示进行过判断,现在P12和P13都落入了凸包的内部,凸包路径变成了P9→P10→P11→P14

同理运用一遍,从右到左,即可求出上凸壳(代码中我做了reverse处理,将上凸壳翻转成了下凸壳)

代码

#include<stdio.h>
#include<cstdlib>
#include<iostream>
#include<math.h>
#include<vector>
#include<algorithm>
#include<iomanip>
using namespace std;
struct point
{
	point() {}
	point(double a, double b) :x(a), y(b) {}
	double x, y;
	int dcmp(double x)//误差取等
	{
		if (abs(x) < 1e-10)return 0;else return x < 0 ? -1 : 1;
	}
	bool operator <(const point& other)//为了sort
	{
		if (dcmp(x - other.x) == 0) {//x1=x2
			if (y < other.y)return 1;
			else return 0;
		}
		else
		{
			if (x < other.x)return 1;
			else return 0;
		}
	}
	bool operator == (const point& other)//为了unique
	{
		return dcmp(x - other.x) == 0 && dcmp(y - other.y) == 0;
	}
};
struct Vect//向量
{
	double x, y;
	Vect(point& p1, point& p2) :x(p2.x - p1.x), y(p2.y - p1.y){}//从点构造向量
	double cross(Vect other)//叉积
	{
		return x * other.y - other.x * y;
	}
};
vector<point> v;//存所有点,并进行从小到大排序
int line[10005],ptr=0;//保存凸壳路径
void convex()//求凸壳
{
	int size = v.size();
	for (int i = 0; i < size; i++)//处理i
	{
		while (ptr > 1 && Vect(v[line[ptr-1]],v[i]).cross(Vect(v[line[ptr-2]],v[line[ptr-1]]))>0)
			//求新加入的点连线 与 原本延伸方向的叉积。根据右手定则,如果是正的,则在原本延伸方向的外侧
			ptr--;//后退,直到能与v[i] 形成凸壳
		line[ptr++] = i;//符合,入凸包(用栈结构储存)
	}
}
double dis(point& a, point& b)
{
	return sqrt((a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y));
}
int main()
{
	ios::sync_with_stdio(false);
	int n;
	double x, y;
	cin >> n;
	point temp;
	for (int i = 0; i < n; i++)
	{
		cin >> x >> y;
		temp = point(x, y);
		v.push_back(temp);
	}
	sort(v.begin(), v.end());
	v.resize(unique(v.begin(), v.end()) - v.begin());//去重点
	if (n == 0 || n == 1)cout << "0.00";
	else if (n == 2)cout << fixed << setprecision(2) << dis(v[0], v[1]);
	double ans = 0;

	convex();
	for (int i = 1; i < ptr; i++)
	{
		ans += dis(v[line[i - 1]], v[line[i]]);
	}
	ptr = 0;
	reverse(v.begin(), v.end());//反转,等效于反向求凸壳
	convex();
	for (int i = 1; i < ptr; i++)
	{
		ans += dis(v[line[i - 1]], v[line[i]]);
	}
	cout <<fixed<<setprecision(2)<< ans;
	return 0;
}

参考了刘汝佳的《算法竞赛入门经典-训练指南》

——————————2020.1.31————————————
重构了代码,加入了凸包的介绍

发布了32 篇原创文章 · 获赞 0 · 访问量 1200

猜你喜欢

转载自blog.csdn.net/engineoid/article/details/104120249