洛谷P2742 【模板】二维凸包 / [USACO5.1] 圈奶牛Fencing the Cows 题解 Andrew 算法
题目链接:https://www.luogu.com.cn/problem/P2742
解题思路:
解决本题的核心在于使用计算几何算法求出点集的最小凸多边形(凸包),并计算其周长。
推荐使用现代算法竞赛中最常用的 Andrew 算法(Graham 扫描法的变体),该算法逻辑清晰、不易写错、不需要处理复杂的斜率和角度极角排序,时间复杂度为 \(O(N \log N)\)。
一、 核心算法:Andrew 算法流程
Andrew 算法的核心思想是将凸包分为“下凸包”和“上凸包”两部分分别求解。
- 排序:将所有点按照 \(x\) 坐标为第一关键字升序、\(y\) 坐标为第二关键字升序进行排序。此时,序列中的第一个点(最左下方)和最后一个点(最右上方)一定在凸包上。
- 求下凸包:从左到右遍历排序后的点,用一个栈维护当前的下凸包节点。每次加入新点 \(P\) 时,检查栈顶的两个点组成的向量与 \(P\) 的位置关系。如果新点未能向“左”拐(即构成了顺时针旋转),说明栈顶的点在凸包内侧,应将其弹出,直到满足向左拐,再将 \(P\) 入栈。
- 求上凸包:从右到左(倒着)再遍历一遍所有的点,用同样的规则和同一个栈去维护上凸包。
二、 几何关键:向量叉积(Cross Product)
判断点 \(P_3\) 是否在向量 \(\vec{P_1P_2}\) 的左侧,需要利用二维向量叉积:
\[\vec{A} \times \vec{B} = A.x \times B.y - A.y \times B.x
\]
设 \(\vec{A} = P_2 - P_1\),\(\vec{B} = P_3 - P_1\):
- 如果 \(\vec{A} \times \vec{B} > 0\),说明 \(P_3\) 在 \(\vec{P_1P_2}\) 的逆时针方向(左侧),保留。
- 如果 \(\vec{A} \times \vec{B} \le 0\),说明 \(P_3\) 在右侧或共线,不符合凸包性质,需要弹出栈顶。
示例程序
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e5 + 5;
struct Point {
double x, y;
} stk[maxn];
double cross(Point p1, Point p2, Point p3) {
return (p2.x - p1.x) * (p3.y - p1.y) - (p2.y - p1.y) * (p3.x - p1.x);
}
double dist(Point a, Point b) {
return sqrt((a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y));
}
// 计算周长
double cal_perimeter(vector<Point> &pts) {
int n = pts.size();
if (n <= 1)
return 0;
sort(pts.begin(), pts.end(), [](auto a, auto b) {
return a.x < b.x || a.x == b.x && a.y < b.y;
});
int top = 0;
// 构建下凸包
for (int i = 0; i < n; i++) {
while (top >= 2 && cross(stk[top-1], stk[top], pts[i]) <= 0) {
top--;
}
stk[++top] = pts[i];
}
// 构建上凸包
int lower_hull_size = top; // 记录当前下凸包的大小,防止上凸包把下凸包的起点弹掉
for (int i = n-2; i >= 0; i--) {
while (top > lower_hull_size && cross(stk[top-1], stk[top], pts[i]) <= 0) {
top--;
}
stk[++top] = pts[i];
}
// 此时栈中首尾都是 pts[0],多包含了一次起点,计算周长恰好形成闭环
double perimeter = 0;
for (int i = 1; i < top; i++) {
perimeter += dist(stk[i], stk[i+1]);
}
return perimeter;
}
int main() {
int n;
scanf("%d", &n);
vector<Point> pts(n);
for (int i = 0; i < n; i++) {
scanf("%lf%lf", &pts[i].x, &pts[i].y);
}
double ans = cal_perimeter(pts);
printf("%.2lf\n", ans);
return 0;
}
浙公网安备 33010602011771号