题目链接:Atlantis
大致题意
现在有平面直角坐标系xoy, 有n个平行于x轴y轴的矩形, 给出这些矩形的左上角和右下角的坐标, 让你求出这些矩形组成的图形的总面积.
解题思路
本题是 线段树 + 扫描线 的典型例题, 是十分经典的模版题.
分析:
例如需要求下图所示的矩形的面积总和:
我们不妨将所有矩形的所有边都延长, 然后将整个坐标轴划分成一个个小区域:
然后再将这些小区域按照垂直于x轴的直线再次划分成一个个小矩形(如下图阴影所示):
现在我们需要计算的总面积可以拆分成图中阴影部分的面积之和, 这样我们如果从小往大枚举x的取值, 必然每两个x1与x2之间会有一个小矩形, 我们只需要把这些小矩形的面积加起来即可.
我们发现, 每两个相邻的x1与x2我们可以通过把x的坐标排序后得到, 那在y轴部分的长度如何计算呢?
这时候就用到了我们的线段树, 我们需要用线段树维护在当前[x1, x2]区间, 小矩形在y轴上线段的总长度.
做法:
维护每两条平行于x轴的线段, 线段树内部记录当前区间出现的次数, 以及当前区间已经覆盖的总长度, 以及懒标记.
假设离散化后用到的y轴坐标有len个, 那么我们就需要维护len-1条线段. 每个矩形必然有两条平行于y轴的线, 在我们从小到大枚举x的过程中, 当我们遇到左侧的线段时, 我们给对应的线段**+1**, 当遇到右侧时, 则**-1**.
如果当前区间的线段有出现过, 则总长度就要加上这段线段的长度.
这里其实有很多的细节问题, 如: 对于当前区间+1无所谓, 因为:
①如果当前区间没有覆盖过, 则覆盖的长度就等于当前区间的长度
②如果当前区间覆盖过, 则多覆盖一次也不影响当前区间覆盖的长度.
而如果是区间-1, 则出现问题了, 我们如何把懒标记下传? 当前区间-1, 则其子区间每个都需要-1, 有的会减为0, 有的则不会. 我们总不能每次都遍历到叶子结点. (但是有方法是可以合理实现的QAQ)
优化:
由于上面维护懒标记的方法太麻烦了, 我们这里说一下不用懒标记实现的方法.
需要用到而线段树+扫描线问题比较特殊, 由于扫描线特殊的性质, 我们每次修改时, +1和-1的操作是成对的, 对于这样成对的操作, 每次修改时下放的区间也是相同的. 所以我们不妨不去记录懒标记, 而去维护一个特殊的值, 这个值只表示这个区间是否被覆盖, 不再向下进行传递.
这样对于每次修改后, 如果当前区间被覆盖, 那么当前区间的长度就为所包含的线段长度. 反之则为左右儿子所包含的线段长度.
那么对于查询操作, 因为每次都只查询根节点, 而我们的pushup一定可以保证根节点的是最新的, 所以也是符合要求的.
结论:
对于这类特殊的 线段树 + 扫描线 的题目, 我们可以不用懒标记, 用个值去记录当前区间出现过几次即可.
特别的, 要特别注意线段树内部维护每个区间内部是线段! 上文中所说的区间也都只是线段树中区间中所包含的线段.
AC代码
#include <bits/stdc++.h>
#define rep(i, n) for (int i = 1; i <= (n); ++i)
using namespace std;
typedef long long ll;
const int N = 110; //矩形个数
vector<double> v;
int find(double x) { return lower_bound(v.begin(), v.end(), x) - v.begin(); }
struct LINE { //垂直于x轴的线段记录.
double x, y1, y2; int c; //c表示矩形左侧线段还是右侧
bool operator< (const LINE& t) const { return x < t.x; }
}; vector<LINE> line; //若不用vector, 应开2倍空间
struct node {
int l, r;
int cou; //cou的意义是, 当前区间被覆盖几次(不下传)
double len;
}t[N << 3]; //特别注意, 应该开8倍空间!! 扫描线本身2倍 + 线段树自身4倍 = 8倍;
double getlen(int l, int r) { return v[r + 1] - v[l]; }
void pushup(int x) {
if (t[x].cou) t[x].len = getlen(t[x].l, t[x].r);
else if (t[x].l != t[x].r) t[x].len = t[x << 1].len + t[x << 1 | 1].len;
else t[x].len = 0;
}
void build(int l, int r, int x = 1) {
t[x] = { l, r, 0, 0 };
if (l == r) return;
int mid = l + r >> 1;
build(l, mid, x << 1), build(mid + 1, r, x << 1 | 1);
//不用pushup
}
void modify(int l, int r, int c, int x = 1) {
if (l <= t[x].l && r >= t[x].r) {
t[x].cou += c;
pushup(x); //特别注意, 别忘了修改后pushup!!!
return;
}
int mid = t[x].l + t[x].r >> 1;
if (l <= mid) modify(l, r, c, x << 1);
if (r > mid) modify(l, r, c, x << 1 | 1);
pushup(x);
}
int main()
{
int n; int T = 1;
while (scanf("%d", &n), n) {
v.clear(); line.clear(); v.push_back(-0x3f3f3f3f);
rep(i, n) {
double x1, y1, x2, y2; scanf("%lf%lf%lf%lf", &x1, &y1, &x2, &y2);
line.push_back({ x1, y1, y2, 1 });
line.push_back({ x2, y1, y2, -1 });
v.push_back(y1), v.push_back(y2); //离散化y
}
sort(v.begin(), v.end()); v.erase(unique(v.begin(), v.end()), v.end());
build(1, v.size() - 2); //因为是对(y轴的线段)建树. 有v.size()-1个点, 则线段应再-1.
sort(line.begin(), line.end());
double res = 0, last = 0; //last表示上一个x的位置
for (auto& op : line) {
res += (op.x - last) * t[1].len; //当前结果累加为△x * y轴覆盖长度
last = op.x;
modify(find(op.y1), find(op.y2) - 1, op.c);
}
printf("Test case #%d\n", T++);
printf("Total explored area: %.2f\n\n", res);
}
return 0;
}