前言
首先说明一下,这题题面有个地方不太严谨:题意要求求凸包直径 ,然而当 \(n=2\) 时凸包并不存在,此时直径也应该不存在。所以应该是求平面中最远的点对的距离。
旋转卡(qia)壳可以用于求凸包的直径、宽度,两个不相交凸包间的最大距离和最小距离等。
这里就不过多赘述了,仅介绍两种不同的方案 ------ 三角形面积比较 与坐标系旋转 ,需要学习旋转卡壳正确性、原理的同学请移步这两位大佬的 blog cjyyb 和 xdruid。
方案一为三角形面积比较,是常规方案,在某些题里较麻烦(比如 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
参考文献: