C++/PTA 神坛
- 题目要求
 - 解题前提及思路
 - 凹包算法
 - 概念
 - 步骤
 
- 解题思路
 
- 代码
 - tan(y/x)及tan2(y,x)函数
 
- 总结
 
题目要求
在古老的迈瑞城,巍然屹立着 n 块神石。长老们商议,选取 3 块神石围成一个神坛。因为神坛的能量强度与它的面积成反比,因此神坛的面积越小越好。特殊地,如果有两块神石坐标相同,或者三块神石共线,神坛的面积为 0.000。
长老们发现这个问题没有那么简单,于是委托你编程解决这个难题。
输入格式:
 输入在第一行给出一个正整数 n(3 ≤ n ≤ 5000)。随后 n 行,每行有两个整数,分别表示神石的横坐标、纵坐标(−109≤ 横坐标、纵坐标 <109)。
输出格式:
 在一行中输出神坛的最小面积,四舍五入保留 3 位小数。
输入样例:
 8
 3 4
 2 4
 1 1
 4 1
 0 3
 3 0
 1 3
 4 2
输出样例:
 0.500
样例解释:
 输出的数值等于图中红色或紫色框线的三角形的面积。
 
解题前提及思路
该程序可归类为计算平面内点集的最小面积的凸包
凹包算法
概念
定义:对于平面内的一组点,凸包是指这组点的凸多边形中,所有的顶点都是这组点中的点的集合。
凸包(Convex Hull)算法是一种计算二维平面点集最小凸多边形的算法。而凸多边形具有如下特征:在其内部的任意两点,连线都必须位于多边形内部。
因此,凹包(Concave Hull)问题就是将给定的点集包围在一个凹多边形中的算法。凹包问题的难点在于如何确定多边形的形状和顶点,目前已经存在一些解决方案,包括基于 alpha 型和 beta 型形态学的方法、基于分割重组的方法、基于斯蒂尔-沃森(Stiel-Watson)统计的方法等等。
步骤
先求出每个点的单位向量,根据单位向量在极坐标系下的极角从小到大排序,然后按照排序后的顺序依次加入点,如果当前点不是凸包上的点,则一直弹掉栈顶元素直到满足要求,最后栈内存储的就是凸包上所有的点了。
解题思路
-  
用结构体存储平面内的点的坐标及其单位向量,以及该点的编号。
 -  
求出每个点的单位向量,并根据该向量的极角从小到大排序。
 -  
遍历排序后的点集,依次判断每个点是否在凸包上。如果是,将其入栈;否则,弹出栈顶元素直到满足凸包条件。
 -  
最后栈内存储的即为凸包上所有的点的编号。
 -  
根据这些点的坐标计算凸包的面积,具体方法是用两个相邻点和原点组成的三角形面积求和。
 
代码
#include<iostream>
#include<algorithm>
#include<cmath> 
using namespace std;
struct node {
    long long x, y; // 存储平面内的点的坐标
    int xiang; // 存储所在象限
};
int n;
bool cmp(node x,node y)
{
    // 判断两个点所在象限是否相同
    if(x.xiang != y.xiang)
        return x.xiang < y.xiang;
    // 如果所在象限相同,则判断它们组成的向量与 x 轴之间的夹角大小来排序,
    // 如果向量之间的叉积为正数,表明顺时针旋转需要交换两个向量的位置,这时
    // 我们认为前一个向量更小;如果叉积为负数,表明逆时针旋转不需要交换,这时
    // 认为后一个向量更小。
    return x.x * y.y - x.y * y.x < 0; 
}
// 判断点所在象限的函数
int judge(node x)
{
    if(x.x > 0 && x.y > 0) return 1;
    if(x.x < 0 && x.y > 0) return 2;
    if(x.x < 0 && x.y < 0) return 3;
    if(x.x > 0 && x.y < 0) return 4;
 }
int main()
{
    cin >> n;
    
    struct node a[5010], b[5010];
    for(int i = 1; i <= n; i++)
    {
        cin >> a[i].x >> a[i].y;
    }
    
    double s = -1;
    int cnt;
    for(int i = 1; i <= n; i++)
    {
        cnt = 1;
        for(int j = 1; j <= n; j++)
        {
            if(i == j) continue;
            b[cnt].x = a[j].x - a[i].x;
            b[cnt].y = a[j].y - a[i].y;
            b[cnt].xiang = judge(b[cnt]);
            cnt++;
        } 
        sort(b + 1, b + n, cmp); // 排序
        for(int j = 1; j < n - 1; j++)
        {
            // 计算三角形面积,求最小值
            if(s == -1 || fabs(b[j + 1].x * b[j].y - b[j + 1].y * b[j].x) * 0.5 < s)
                s = fabs(b[j + 1].x * b[j].y - b[j + 1].y * b[j].x) * 0.5;
        }
    }
    printf("%.3f\n", s);
    return 0;
} 
 } 
 
需要注意的是,在计算向量的极角时,可以用 atan2(y, x) 函数,而不是简单地使用 arctan(y/x),这是因为 atan2 函数会根据 y 和 x 的符号自动确定向量在哪个象限,避免了分段计算的麻烦。
tan(y/x)及tan2(y,x)函数
tan(y, x) 函数表示求以 (x, y) 为起点的射线与 x 轴正方向之间的夹角的正切值。在求解过程中,需要使用参数 y 和 x 分别代表这条射线在 y 方向和 x 方向上的长度。因此,tan 函数的返回值是一个实数类型。
而 atan2(y, x) 函数则是求以原点为起点的射线与 x 轴之间的夹角,其输入参数也是射线在 y 和 x 方向上的长度。需要注意的是,atan2(y, x) 函数返回值的单位是弧度制,通常需要进行转换后才能得到以角度为单位的值。
atan2(y, x) 函数在计算以原点为起点的向量与 x 轴正方向所夹角度数时,会根据向量在平面上的位置自动确定向量所在的象限。
具体来说,如果向量位于第一象限,即其x和y坐标均为正数,
那么atan2(y, x)计算出的角度值将是 0 到 π/2 之间的一个正值。
如果向量位于第二象限,即其x坐标为负数,而y坐标为正数,
那么atan2(y, x)计算出的角度值将是 π/2 到 π 之间的一个正值。
 
另外,由于 tan(y, x) 中需要除以 x 的值,所以当 x 的值为 0 时会导致除数为 0 的错误。而 atan2(y, x) 可以避免这种错误的发生,因此更加常用。
总结
本文使用凹包算法、atan2(y,x)函数进行解题,读者可躬身实践。
 我是秋说,我们下次见。



















