凸包 初学 Andrew 和 Melkman (模板) 旋转卡(qia)壳(ke)

凸包初学

定义:

给你n个散落的点,让你求出最小的凸多边形将所有的点包括起来,或者点在边上。

稳定凸包,不能再扩充,每条边都用3个点

必备知识:

会用叉积判断点与直线的关系(这里指 点在线的那一边, 利用向量的叉积)
利用叉积比较点离线的距离

  1. 设 a 为直线所在向量 b为 直线末尾与点相连的向量,由叉积可得 a×b=|a||b|sin(<a,b>) a × b = | a | | b | s i n ( < a , b > ) 我们可以利用a b向量的夹角来求 a×b>0 a × b > 0 说明点在线的一边, a×b==0 a × b == 0 说明点在线上, a×b<0 a × b < 0 说明点在线的另一边
  2. 还是 a×b=|a||b|sin(<a,b>) a × b = | a | | b | s i n ( < a , b > ) , |b|sin(<a,b>) | b | s i n ( < a , b > ) 就是点到直线的距离

叉积求法

二维
a=(x1,y1),b=(x2,y2) a = ( x 1 , y 1 ) , b = ( x 2 , y 2 )
ab=x1y2x2y1 a ∗ b = x 1 ∗ y 2 − x 2 ∗ y 1
三维 略 (提示:行列式)

参考博客:

https://blog.csdn.net/u011001084/article/details/72768075

常用算法:
一、 暴力求解 ( O(n3) O ( n 3 ) )
  1. 暴力枚举每一条边,把所有的边都枚举出来
  2. 判段所有的点是不是在线的同一侧,是的话这条边就是,不是的话就不是
二、 Jarvis步进法 ( O(nH) O ( n H ) )
  1. 先找一个 肯定是凸包上的点,例如 最低点 最左边的点
  2. 枚举所有点找与这个点和他前一个点组成夹角的最小点(初始只有一个点,则求与x轴的夹角)
三、 分冶法 ( O(nlog(n)) O ( n l o g ( n ) )
  1. 先求最左最右两点 把点分成两部分
  2. 分别求每部分到分线的距离找最远的,然后继续分
四、 Graham扫描法 ( O(nlog(n)) O ( n l o g ( n ) )
  1. 找一个最左边的点,以这个点为基准从新建立坐标系
  2. 把所有点按极角进行排序
  3. 按顺序扫描每个点,判断是否为左拐,是加入栈,不是弹出栈顶,直到满足然后入栈
    缺点 : 精度问题 ,直接求会造成误差 ,可利用叉积排序 ,会造成丢点问题,详见 五
五、Andrew ( Graham扫描法 的改进版) ( O(nlog(n)) O ( n l o g ( n ) )

用 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值大的点 也就是上凸包 下面没有,所以我们要反着找一遍 在第一遍时最后一个点肯定会加入所以第二遍就直接从倒数第二个点开始找了

入门题

六、 Melkman算法 ( O(n) O ( n )

这个是个 O(n) O ( n ) 的算法,感觉很牛掰,仔细看后有限制条件 有序 如果求多边形,多边形的点是有序的(就是定点按一定顺序编的号)可以直接用,否则必须排序
优点 : 在线算法,可以动态加点

板子

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 Melkman(vector<paint>p){
    int n=p.size();
    vector<paint>q(n*2+10);
    int head,tail;
     head=tail=n;
    q[tail++]=p[0];

    int i;
    for(i=0;i<n-1;i++){
        q[tail]=p[i];

        if(cross(p[i]-q[head],p[i+1]-q[head])) break;
    }
    if(n==1) return 0;
    if(n==2) return dis(p[0],p[1]);
    if(n==3){
        return dis(p[0],p[1])+dis(p[0],p[2])+dis(p[1],p[2]);
    } 
    q[--head]=q[++tail]=p[++i];
    if(cross(q[n+1]-q[n],q[n+2]-q[n])<0) swap(q[n],q[n+1]);
    for(++i;i<n;i++){

       if(cross(q[tail]-q[tail-1],p[i]-q[tail-1])>0
        &&cross(q[head]-q[head+1],p[i]-q[head+1])<0) continue; //凹凸全部满足,这个点是内部点,不考虑
      while(tail-head>1&&cross(q[tail]-q[tail-1],p[i]-q[tail-1])<=0)  tail--;
      q[++tail]=p[i];
      while(tail-head>1&&cross(q[head]-q[head+1],p[i]-q[head+1])>=0)  head++;
      q[--head]=p[i];

    }
    double ans=0;
    for(int i=head;i<tail;i++){
        ans+=dis(q[i],q[i+1]);
    }
    return ans;

}

原理:

建一个小凸包,然后加入点 同时维护凹凸

利用双端队列进行维护,先找一个点扔到对首或队尾(一般是0号点) ,找连续两个点i, i+10点能形成凸行的点,把i点入队首,然后这时候队列里有两个点,再把i+1同时入队首和队尾,这时候队里的点已经成了一个由三个点组成的小凸包,加入第四个点时开始判断,如果对首是凸(和四的左拐类似)就直接入队首,否则弹出队首,直到满足,入队首,如果队尾是凹行(右拐),就直接入队尾,否则弹出队尾,直到满足,入队尾

实际上就是在一个小的凸边行上逐渐加点,然后同时判断加的这个点在前面是否满足,和在后面是否满足

练习题目




求点集中的点相距的最远距离( 旋转卡壳)

参考博客:

https://www.cnblogs.com/xdruid/archive/2012/07/01/2572303.html

思路:
  1. 相距最远的点肯定是凸包上的点,用 Andrew求出来凸包,然后暴力枚举凸包上所有点的距离
  2. 利用凸包旋转卡壳求
算法思路
  • 卡壳,就是在凸包上做两个平行线,把凸包卡住,而卡住的点称为对踵点
  • 然后旋转两条平行线,直到与一条边相重合,这时候你会发现,距离没在与平行线重合线上的点最远的点就是这个线上的两点中的一个
  • 如果直接求,那么平行线很多,还是 n2 n 2 的复杂度,这样的花还不如暴力求,但是你会发现一个规律,当你找到一个对应点,换边时(旋转),距离这条边最远的点在你之前找到那个点后面,也就是说没有必要再从头开始找,直接接着往后面找点

大佬的图

借用大佬的图

比如说你刚找完 上图颜色最多的那个点,他的对应点就是最上面那个点,然后你旋转成左边相邻的那条边,这条边的最远点明显在刚才那个点的后面,也就是上面说的可以直接往后找,(具体的原理看上面的参考博客,大佬讲的很清楚)

模板
double RC(int len){  //len 指凸包点集合长度 vic[] 凸包点 cross 叉积 同上
    int p=1;;
    double ans=dist(vic[0],vic[1]);
    for(int i=0;i<len;i++){

        while(abs(cross(vic[(i+1)%len]-vic[i],vic[p]-vic[i]))
           <abs(cross(vic[(i+1)%len]-vic[i],vic[(p+1)%len]-vic[i])))
          p=(p+1)%len;
          ans=max(ans,max(dist(vic[p],vic[i]),dist(vic[(i+1)%len],vic[p])));

    }
    return ans;
}

练习题目

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
使用GaoCp Math Package中的Vector2d向量库以及Melkman算法来求凸包,以下是示例代码: ```c++ #include <bits/stdc++.h> #include <gcmp.hpp> using namespace std; using namespace gcmp; const int N = 1e5 + 5; int n, top; Vector2d p[N], q[N], stk[N]; void melkman() { int front = 1, back = 1; stk[1] = q[1]; for (int i = 2; i <= n; ++i) { // 加入左侧的点 if (q[i].x > stk[front].x) { stk[--front] = q[i]; } else if (q[i].x < stk[front].x) { stk[++front] = q[i]; } else { stk[front] = q[i]; } // 加入右侧的点 if (q[n - i + 1].x > stk[back].x) { stk[++back] = q[n - i + 1]; } else if (q[n - i + 1].x < stk[back].x) { stk[--back] = q[n - i + 1]; } else { stk[back] = q[n - i + 1]; } } top = 0; for (int i = front; i <= back; ++i) { stk[++top] = stk[i]; } } bool cmp(Vector2d a, Vector2d b) { double c = a.x - b.x; if (c == 0) return a.y < b.y; return c < 0; } int main() { scanf("%d", &n); for (int i = 1; i <= n; ++i) { scanf("%lf%lf", &p[i].x, &p[i].y); } sort(p + 1, p + n + 1, cmp); int k = 0; for (int i = 1; i <= n; ++i) { while (k > 1 && (p[i] - q[k - 1]).cross(q[k] - q[k - 1]) <= 0) { --k; } q[++k] = p[i]; } int t = k; for (int i = n - 1; i >= 1; --i) { while (k > t && (p[i] - q[k - 1]).cross(q[k] - q[k - 1]) <= 0) { --k; } q[++k] = p[i]; } melkman(); for (int i = 1; i <= top; ++i) { printf("%.0f %.0f\n", stk[i].x, stk[i].y); } return 0; } ``` 这段代码使用了GaoCp Math Package中的Vector2d向量库和Melkman算法来求凸包。具体思路可以参考以下步骤: 1. 将所有点按横坐标排序。 2. 对排序后的点依次进行半平面交,得到凸包的上下两个部分。 3. 对上下两个部分使用Melkman算法进行合并。 4. 最后得到的栈中就是凸包的所有点。 需要注意的是,在使用Melkman算法进行合并时,需要分别从左至右和从右至左两次进行,最后将结果合并。 希望对你有所帮助!

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值