UVa 11243 Texas Trip

题目描述

Harry\texttt{Harry}Harry 发现他的 SUV\texttt{SUV}SUV 车门上有一些小孔,这些小孔可以视为平面上的整数坐标点。当地的 American Tire\texttt{American Tire}American Tire 商店只出售正方形的玻璃纤维修补片。你需要帮助 Harry\texttt{Harry}Harry 找到能够覆盖所有小孔的最小正方形修补片的面积。

输入格式

  • 第一行包含一个整数 TTTT≤30T \leq 30T30 ),表示测试用例的数量。
  • 每个测试用例的第一行包含一个整数 nnnn≤30n \leq 30n30 ),表示点的数量。
  • 接下来的 nnn 行每行包含两个整数 xxxyyy−500≤x,y≤500-500 \leq x, y \leq 500500x,y500 ),表示点的坐标。

输出格式

  • 对于每个测试用例,输出一行,保留两位小数,表示覆盖所有点的最小正方形面积。
  • 如果输出值与正确答案的误差不超过 0.10.10.1 ,即视为正确。

样例输入

2
4
-1 -1
1 -1
1 1
-1 1
4
10 1
10 -1
-10 1
-10 -1

样例输出

4.00
242.00

题目分析

这是一个 最小外接正方形 问题。给定平面上的一组点,需要找到一个面积最小的正方形,使得所有点都在正方形内部或边界上。

关键难点

  1. 正方形可以旋转任意角度 ,不一定是轴对齐的。
  2. 正方形的边不一定与点集的凸包边平行。
  3. 最优正方形的对角线与点集的最远点对(直径)不一定重合。

几何观察

考虑一个能覆盖所有点的正方形。通过旋转坐标系,我们可以使正方形的边与坐标轴平行。因此,问题可以转化为:

找到一个旋转角度 θ\thetaθ ,使得在旋转后的坐标系中,覆盖所有点所需的轴对齐正方形边长最小。

解题思路

基本思路

设点集为 P={p1,p2,...,pn}P = \{p_1, p_2, ..., p_n\}P={p1,p2,...,pn} 。对于给定的旋转角度 θ\thetaθ ,我们将所有点绕原点逆时针旋转 −θ-\thetaθ ,得到新的点集 P′={p1′,p2′,...,pn′}P' = \{p_1', p_2', ..., p_n'\}P={p1,p2,...,pn} ,其中 pi′=rotate(pi,−θ)p_i' = \text{rotate}(p_i, -\theta)pi=rotate(pi,θ)

在旋转后的坐标系中,覆盖所有点的最小轴对齐正方形边长为:
s(θ)=max⁡(max⁡ixi′−min⁡ixi′,max⁡iyi′−min⁡iyi′) s(\theta) = \max(\max_{i} x_i' - \min_{i} x_i', \max_{i} y_i' - \min_{i} y_i') s(θ)=max(imaxxiiminxi,imaxyiiminyi)
其中 xi′x_i'xiyi′y_i'yi 是点 pi′p_i'pi 的坐标。

我们的目标是找到最小的 s(θ)s(\theta)s(θ) ,即:
min_side=min⁡θ∈[0,π2]s(θ) \text{min\_side} = \min_{\theta \in [0, \frac{\pi}{2}]} s(\theta) min_side=θ[0,2π]mins(θ)
正方形的面积即为 min_side2\text{min\_side}^2min_side2

为什么搜索区间是 [0,π2][0, \frac{\pi}{2}][0,2π]

因为正方形具有 90∘90^\circ90 的旋转对称性。对于任意角度 θ\thetaθs(θ)=s(θ+π2)s(\theta) = s(\theta + \frac{\pi}{2})s(θ)=s(θ+2π) 。因此,我们只需要在长度为 π2\frac{\pi}{2}2π 的区间内搜索即可。

函数 s(θ)s(\theta)s(θ) 的性质

虽然 s(θ)s(\theta)s(θ) 不是严格的凸函数,但它通常具有单谷性质(即先递减后递增),或者至少在一个区间内是凸的。这使得我们可以使用 三分搜索 来找到最小值。

三分搜索算法

三分搜索用于在单峰函数(包括单谷函数)上寻找极值点。对于区间 [l,r][l, r][l,r] ,我们取两个中间点 m1=l+r−l3m_1 = l + \frac{r-l}{3}m1=l+3rlm2=r−r−l3m_2 = r - \frac{r-l}{3}m2=r3rl ,计算 f(m1)f(m_1)f(m1)f(m2)f(m_2)f(m2)

  • 如果 f(m1)<f(m2)f(m_1) < f(m_2)f(m1)<f(m2) ,则最小值在 [l,m2][l, m_2][l,m2] 内。
  • 否则,最小值在 [m1,r][m_1, r][m1,r] 内。

重复上述过程直到区间足够小。

算法步骤

  1. 对于每个测试用例,读取 nnn 个点。
  2. 在角度区间 [0,π2][0, \frac{\pi}{2}][0,2π] 上对 s(θ)s(\theta)s(θ) 进行三分搜索。
  3. 每次迭代计算两个中间角度的 s(θ)s(\theta)s(θ) 值。
  4. 迭代足够多次(例如 606060 次)以保证精度。
  5. 计算最小边长,输出面积。

时间复杂度

  • 每次计算 s(θ)s(\theta)s(θ) 需要 O(n)O(n)O(n) 时间。
  • 三分搜索迭代 606060 次。
  • 总时间复杂度为 O(60×n)O(60 \times n)O(60×n) ,对于 n≤30n \leq 30n30 非常高效。

精度说明

题目要求答案误差不超过 0.10.10.1 。通过 606060 次三分搜索迭代,角度精度可以达到约 10−1110^{-11}1011 弧度,完全满足要求。

代码实现

// Texas Trip
// UVa ID: 11243
// Verdict: Accepted
// Submission Date: 2025-12-04
// UVa Run Time: 0.000s
//
// 版权所有(C)2025,邱秋。metaphysis # yeah dot net

#include <bits/stdc++.h>
using namespace std;

const double PI = acos(-1.0);
const double EPS = 1e-9;
const double INF = 1e20;

struct Point {
    double x, y;
    Point(double x = 0, double y = 0) : x(x), y(y) {}
    
    Point operator + (const Point& p) const { return Point(x + p.x, y + p.y); }
    Point operator - (const Point& p) const { return Point(x - p.x, y - p.y); }
    // 将点绕原点逆时针旋转 angle 弧度
    Point rotate(double angle) const {
        double c = cos(angle), s = sin(angle);
        return Point(x * c - y * s, x * s + y * c);
    }
};

// 计算在给定角度 angle 下,覆盖所有点的最小正方形边长
double minSquareSideForAngle(const vector<Point>& points, double angle) {
    double minX = INF, maxX = -INF, minY = INF, maxY = -INF;
    
    for (const auto& p : points) {
        // 将点旋转 -angle 角度,使正方形边与坐标轴平行
        Point rotated = p.rotate(angle);
        minX = min(minX, rotated.x);
        maxX = max(maxX, rotated.x);
        minY = min(minY, rotated.y);
        maxY = max(maxY, rotated.y);
    }
    
    double width = maxX - minX;
    double height = maxY - minY;
    // 正方形边长取宽度和高度的最大值
    return max(width, height);
}

// 在区间 [0, PI/2] 内三分搜索最小正方形边长
double ternarySearch(const vector<Point>& points) {
    double left = 0, right = PI / 2.0;
    
    // 迭代 60 次保证足够精度
    for (int iter = 0; iter < 60; ++iter) {
        double mid1 = left + (right - left) / 3.0;
        double mid2 = right - (right - left) / 3.0;
        
        double side1 = minSquareSideForAngle(points, mid1);
        double side2 = minSquareSideForAngle(points, mid2);
        
        if (side1 < side2) {
            right = mid2;
        } else {
            left = mid1;
        }
    }
    
    double bestAngle = (left + right) / 2.0;
    return minSquareSideForAngle(points, bestAngle);
}

double solveCase(const vector<Point>& points) {
    int n = points.size();
    if (n <= 1) return 0.0;
    
    // 三分搜索得到最小边长
    double bestSide = ternarySearch(points);
    
    // 检查轴对齐的情况(角度为 0)
    bestSide = min(bestSide, minSquareSideForAngle(points, 0));
    
    // 检查角度为 45 度的情况
    bestSide = min(bestSide, minSquareSideForAngle(points, PI/4.0));
    
    // 返回面积
    return bestSide * bestSide;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    
    int T; cin >> T;
    cout << fixed << setprecision(2);
    
    while (T--) {
        int n; cin >> n;
        vector<Point> points(n);
        for (int i = 0; i < n; ++i) 
            cin >> points[i].x >> points[i].y;
        double ans = solveCase(points);
        cout << ans << endl;
    }
    
    return 0;
}

算法正确性验证

样例 1

输入点: (−1,−1)(-1,-1)(1,1) , (1,−1)(1,-1)(1,1) , (1,1)(1,1)(1,1) , (−1,1)(-1,1)(1,1)
这些点本身就构成一个正方形,边长为 222 ,面积为 4.004.004.00
对于任何旋转角度, s(θ)=2s(\theta) = 2s(θ)=2 ,因此输出 4.004.004.00

样例 2

输入点: (10,1)(10,1)(10,1) , (10,−1)(10,-1)(10,1) , (−10,1)(-10,1)(10,1) , (−10,−1)(-10,-1)(10,1)
这些点构成一个 20×220 \times 220×2 的矩形。
通过几何计算或程序运行可知,最优正方形边长约为 15.556315.556315.5563 ,面积约为 242.00242.00242.00
我们的算法通过三分搜索找到这个最优角度,输出正确结果。

总结

本题的关键在于认识到最小外接正方形问题可以通过旋转坐标系转化为一系列轴对齐正方形问题,然后使用三分搜索在角度空间中找到最优解。这种方法避免了复杂的几何构造和枚举,代码简洁且效率高,能够满足题目对精度和时间的所有要求。

核心要点

  1. 利用正方形的旋转对称性,将搜索区间缩小到 [0,π2][0, \frac{\pi}{2}][0,2π]
  2. 使用三分搜索高效地找到最优旋转角度。
  3. 注意处理边界情况,如点集只有一个点或所有点重合。

通过这道题,我们学习了如何将几何问题转化为函数极值问题,并利用数值方法求解。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值