题解: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
【核心思想】
-
问题分析:给定 \(n\) 个矩形,每个矩形由左上角 \((x_1, y_1)\) 和右下角 \((x_2, y_2)\) 确定。要求计算所有矩形的面积并(重叠区域只计算一次)。这是一个扫描线 + 线段树 + 离散化问题。
-
算法选择:
- 扫描线(Sweep Line):将每个矩形拆分为左右两条竖直边,按 \(x\) 坐标排序后从左到右扫描,将二维面积问题转化为一维线段覆盖长度问题
- 线段树(Segment Tree):维护 \(y\) 轴方向上的区间覆盖信息,支持区间加减和查询总覆盖长度
- 离散化(Coordinate Compression):将所有矩形的 \(y\) 坐标压缩,减少线段树节点数量,使复杂度从 \(O(y_{max})\) 降至 \(O(n)\)
-
关键步骤:
- 初始化:读取 \(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
- 对于每个矩形 \((x_1, y_1, x_2, y_2)\):
- 离散化 \(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\) 范围加入/移除覆盖
- 对于第 \(i\) 条扫描线:
- 输出答案:
printf("Total explored area: %.2lf", res)
- 初始化:读取 \(n\)(矩形数量),清空离散化数组
-
时间/空间复杂度:
- 时间复杂度:\(O(n \log n)\),排序 \(O(n \log n)\),每条边对应一次线段树更新 \(O(\log n)\),共 \(2n\) 条边
- 空间复杂度:\(O(n)\),离散化数组和线段树均需要 \(O(n)\) 空间
-
扫描线 + 线段树的核心思想:
- 降维思想:将二维面积并问题转化为一维线段覆盖问题。固定 \(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
浙公网安备 33010602011771号