~凸包(二维板子)~(结合各位大佬的文章总结的)

什么是凸包?

打个比方,有许多散乱木桩,问你怎样用最少的木桩把所有木桩都围起来(如下图):
凸包
大家可以思考一下,这之前有一些小知识需要大家提前了解下(当然可能只有我不会

小知识:

1)点与直线的关系:
行列式
(坐标:p1(x1,y1),p2(x2,y2),p3(x3,y3))

  1. 当上式结果为正时,p3在直线 p1p2 的左侧;当结果为负时,p3在直线 p1p2 的右边。

  2. 对上式的结果取绝对值,绝对值越大,则距离直线越远。

2)夹角公式:
夹角公式
一般用函数antan2()来计算夹角(头文件#include<math.h>),atan2(y,x)求的是y/x的反正切。

atan2返回值解释:
在三角函数中,两个参数的函数atan2是正切函数的一个变种。对于任意不同时等于0的实参数x和y,atan2(y,x)所表达的意思是坐标原点为起点,指向(y,x)的射线在坐标平面上与x轴正方向之间的角的角度度。当y>0时,射线与x轴正方向的所得的角的角度指的是x轴正方向绕逆时针方向到达射线旋转的角的角度;而当y<0时,射线与x轴正方向所得的角的角度指的是x轴正方向绕顺时针方向达到射线旋转的角的角度。
(拉的概念,总是就是夹角取小角,α和360°-α)

3)极角排序:
就是选取一个最左的点,按y最小,其次x最小来定义,接下来所有的点针对该点的射线,按角度由小到大,若相同按距离由近到远来排序。

4)左转判定:
这个和叉积有关,对于向量p1(x1,y1),p2(x2,y2)如果x1y2-x2y1>0,则从p1到p2左转

思路/方法:

市面上主要有:穷举法O(n^3),分治法O(nlogn),Jarvis步进法 O(nH),Graham扫描法 O(nlogn),Melkman O(n)!!! 算法() 5中算法。

这里先介绍Graham扫描法,比较容易上手。(Melkman还没学完)

Graham扫描法:

先来看一下这个生动的动图:

动图
Graham扫描法的思想呢,简单说说一下就是先确定最左下一个点(y相同则取x小的 ,然后以这个点和其他点的夹角进行极角排序,这个点和与它极角最小的肯定是凸包的节点(最外面一条边,你说为什么 ),将它们压入节点栈,逆时针对于每个排序好的点进行扫描,如果这个点和栈顶第二个点组成的直线在栈顶2个点组成的直线的右边,那么栈顶2个点组成的直线肯定不是凸包的边,栈顶元素肯定不是节点,弹出栈顶的点,加入新的点做栈顶;如果在左边,那没事了,栈顶点就是节点,加入新的点做栈顶。重复操作直到便利所有的点。
如果不是很清楚,那下面看慢动作:

这是一片点。
在这里插入图片描述

找到最靠近左下的一个点,其他的点按照极角排序

在这里插入图片描述

然后把1丢到凸包的栈里面,准备开始扫描

在这里插入图片描述

检查2号点是否在1的一侧,(检查一下是不是凸多边形)
这里检查到2号可行,先加入到栈中

在这里插入图片描述

检查到3更加靠近外侧(如果加入3号就会形成凹多边形,显然3在凸包中,而2不在)
然后把2号点弹出栈,判断1号和3号节点的关系(同判断2号)
在这里插入图片描述

依次这么判断,最后所有凸包上的点都会在栈中
在这里插入图片描述

(原文链接:https://blog.csdn.net/qq_30974369/article/details/76405546)

练习题(模板):

洛谷板子题:
P2742 [USACO5.1]圈奶牛Fencing the Cows /【模板】二维凸包
题目描述
农夫约翰想要建造一个围栏用来围住他的奶牛,可是他资金匮乏。他建造的围栏必须包括他的奶牛喜欢吃草的所有地点。对于给出的这些地点的坐标,计算最短的能够围住这些点的围栏的长度。

输入格式
输入数据的第一行是一个整数。表示农夫约翰想要围住的放牧点的数目 n。第 2行到第 (n + 1) 行,每行两个实数,第 (i+1) 行的实数x,y分别代表第 i个放牧点的横纵坐标。

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

输入输出样例
输入
4
4 8
4 12
5 9.3
7 8
输出
12.00
说明/提示数据规模与约定
对于 100% 的数据,保证1<=n<=10^5;
-10^6<= x,y<=10^6 。小数点后最多有 2 位数字。

下面贴一下本蒟蒻的代码 ==

#include <bits/stdc++.h>
using namespace std;
const int maxn=1e5+5,inf=1e8;
int n,top;
double sum;
struct node{
    double x,y;
}p[maxn],s[maxn];
double dis(double x1,double y1,double x2,double y2){
    return sqrt((x1-x2)*(x1-x2)+(y1-y2)*(y1-y2));
}
//极角排序
inline bool cmp(node a,node b){
    double A=atan2((a.y-p[1].y),(a.x-p[1].x));
    double B=atan2((b.y-p[1].y),(b.x-p[1].x));
    if(A!=B) return A<B;
    else return a.x<b.x;
}
//叉积
long long cross(node a,node b,node c){
    return 1LL*(b.x-a.x)*(c.y-a.y)-1LL*(b.y-a.y)*(c.x-a.x);
}
//求凸包
void get(){
    p[0].x=inf;
    p[0].y=inf;
    int k;
    for(int i=1;i<=n;++i){//找到最左下角的点
        if(p[0].y>p[i].y||(p[0].y==p[i].y&&p[i].x<p[0].x)){
            p[0]=p[i];
            k=i;
        }
    }
    swap(p[k],p[1]);
    sort(p+2,p+n+1,cmp);//进行极角排序
    s[0]=p[1];s[1]=p[2];top=1;//初始化栈
    for(int i=3;i<=n;){
        if(top&&cross(s[top-1],p[i],s[top])>=0)
            top--;//如果栈顶不是凸包节点则弹出
        else s[++top]=p[i++];//加入凸包栈
    }
}
int main(){
    cin>>n;
    for(int i=1;i<=n;++i){
        cin>>p[i].x>>p[i].y;
    }
    get();
    //下面求距离和就行了
    for(int i=1;i<=top;++i)
        sum+=dis(s[i-1].x,s[i-1].y,s[i].x,s[i].y);
    sum+=dis(s[0].x,s[0].y,s[top].x,s[top].y);
    printf("%.2f",sum);
}

[借鉴这位老哥的板子学的,感谢!]
(https://blog.csdn.net/qq_30974369/article/details/76405546)

缺点及改进:

用 Graham扫描法 极角排序会造成丢点
例如
0 0
1 1
2 2
0 1
0 2
他扫描时会丢掉一个点,所以有人改进了极角排序,变成了以x y坐标排序

struct paint{
    int x;
    int y;
    paint(){} 
    paint(int xx,int yy):x(xx),y(yy){}
    bool friend operator <(const paint &a,const paint &b){
        if(a.x==b.x) return a.y<b.y;
        return a.x<b.x;
    }
    bool friend operator ==(const paint &a,const paint &b){
        return a.x==b.x&&a.y==b.y;
    }
    paint friend operator -(const paint &a,const paint &b){
        return paint(a.x-b.x,a.y-b.y);
    }
} ;
int cross(paint a,paint b){
    return a.x*b.y-b.x*a.y;
}
double dis(paint a,paint b){
    return sqrt((a.x-b.x)*(a.x-b.x)+(a.y-b.y)*(a.y-b.y));
}
double Andrew(vector<paint>p){

    int n=p.size();
    sort(p.begin(),p.end());
    vector<paint>t(n+1);
    int m=0;
    for(int i=0;i<n;i++){
        while(m>1&&cross(t[m-1]-t[m-2],p[i]-t[m-2])<=0) m--; 
        //等号决定共线的点是否进入栈,也就是是否要凸边上的点
        t[m++]=p[i];        
    } 
    int o=m;
    for(int i=n-2;i>=0;i--){
        while(m>o&&cross(t[m-1]-t[m-2],p[i]-t[m-2])<=0) m--;
        //等号决定共线的点是否进入栈,也就是是否要凸边上的点
        t[m++]=p[i];
    }
    if(n>1) m--;
    double ans=0;
    for(int i=1;i<m;i++){
        ans+=dis(t[i],t[i-1]);
    }
    return ans+dis(t[0],t[m-1]);
}

因为以x 和 y 排序所以每次只会求一半,需要正反扫两边 第二遍从n-2开始 因为第一遍的时候 n-1点肯定会进去
原理
还是利用夹角,让整个图形保持左转 (向左拐弯),先找一个最左边的点,因为经过排序,所以就是 0 号点了,先加入一个点(因为栈里只有一个点,无法构成角,直接跳过),在加一个点判断是否左拐(利用叉积 (角度)) 如果是就把点加入 ,如果不是就弹出栈顶,直到为左拐,把点加入,因为以x y排序第一遍找的都是y值大的点 也就是上凸包 下面没有,所以我们要反着找一遍 在第一遍时最后一个点肯定会加入所以第二遍就直接从倒数第二个点开始找了

原文链接:https://blog.csdn.net/qq_37493070/article/details/81774876

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值