计算几何-旋转卡壳两种实现方案(兼P1452题解

前言

题目链接

首先说明一下,这题题面有个地方不太严谨:题意要求求凸包直径 ,然而当 \(n=2\) 时凸包并不存在,此时直径也应该不存在。所以应该是求平面中最远的点对的距离。

旋转卡(qia)壳可以用于求凸包的直径、宽度,两个不相交凸包间的最大距离和最小距离等。

这里就不过多赘述了,仅介绍两种不同的方案 ------ 三角形面积比较坐标系旋转 ,需要学习旋转卡壳正确性、原理的同学请移步这两位大佬的 blog cjyybxdruid

方案一为三角形面积比较,是常规方案,在某些题里较麻烦(比如 P3187 [HNOI2007] 最小矩形覆盖 );方案二坐标系旋转貌似没有那么常见,但在大多数凸包题里更加方便与实用。

注意事项 ------ 从零或一开始存储凸包中的点

在提供方案前我先多嘴几句注意事项,个人认为挺重要的(\(\sout{因为踩坑了}\))。

  • 注意:你的 \(stk\) 从 \(0\) 和从 \(1\) 开始的实现方式是不同 的。(\(stk\) 数组中存储凸包中的点)

    主要在于取模方式:如果从 \(0\) 开始,须使用 \((j+1)\mod tp\);如果从 \(1\) 开始,则须使用 \(j \mod tp + 1\)。

  • 原因:

    • 对于从 \(0\) 开始,先加后模可以取到 \(0\),而先模后加无法取到 \(0\)。

      解释:当 \(j=tp-1\) 时,\((j+1)\mod tp=0\),取到了 \(0\)。然而 \(j\mod tp+1=tp\),由于从 \(0\) 开始存储,所以 \(tp\) 是一个空位,并且 \(tp\mod tp+1=1\),故无法取到 \(0\)。

    • 同理,对于从 \(1\) 开始,先加后模取到了 \(0\),没有取到 \(tp\),然而 \(0\) 是个空位,所以需要先模后加。

具体的代码差别可以看方案一中的两种代码实现

方案一:三角形面积比较

实现原理

众所周知,叉积可以求平行四边形的面积,而三角形面积为平行四边形面积的一半,并且它们同底等高。所以比较三角形的高等价于比较平行四边形的面积大小,即比较叉积大小。

实现

从零开始

:::success[从零开始]

cpp 复制代码
void Andrew(){
    sort(p,p+n);
    n=unique(p,p+n)-p;
    int lst=1;
    for(int i=0;i<n;i++){
        while(tp>lst&&Cross(stk[tp-2],stk[tp-1],p[i])<=0) tp--;
        stk[tp++]=p[i];
    }
    lst=tp;
    for(int i=n-2;i>=0;i--){
        while(tp>lst&&Cross(stk[tp-2],stk[tp-1],p[i])<=0) tp--;
        stk[tp++]=p[i];
    }
}
int RC(){//rotating calipers
    if(tp<=3) return dis(stk[0],stk[1]);
    int ans=0;
    for(int i=0,j=2;i<tp-1;i++){//从0开始
        while(Cross(stk[i],stk[i+1],stk[j])<=Cross(stk[i],stk[i+1],stk[(j+1)%tp]))//三角形面积 S=(|A||B|sin<A,B>)/2
            j=(j+1)%tp;
        ans=max(ans,dis(stk[i],stk[j]));
        ans=max(ans,dis(stk[i+1],stk[j]));
    }
    return ans;
}

:::

从一开始

:::success[从一开始]

cpp 复制代码
void Andrew(){
    sort(p+1,p+n+1);
    n=unique(p+1,p+n+1)-(p+1);
    int lst=1;
    for(int i=1;i<=n;i++){
        while(tp>lst&&Cross(stk[tp-1],stk[tp],p[i])<=0)
            tp--;
        stk[++tp]=p[i];
    }
    lst=tp;
    for(int i=n-1;i>=1;i--){
        while(tp>lst&&Cross(stk[tp-1],stk[tp],p[i])<=0)
            tp--;
        stk[++tp]=p[i];
    }
}
int RC(){//rotating calipers
    if(tp<=3) return dis(stk[1],stk[2]);
    int ans=0;
    for(int i=1,j=3;i<=tp-1;i++){//从1开始
        while(Cross(stk[i],stk[i+1],stk[j])<=Cross(stk[i],stk[i+1],stk[j%tp+1]))//三角形面积 S=(|A||B|sin<A,B>)/2
            j=j%tp+1;
        ans=max(ans,dis(stk[i],stk[j]));
        ans=max(ans,dis(stk[i+1],stk[j]));
    }
    return ans;
}

:::

方案二:坐标系旋转

这边建议旋转卡壳都使用此方案,会比较方便。

实现原理:

先看如何实现旋转卡壳

我们知道旋转卡壳需要求取以当前向量为底,高最长的点。如图 \(1\) ,如果直接以 \(\overrightarrow{OP}\) 建立如图 \(1\) 所示新坐标系 \(x'Oy'\),那么直接找 \(y'\) 坐标最大的点就行了。

坐标系旋转与坐标旋转

  • 获取新坐标系:

    我们已知当前所在的向量 \(\overrightarrow{OP}\),它的方向向量 \(\overrightarrow{u}\) 即为 \(x'\) 轴所在方向,法向量 \(\overrightarrow{v}\) 即为 \(y'\) 轴所在方向。

    其中 \(\overrightarrow{u}=\frac{\overrightarrow{OP}}{|\overrightarrow{OP}|}\),\(\overrightarrow{v}=\{-\overrightarrow{u}.y,\overrightarrow{u}.x\}\)。(\(\overrightarrow{v}\) 坐标推导如图 \(2\))

  • 获取点在新坐标系上的坐标:

    如图 \(1\),\(\overrightarrow{OQ}\) 已知,\(Q\) 的 \(x'\) 坐标等价于 \(\overrightarrow{OQ}\) 在 \(\overrightarrow{u}\) 上的投影向量的模长。又因为 \(\overrightarrow{u}\) 为单位向量,模长为 \(1\),所以 \(\overrightarrow{OQ}\) 投影向量的模长就等于 \(\overrightarrow{OQ}\cdot\overrightarrow{u}\),即 \({Q.x'}=\overrightarrow{OQ}\cdot\overrightarrow{u}\)。

    同理,\({Q.y'}=\overrightarrow{OQ}\cdot\overrightarrow{v}\)。

  • 将新坐标转化为原坐标( P3187 [HNOI2007] 最小矩形覆盖 中需要):

    其实怎么变过来的怎么变回去就行了。

    如图 \(1\) 已知 \(Q.x'\) 与 \(Q.y'\),那么 \(\overrightarrow{OQ}={Q.x'} \times \overrightarrow{u}+{Q.y'} \times \overrightarrow{v}\)。

实现

注意事项

同样也要考虑 \(stk\) 从 \(0\) 或 \(1\) 开始存储的取模问题。其他没什么好注意的,记得求模长需要开 double 。

代码:

:::success[坐标系旋转]

cpp 复制代码
void Andrew(){
    sort(p+1,p+n+1);
    n=unique(p+1,p+n+1)-(p+1);
    int lst=1;
    for(int i=1;i<=n;i++){
        while(tp>lst&&Cross(stk[tp-1],stk[tp],p[i])<=0) tp--;
        stk[++tp]=p[i];
    }
    lst=tp;
    for(int i=n-1;i>=1;i--){
        while(tp>lst&&Cross(stk[tp-1],stk[tp],p[i])<=0) tp--;
        stk[++tp]=p[i];
    }
}
int RC(){
    if(tp<=3) return dis(stk[1],stk[2]);
    int ans=0;
    for(int i=1,j=3;i<=tp-1;i++){
        double len=sqrt(dis(stk[i],stk[i+1]));
        Point P=Vec(stk[i],stk[i+1]);//当前向量i(i+1),即图中向量OP
        Point u={P.x/len,P.y/len};//向量i(i+1)的方向向量,即x'轴
        Point v={-u.y,u.x};//方向向量u的法向量,即y'轴
        while(Dot(stk[j],v)<=Dot(stk[j%tp+1],v)) j=j%tp+1;
        ans=max(ans,dis(stk[i],stk[j]));
        ans=max(ans,dis(stk[i+1],stk[j]));
    }
    return ans;
}

:::

这道题用坐标系旋转的好处不是明显,但用在 P3187 [HNOI2007] 最小矩形覆盖 中效果就很明显。

The End

指导鸣谢:ssam

参考文献:

相关推荐
代码游侠2 小时前
学习笔记——Linux字符设备驱动开发
linux·arm开发·驱动开发·单片机·嵌入式硬件·学习·算法
m0_736919102 小时前
C++中的享元模式变体
开发语言·c++·算法
罗湖老棍子2 小时前
【 例 1】石子合并(信息学奥赛一本通- P1569)
数据结构·算法·区间dp·区间动态规划·分割合并
wangluoqi3 小时前
26.2.4练习总结
算法
流㶡3 小时前
逻辑回归实战:从原理到不平衡数据优化(含欠拟合/过拟合诊断与召回率提升)
算法·机器学习·逻辑回归
Tisfy3 小时前
LeetCode 3637.三段式数组 I:一次遍历(三种实现)
算法·leetcode·题解·模拟·数组·遍历·moines
遨游xyz3 小时前
数据结构-哈希表
算法·哈希算法
dyyx1113 小时前
C++中的过滤器模式
开发语言·c++·算法
lrh1228004 小时前
详解决策树算法:分类任务核心原理、形成流程与剪枝优化
算法·决策树·机器学习