题目地址:
https://www.acwing.com/problem/content/249/
有几个古希腊书籍中包含了对传说中的亚特兰蒂斯岛的描述。其中一些甚至包括岛屿部分地图。但不幸的是,这些地图描述了亚特兰蒂斯的不同区域。您的朋友Bill必须知道地图的总面积。你自告奋勇写了一个计算这个总面积的程序。
输入格式:
输入包含多组测试用例。对于每组测试用例,第一行包含整数
n
n
n,表示总的地图数量。接下来
n
n
n行,描绘了每张地图,每行包含四个数字
x
1
,
y
1
,
x
2
,
y
2
x_1,y_1,x_2,y_2
x1,y1,x2,y2(不一定是整数),
(
x
1
,
y
1
)
(x_1,y_1)
(x1,y1)和
(
x
2
,
y
2
)
(x_2,y_2)
(x2,y2)分别是地图的左上角位置和右下角位置。注意,坐标轴
x
x
x轴从上向下延伸,
y
y
y轴从左向右延伸。当输入用例
n
=
0
n=0
n=0时,表示输入终止,该用例无需处理。
输出格式:
每组测试用例输出两行。第一行输出Test case #k
,其中
k
k
k是测试用例的编号,从
1
1
1开始。第二行输出Total explored area: a
,其中a
是总地图面积(即此测试用例中所有矩形的面积并,注意如果一片区域被多个地图包含,则在计算总面积时只计算一次),精确到小数点后两位数。在每个测试用例后输出一个空行。
数据范围:
1
≤
n
≤
10000
1≤n≤10000
1≤n≤10000
0
≤
x
1
<
x
2
≤
100000
0≤x_1<x_2≤100000
0≤x1<x2≤100000
0
≤
y
1
<
y
2
≤
100000
0≤y_1<y_2≤100000
0≤y1<y2≤100000
注意,本题
n
n
n的范围上限加强至
10000
10000
10000。
思路是扫描线 + 线段树。首先对于
x
x
x和
y
y
y的方向,我们可以直接以数学里的直角坐标系的规定来看,这不影响答案的正确性。接着,可以用”积分“的思想,将整个图形划分为若干不相交的矩形。想象有条竖直的线,从左向右扫描,在扫描的过程中,扫过的部分一定能写为若干不相交矩形的并。所以我们只需要将每部分矩形的面积总和加起来就行了。如下图所示:
想象一个数轴(即图中的
y
y
y轴),对于这个
y
y
y轴要做两种操作,一个是将某个区间的权值整体加
1
1
1或
−
1
-1
−1(分别代表扫描线扫到了矩形左右边界),另一个是询问当前有多少长度的权值为正。可以用线段树来做。构造一个线段树,我们先将所有的
y
y
y坐标做离散化(主要原因是
y
y
y坐标不一定是整数,需要先离散化为整数才方便线段树进行操作),设离散化后一共
n
n
n个点,则有
n
−
1
n-1
n−1个区间,让这些区间被线段树所维护,线段树每个节点存的是维护的区间的两个端点区间的下标(这里的意思是,若该节点维护的区间是
[
a
1
,
a
k
]
=
[
a
1
,
a
2
]
∪
[
a
2
,
a
3
]
∪
.
.
.
∪
[
a
k
−
1
,
a
k
]
[a_1,a_k]=[a_1,a_2]\cup [a_2,a_3]\cup...\cup [a_{k-1},a_k]
[a1,ak]=[a1,a2]∪[a2,a3]∪...∪[ak−1,ak],那么该节点需要存储
[
a
1
,
a
2
]
[a_1,a_2]
[a1,a2]和
[
a
k
−
1
,
a
k
]
[a_{k-1},a_k]
[ak−1,ak]的下标),以及该区间被覆盖的长度和覆盖次数(即权值)。修改操作只会修改覆盖次数。其两个操作以及pushup操作如下:
1、pushup操作:如果当前区间权值为正,则其覆盖长度等于其维护的区间的长度;如果当前区间权值为
0
0
0(当然不可能为负),则看一下是否是叶子节点(即是否维护的区间是单个”原子“区间),如果不是,则其覆盖长度就是其两个儿子覆盖长度之和;否则覆盖长度是
0
0
0;
2、查询操作:只会查询树根的覆盖长度;
3、修改操作:如果当前节点的区间被完全覆盖,则直接增加或减少权值,否则递归修改左右儿子,最后pushup。
我们看到,由于查询操作只会对树根查询,所以不需要pushdown。对于修改操作,由于操作是成对出现的,并且成对的两次操作是对同样的区间进行了先增 1 1 1后减 1 1 1的操作,而且每次询问的答案只与每个区间被没被覆盖有关,而跟其覆盖了多少次无关(也就是说覆盖了 1 1 1次还是多次都不影响答案,答案只与次数是不是 0 0 0有关),所以这里是不需要pushdown的(具体可以用数学归纳法证明。每次修改操作可以视为是对线段树中的若干个节点的权值做了增 1 1 1或减 1 1 1的操作,可以归纳假设每次操作完,每个节点的权值是计算正确的,这里的计算正确的意思是,它本身的权值只和直接对它自己的操作有关,不考虑其父亲被操作的影响。那么每个节点维护的覆盖长度在pushup的作用下计算是正确的。接下来考虑下一次操作的影响,如果其是将权值加 1 1 1,则pushup完之后每个区间的覆盖长度仍然正确;如果是减 1 1 1,那么就是将之前加过的某若干个区间权值都减 1 1 1,且减完仍然非负,这样pushup完之后每个区间的覆盖长度也仍然正确)。代码如下:
#include <iostream>
#include <cstring>
#include <algorithm>
#include <vector>
using namespace std;
const int N = 100010;
int n;
struct Segment {
double x, y1, y2;
int k;
bool operator<(const Segment &t) const { return x < t.x; }
} seg[N * 2];
struct Node {
// l和r存的是当前节点维护的区间的两个端点区间在离散化情况下的下标
int l, r;
// cnt存当前节点维护的区间整体的权值
int cnt;
// len存当前节点维护的区间内被覆盖的长度
double len;
} tr[N * 8];
vector<double> ys;
// 找到y离散化后的下标
int find(double y) {
return lower_bound(ys.begin(), ys.end(), y) - ys.begin();
}
// pushup只为了更新len
void pushup(int u) {
if (tr[u].cnt) tr[u].len = ys[tr[u].r + 1] - ys[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;
}
}
void build(int u, int l, int r) {
tr[u] = {l, r};
if (l == r) return;
int mid = l + r >> 1;
build(u << 1, l, mid), build(u << 1 | 1, mid + 1, r);
}
void modify(int u, int l, int r, int k) {
if (tr[u].l >= l && tr[u].r <= r) {
tr[u].cnt += k;
} else {
int mid = tr[u].l + tr[u].r >> 1;
if (l <= mid) modify(u << 1, l, r, k);
if (r > mid) modify(u << 1 | 1, l, r, k);
}
pushup(u);
}
int main() {
int T = 1;
while (cin >> n, n) {
ys.clear();
for (int i = 0, j = 0; i < n; i++) {
double x1, y1, x2, y2;
scanf("%lf%lf%lf%lf", &x1, &y1, &x2, &y2);
seg[j++] = {x1, y1, y2, 1};
seg[j++] = {x2, y1, y2, -1};
ys.push_back(y1), ys.push_back(y2);
}
// 执行对y坐标的离散化,先排序,然后去重
sort(ys.begin(), ys.end());
ys.erase(unique(ys.begin(), ys.end()), ys.end());
build(1, 0, ys.size() - 2);
sort(seg, seg + n * 2);
double res = 0;
for (int i = 0; i < n * 2; i++) {
if (i) res += tr[1].len * (seg[i].x - seg[i - 1].x);
modify(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);
}
}
每组数据时间复杂度 O ( n log n ) O(n\log n) O(nlogn),空间 O ( n ) O(n) O(n)。