洛谷P2742 【模板】二维凸包 / [USACO5.1] 圈奶牛Fencing the Cows 题解 Andrew 算法

题目链接:https://www.luogu.com.cn/problem/P2742

解题思路:

解决本题的核心在于使用计算几何算法求出点集的最小凸多边形(凸包),并计算其周长。

推荐使用现代算法竞赛中最常用的 Andrew 算法(Graham 扫描法的变体),该算法逻辑清晰、不易写错、不需要处理复杂的斜率和角度极角排序,时间复杂度为 \(O(N \log N)\)

一、 核心算法:Andrew 算法流程

Andrew 算法的核心思想是将凸包分为“下凸包”和“上凸包”两部分分别求解。

  1. 排序:将所有点按照 \(x\) 坐标为第一关键字升序、\(y\) 坐标为第二关键字升序进行排序。此时,序列中的第一个点(最左下方)和最后一个点(最右上方)一定在凸包上。
  2. 求下凸包:从左到右遍历排序后的点,用一个栈维护当前的下凸包节点。每次加入新点 \(P\) 时,检查栈顶的两个点组成的向量与 \(P\) 的位置关系。如果新点未能向“左”拐(即构成了顺时针旋转),说明栈顶的点在凸包内侧,应将其弹出,直到满足向左拐,再将 \(P\) 入栈。
  3. 求上凸包:从右到左(倒着)再遍历一遍所有的点,用同样的规则和同一个栈去维护上凸包。

二、 几何关键:向量叉积(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;
}
posted @ 2026-07-30 20:21  quanjun  阅读(2)  评论(0)    收藏  举报