1 条题解

  • 0
    @ 2025-11-26 21:57:21

    「PA 2018」Magiczne wieże 题解

    问题分析

    我们需要找到所有安全点构成的区域面积。

    安全点的定义:从该点向任何方向移动,都会接近某个魔法师(在某个塔上)。

    数学表达:点 PP 是安全的,当且仅当对于任意方向向量 v\vec{v},存在某个魔法塔 TT 使得:

    $$\frac{d}{dt} \text{dist}(P + t\vec{v}, T) < 0 \quad \text{当 } t \to 0^+ $$

    关键观察

    这个问题等价于求所有魔法塔的 Voronoi 区域的交集

    Voronoi 图:平面被划分为多个区域,每个区域包含距离某个站点最近的所有点。

    安全区域 = 对于任意点 PP,存在某个魔法塔 TT,使得 PPTT 的距离小于等于到其他所有塔的距离。

    算法思路

    方法:半平面交

    对于每对魔法塔 (Ti,Tj)(T_i, T_j),它们的垂直平分线将平面分成两个半平面:

    • 距离 TiT_i 更近的半平面
    • 距离 TjT_j 更近的半平面

    安全区域就是所有"距离 TiT_i 更近"的半平面的交集。

    步骤

    1. 枚举每个魔法塔 TiT_i
    2. 对于每个 TiT_i,构建它相对于其他所有塔的最近半平面
    3. 求所有这些半平面的交集
    4. 计算交集多边形的面积

    C++ 代码实现

    #include <bits/stdc++.h>
    using namespace std;
    
    const double EPS = 1e-8;
    const double INF = 1e9;
    
    struct Point {
        double x, y;
        Point(double x = , double y = ) : 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); }
        Point operator * (double k) const { return Point(x * k, y * k); }
        Point operator / (double k) const { return Point(x / k, y / k); }
        
        double dot(const Point& p) const { return x * p.x + y * p.y; }
        double cross(const Point& p) const { return x * p.y - y * p.x; }
        double len2() const { return x * x + y * y; }
        double len() const { return sqrt(len2()); }
        
        Point rotate90() const { return Point(-y, x); }
        Point normalize() const { return *this / len(); }
    };
    
    struct Line {
        Point p, v;
        Line(Point p = Point(), Point v = Point()) : p(p), v(v) {}
        
        // 点在有向线的左侧
        bool onLeft(Point a) const {
            return v.cross(a - p) > EPS;
        }
    };
    
    // 半平面交
    vector<Point> halfplaneIntersection(vector<Line> lines) {
        // 按极角排序
        sort(lines.begin(), lines.end(), [](const Line& a, const Line& b) {
            return atan2(a.v.y, a.v.x) < atan2(b.v.y, b.v.x);
        });
        
        int n = lines.size();
        vector<Line> q(n);
        vector<Point> ans;
        int l = , r = -1;
        
        for (int i = ; i < n; i++) {
            while (l < r && !lines[i].onLeft(ans[r - 1])) r--;
            while (l < r && !lines[i].onLeft(ans[l])) l++;
            
            if (l > r) {
                q[++r] = lines[i];
            } else {
                if (abs(q[r].v.cross(lines[i].v)) < EPS) {
                    if (q[r].v.dot(lines[i].v) > ) {
                        if (!lines[i].onLeft(q[r].p)) {
                            q[r] = lines[i];
                        }
                    } else {
                        return vector<Point>(); // 无解
                    }
                } else {
                    q[++r] = lines[i];
                }
            }
            
            if (l < r) {
                Point dir = q[r].v.rotate90();
                double t = (q[r-1].p - q[r].p).cross(q[r-1].v) / dir.cross(q[r-1].v);
                ans.push_back(q[r].p + dir * t);
            }
        }
        
        while (l < r && !q[l].onLeft(ans[r - 1])) r--;
        if (r - l <= 1) return vector<Point>();
        
        Point dir = q[l].v.rotate90();
        double t = (q[r].p - q[l].p).cross(q[r].v) / dir.cross(q[r].v);
        ans.push_back(q[l].p + dir * t);
        
        return ans;
    }
    
    // 计算多边形面积
    double polygonArea(const vector<Point>& poly) {
        double area = ;
        int n = poly.size();
        for (int i = ; i < n; i++) {
            area += poly[i].cross(poly[(i + 1) % n]);
        }
        return abs(area) / 2.0;
    }
    
    int main() {
        ios::sync_with_stdio(false);
        cin.tie();
        
        int n;
        cin >> n;
        vector<Point> towers;
        
        for (int i = ; i < n; i++) {
            double ax, ay, bx, by;
            cin >> ax >> ay >> bx >> by;
            towers.push_back(Point(ax, ay));
            towers.push_back(Point(bx, by));
        }
        
        int m = towers.size();
        double ans = ;
        
        // 对每个塔,构建它的安全区域
        for (int i = ; i < m; i++) {
            vector<Line> lines;
            
            // 添加边界(一个大正方形)
            lines.push_back(Line(Point(-1000, -1000), Point(1, )));
            lines.push_back(Line(Point(1000, -1000), Point(, 1)));
            lines.push_back(Line(Point(1000, 1000), Point(-1, )));
            lines.push_back(Line(Point(-1000, 1000), Point(, -1)));
            
            // 添加与其他塔的垂直平分线
            for (int j = ; j < m; j++) {
                if (i == j) continue;
                
                Point mid = (towers[i] + towers[j]) / 2.0;
                Point dir = (towers[j] - towers[i]).rotate90();
                lines.push_back(Line(mid, dir));
            }
            
            vector<Point> safe_region = halfplaneIntersection(lines);
            if (!safe_region.empty()) {
                ans += polygonArea(safe_region);
            }
        }
        
        cout << fixed << setprecision(10) << ans << endl;
        
        return ;
    }
    

    算法正确性

    定理:上述算法能正确计算安全区域的面积。

    证明

    1. 完备性:安全点必须位于某个塔的 Voronoi 区域内
    2. 正确性:半平面交精确地计算了 Voronoi 区域的交集
    3. 无重复:每个安全点只被一个塔的 Voronoi 区域包含

    复杂度分析

    • 外层循环O(2n)O(2n)
    • 内层循环O(2n)O(2n) 构建半平面
    • 半平面交O(nlogn)O(n \log n)
    • 总复杂度O(n3logn)O(n^3 \log n),对于 n100n \leq 100 可接受

    优化版本

    对于大规模数据,可以使用更高效的算法:

    // 优化:预先计算所有垂直平分线
    vector<Line> getAllBisectors(const vector<Point>& towers) {
        vector<Line> bisectors;
        int m = towers.size();
        
        for (int i = ; i < m; i++) {
            for (int j = i + 1; j < m; j++) {
                Point mid = (towers[i] + towers[j]) / 2.0;
                Point dir = (towers[j] - towers[i]).rotate90();
                bisectors.push_back(Line(mid, dir));
            }
        }
        
        return bisectors;
    }
    

    算法标签

    • 计算几何
    • 几何图形的交与并

    总结

    本题的核心在于将安全区域问题转化为 Voronoi 图的交集问题,通过半平面交算法精确计算面积。虽然复杂度较高,但在给定的数据范围内完全可行。

    • 1

    信息

    ID
    5612
    时间
    1000ms
    内存
    256MiB
    难度
    10
    标签
    递交数
    1
    已通过
    1
    上传者