凸包进阶旋转卡壳(模板题目集)
前置了解:
Andrew算法(求凸包)-CSDN博客
旋转卡壳
(Rotating Calipers)算法是一种用于解决凸包相关问题的高效算法,尤其适用于计算凸包直径(即凸包上任意两点间的最大距离)、最小包围矩形等问题
旋转卡壳算法的核心思想是:通过一对平行线(卡壳)“夹住” 凸包,并旋转这对平行线,在旋转过程中枚举所有可能的对踵点对(即凸包上距离最远的点对),从而找到凸包的直径。
1. 对踵点对(Antipodal Pairs)
- 定义:如果过凸包上的两个点可以作一对平行切线,使得凸包夹在这两条平行线之间,则称这两个点为一对对踵点。
- 性质:凸包的直径一定是某一对对踵点之间的距离。因此,只需要枚举所有对踵点对,计算它们之间的距离,取最大值即可。
2. 旋转过程
- 边 - 点对:对于凸包上的每条边,找到距离该边最远的点。这个点和边的两个端点构成潜在的对踵点对。
- 旋转对称性:当一条边对应的最远点确定后,随着边按顺序旋转,对应的最远点也会按顺序移动(不会回溯),因此可以在线性时间内枚举所有对踵点对。
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5;
struct vec{
double x,y;
bool operator<(const vec& other) const {
if(x != other.x) return x < other.x;
return y < other.y;
}
};//凸包:比较函数
vec p[N], st[N];
double cross(vec A, vec B){
return A.x*B.y-A.y*B.x;
}//凸包,叉乘函数
double Side(vec a, vec b, vec p) {
vec A={b.x-a.x, b.y-a.y};
vec B={p.x-a.x, p.y-a.y};
return cross(A,B);
}//凸包,根据3点两向量叉乘求左右
double dis2(vec a, vec b) {
return (a.x-b.x)*(a.x-b.x) + (a.y-b.y)*(a.y-b.y);
}//答案,平方距离
int Andrew(int n) {
if(n == 0) return 0;
sort(p+1, p+n+1);
int top = 0;
// 构建下凸
for(int i=1; i<=n; i++){
while(top >= 2 && Side(st[top-2], st[top-1], p[i]) <= 0)
top--;
st[top++] = p[i];
}
int temp = top;
// 构建上凸
for(int i=n-1; i>=1; i--){
while(top > temp && Side(st[top-2], st[top-1], p[i]) <= 0)
top--;
st[top++] = p[i];
}
// 如果只有一个点,凸包就是这个点本身
if(n == 1) return 1;
// 如果有两个点,凸包就是这两个点
if(n == 2) return 2;
// 移除重复的起点
if(top > 1) top--;
return top;
}
double rotating_calipers(int n){/核心转圈圈
if(n < 2) return 0;
if(n == 2) return dis2(st[0], st[1]);//12特判
double res = 0;
int j = 1; // 初始化最远点为 st[1]
// 枚举每条边 st[i] - st[i+1]
for(int i=0; i<n; i++){
// 找到边 st[i]-st[i+1] 对应的最远点 st[j]
while(Side(st[i], st[(i+1)%n], st[j]) < Side(st[i], st[(i+1)%n], st[(j+1)%n]))//n只是为了防首尾那一个点
j = (j+1)%n;
// 更新最大距离
res = max(res, max(dis2(st[i], st[j]), dis2(st[(i+1)%n], st[j])));
// 检查对踵点对 (i, j+1) 和 (i+1, j+1)
res = max(res, max(dis2(st[i], st[(j+1)%n]), dis2(st[(i+1)%n], st[(j+1)%n])));/直接线性了!!
}
return res;
}
int main(){
int n;
cin >> n;
for(int i=1; i<=n; i++){
cin >> p[i].x >> p[i].y;
}
int m = Andrew(n);
cout << fixed << setprecision(0) << rotating_calipers(m) << endl;
return 0;
}
题目2
很明显,对踵点对他又要发挥作用惹
以下代码只有黑色部分与上面不同
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5;
struct vec{
double x,y;
bool operator<(const vec& other) const {
if(x != other.x) return x < other.x;
return y < other.y;
}
};
vec p[N], st[N];
double cross(vec A, vec B){
return A.x*B.y-A.y*B.x;
}
double Side(vec a, vec b, vec p) {
vec A={b.x-a.x, b.y-a.y};
vec B={p.x-a.x, p.y-a.y};
return cross(A,B);
}
double dis2(vec a, vec b) {
return (a.x-b.x)*(a.x-b.x) + (a.y-b.y)*(a.y-b.y);
}//没用,只不过没有删
int Andrew(int n) {
if(n == 0) return 0;
sort(p+1, p+n+1);
int top = 0;
for(int i=1; i<=n; i++){
while(top >= 2 && Side(st[top-2], st[top-1], p[i]) <= 0)
top--;
st[top++] = p[i];
}
int temp = top;
for(int i=n-1; i>=1; i--){
while(top > temp && Side(st[top-2], st[top-1], p[i]) <= 0)
top--;
st[top++] = p[i];
}
if(n == 1) return 1;
if(n == 2) return 2;
if(top > 1) top--;
return top;
}
double rotating_calipers(int n){
double ans=0;
for (int i=1;i<n;i++){
int a=i,b=i+1;
for (int j=i+1;j<=n;j++){
//众所周知,叉乘是两向量的对应平行四边形的面积。。。。。。
while (Side(st[i],st[j],st[a+1])<Side(st[i],st[j],st[a])) a=a%n+1;//这里的上面为什么是小于号:原因:在该算法中,我们其实要遍历每条对角线,也就是i-j线,假设j现在离i比较远,由于a初始是i,很明显加1之后i-a在i-j的右边,叉积是负数。。。。。所以<才对;;而b为什么又需要大于呢?根据已有的j=i+1;很明显从一开始,b+1的点一定是要大于b点(0),所以从一开始,b就被推到了i-j的上面,也就是左侧,,,很明显成为了正数;然后对于一条对角线,很明显我们需要在他的左右侧各找一个最大三角形来围最后的最大四边形。。完美!
while (Side(st[i],st[j],st[b+1])>Side(st[i],st[j],st[b])) b=b%n+1;//主要还是加1,%n还是为了防止那个首尾连接点。。作用:通过不断迭代,找到选择了当前边i=1,j=i+1的最大面积;之后因为ab已经++,不会回溯,就是开始逆时针转圈圈了(i大循环是会回溯的)
ans=max(ans,-Side(st[i],st[j],st[a])+Side(st[i],st[j],st[b]));
}
}
return ans/2;
}
int main(){
int n;
cin >> n;
for(int i=1; i<=n; i++){
cin >> p[i].x >> p[i].y;
}
int m = Andrew(n);
cout << fixed << setprecision(3) << rotating_calipers(m) << endl;
return 0;
}