题解:AcWing 247 亚特兰蒂斯

【题目来源】

AcWing:247. 亚特兰蒂斯 - AcWing题库

【题目描述】

有几个古希腊书籍中包含了对传说中的亚特兰蒂斯岛的描述。

其中一些甚至包括岛屿部分地图。

但不幸的是,这些地图描述了亚特兰蒂斯的不同区域。

您的朋友 Bill 必须知道地图的总面积。

你自告奋勇写了一个计算这个总面积的程序。

【输入】

输入包含多组测试用例。

对于每组测试用例,第一行包含整数 \(n\),表示总的地图数量。

接下来 \(n\) 行,描绘了每张地图,每行包含四个数字 \(x_1,y_1,x_2,y_2\)(不一定是整数),\((x_1,y_1)\) 和 \((x_2,y_2)\) 分别是地图的左上角位置和右下角位置。

注意,坐标轴 \(x\) 轴从上向下延伸,\(y\) 轴从左向右延伸。

当输入用例 \(n=0\) 时,表示输入终止,该用例无需处理。

【输出】

每组测试用例输出两行。

第一行输出 Test case #k,其中 \(k\) 是测试用例的编号,从 \(1\) 开始。

第二行输出 Total explored area: a,其中 \(a\) 是总地图面积(即此测试用例中所有矩形的面积并,注意如果一片区域被多个地图包含,则在计算总面积时只计算一次),精确到小数点后两位数。

在每个测试用例后输出一个空行。

【输入样例】

2
10 10 20 20
15 15 25 25.5
0

【输出样例】

Test case #1
Total explored area: 180.00

【核心思想】

  1. 问题分析:给定 \(n\) 个矩形,每个矩形由左上角 \((x_1, y_1)\) 和右下角 \((x_2, y_2)\) 确定。要求计算所有矩形的面积并(重叠区域只计算一次)。这是一个扫描线 + 线段树 + 离散化问题。

  2. 算法选择

    • 扫描线(Sweep Line):将每个矩形拆分为左右两条竖直边,按 \(x\) 坐标排序后从左到右扫描,将二维面积问题转化为一维线段覆盖长度问题
    • 线段树(Segment Tree):维护 \(y\) 轴方向上的区间覆盖信息,支持区间加减和查询总覆盖长度
    • 离散化(Coordinate Compression):将所有矩形的 \(y\) 坐标压缩,减少线段树节点数量,使复杂度从 \(O(y_{max})\) 降至 \(O(n)\)
  3. 关键步骤

    • 初始化:读取 \(n\)(矩形数量),清空离散化数组 alls 和边数组 seg
    • 拆分为扫描线
      • 对于每个矩形 \((x_1, y_1, x_2, y_2)\)
        • 左边(入边):\((x_1, y_1, y_2, k=+1)\),表示在 \(x=x_1\)\([y_1, y_2]\) 区间增加一层覆盖
        • 右边(出边):\((x_2, y_1, y_2, k=-1)\),表示在 \(x=x_2\)\([y_1, y_2]\) 区间减少一层覆盖
        • \(y_1, y_2\) 加入离散化数组 alls
    • 离散化 \(y\) 坐标
      • sort(alls.begin(), alls.end())
      • alls.erase(unique(...), alls.end())
      • 线段树节点 \(i\) 代表实际区间 \([alls[i], alls[i+1]]\)
    • 建立线段树:维护区间 \([0, alls.size()-2]\),初始覆盖次数 cnt=0,覆盖长度 len=0
    • \(x\) 排序扫描线sort(seg, seg + 2n)
    • 扫描过程(从左到右):
      • 对于第 \(i\) 条扫描线:
        • 计算面积:若 \(i > 0\),面积增量 \(\Delta S = tr[1].len \times (seg[i].x - seg[i-1].x)\),累加到答案
        • 更新线段树update(1, find(y1), find(y2)-1, k),将当前边的 \(y\) 范围加入/移除覆盖
    • 输出答案printf("Total explored area: %.2lf", res)
  4. 时间/空间复杂度

    • 时间复杂度:\(O(n \log n)\),排序 \(O(n \log n)\),每条边对应一次线段树更新 \(O(\log n)\),共 \(2n\) 条边
    • 空间复杂度:\(O(n)\),离散化数组和线段树均需要 \(O(n)\) 空间
  5. 扫描线 + 线段树的核心思想

    • 降维思想:将二维面积并问题转化为一维线段覆盖问题。固定 \(x\) 方向后,只需知道当前 \(y\) 方向的覆盖总长度,乘以 \(x\) 方向宽度即可得到该条带的面积
    • 入边出边机制:每条扫描线带有 \(k=+1\)(入边)或 \(k=-1\)(出边)标记,线段树维护覆盖次数 cnt。若 cnt > 0 表示该区间被覆盖,len 为区间实际长度;否则 len 继承子节点之和
    • 离散化优化:坐标范围很大时,将所有端点坐标排序去重后映射为连续下标,线段树只需维护 \(O(n)\) 个区间而非 \(O(y_{max})\)
    • 区间与点对应:线段树节点 \(i\) 对应实际区间 \([alls[i], alls[i+1]]\),因此更新时右端点需用 find(y2)-1
    • 适用于矩形面积并、矩形周长并、多维前缀和/差分等几何覆盖问题

【算法标签】

线段树

【代码详解】

#include <bits/stdc++.h>
using namespace std;
const int N = 100005;           // 最大线段数量
int n;                          // 矩形数量

// 扫描线结构:每条边对应一条扫描线
struct Seg
{
    double x, y1, y2;           // x: 边的x坐标, y1,y2: 边的y范围(上下端点)
    int k;                      // k=1 表示矩形左边(入边), k=-1 表示矩形右边(出边)
} seg[N * 2];                   // 每个矩形产生2条边(左右),共2n条

// 线段树节点:维护y轴方向的覆盖信息
struct Node
{
    int l, r;                   // 节点代表的y区间在离散化数组中的下标范围
    int cnt;                    // 当前区间被完全覆盖的次数
    double len;                 // 当前区间内被覆盖的实际长度
} tr[N * 8];                    // 线段树数组(8倍空间)

vector<double> alls;            // 离散化数组:存储所有y坐标,用于压缩坐标

// 扫描线按x坐标升序排序
bool operator<(Seg a, Seg b)
{
    return a.x < b.x;
}

// 在离散化数组中查找x的位置(二分查找,返回最大的满足 alls[i] <= x 的下标)
int find(double x)
{
    int l = 0, r = alls.size() - 1;
    while (l < r)
    {
        int mid = (l + r + 1) >> 1;
        if (alls[mid] <= x)
            l = mid;
        else
            r = mid - 1;
    }
    return l;
}

// 向上更新:根据子节点信息计算父节点的覆盖长度
// 核心思想:若当前区间被覆盖(cnt>0),则覆盖长度=区间实际长度
// 否则,若当前是叶子节点,长度为0;否则长度=左右子节点长度之和
void pushup(int u)
{
    if (tr[u].cnt)
        tr[u].len = alls[tr[u].r + 1] - alls[tr[u].l];  // 被覆盖,长度为整个区间
    else if (tr[u].l != tr[u].r)
    {
        tr[u].len = tr[u << 1].len + tr[u << 1 | 1].len;  // 未被覆盖,继承子节点
    }
    else
        tr[u].len = 0;  // 叶子节点且未被覆盖,长度为0
}

// 建立线段树
void build(int u, int l, int r)
{
    if (l == r)
        tr[u] = {l, r, 0, 0};   // 叶子节点:cnt=0, len=0
    else
    {
        tr[u] = {l, r};         // 初始化区间
        int mid = l + r >> 1;
        build(u << 1, l, mid), build(u << 1 | 1, mid + 1, r);  // 递归建左右子树
        pushup(u);              // 向上更新
    }
}

// 区间更新:在区间[l,r]上增加覆盖次数d(+1表示入边,-1表示出边)
void update(int u, int l, int r, int d)
{
    if (tr[u].l >= l && tr[u].r <= r)  // 完全覆盖
    {
        tr[u].cnt += d;         // 增加/减少覆盖次数
        pushup(u);              // 更新覆盖长度
    }
    else
    {
        int mid = tr[u].l + tr[u].r >> 1;
        if (l <= mid) update(u << 1, l, r, d);   // 左子树有交集
        if (r > mid) update(u << 1 | 1, l, r, d); // 右子树有交集
        pushup(u);              // 向上更新
    }
}

int main()
{
    int T = 1;                  // 测试用例编号
    while (cin >> n, n)         // 读入n,当n=0时结束
    {
        alls.clear();           // 清空离散化数组
        int cur = 0;            // 当前边数计数器

        // 读入n个矩形,每个矩形拆分为左右两条扫描线
        for (int i = 0; i < n; i++)
        {
            double x1, y1, x2, y2;
            cin >> x1 >> y1 >> x2 >> y2;
            // 矩形左边(入边):x=x1, y范围[y1,y2], 覆盖+1
            seg[cur++] = {x1, y1, y2, 1};
            // 矩形右边(出边):x=x2, y范围[y1,y2], 覆盖-1
            seg[cur++] = {x2, y1, y2, -1};
            // 收集y坐标用于离散化
            alls.push_back(y1), alls.push_back(y2);
        }

        // 离散化y坐标:排序并去重
        sort(alls.begin(), alls.end());
        alls.erase(unique(alls.begin(), alls.end()), alls.end());

        // 建立线段树,维护y轴区间[0, alls.size()-2]
        // 注意:线段树节点i代表区间[alls[i], alls[i+1]]
        build(1, 0, alls.size() - 2);

        // 按x坐标排序所有扫描线
        sort(seg, seg + n * 2);

        // 扫描线算法:从左到右扫描
        double res = 0;         // 总面积
        for (int i = 0; i < n * 2; i++)
        {
            // 计算当前扫描线与上一条扫描线之间的矩形面积
            // 面积 = y方向覆盖长度 * x方向宽度
            if (i > 0)
                res += tr[1].len * (seg[i].x - seg[i - 1].x);

            // 更新线段树:将当前边的y范围加入/移除覆盖
            // find(seg[i].y1) 找到y1对应的离散化下标
            // find(seg[i].y2)-1 找到y2对应的离散化下标的前一个(因为线段树节点代表[y_i, y_{i+1}])
            update(1, find(seg[i].y1), find(seg[i].y2) - 1, seg[i].k);
        }

        // 输出结果
        printf("Test case #%d\n", T++);
        printf("Total explored area: %.2lf\n\n", res);
    }
    return 0;
}

【运行结果】

2
10 10 20 20
15 15 25 25.5
Test case #1
Total explored area: 180.00

0
posted @ 2026-06-25 09:11  团爸讲算法  阅读(16)  评论(0)    收藏  举报