计算几何-旋转卡壳两种实现方案(兼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[从零开始]

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[从一开始]

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‘\) 坐标最大的点就行了。

图12

坐标系旋转与坐标旋转

  • 获取新坐标系:

    我们已知当前所在的向量 \(\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[坐标系旋转]

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

参考文献:

posted @ 2026-02-05 17:28  Kx_Triumphs  阅读(31)  评论(0)    收藏  举报