文章目录
扫描线
一、问题引入
对于给定坐标的几个矩形,如何求取它们覆盖的面积?或者,如何求取它们构成的不规则图形的周长?
最暴力的方式就是将所有矩形的面积加起来,再减去相邻矩形重合的面积。但是这种O(n2)的算法显然没有那么优雅。如何优雅地解决这个问题呢?我们引入扫描线算法。
二、算法描述
想象有一根竖直的线从左向右进行扫描,如图蓝色部分所示:
OI Wiki上是从下往上扫描,本质上是一样的:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MQeZDf7H-1663490250819)(https://pic.leetcode-cn.com/1663294002-IWgpYx-scanning.svg)]
每当扫描到一个矩形的边缘时,前后两条扫描线就可能与原来的不规则图形构成一个规则的矩形。如此一来,不管是不规则图形的面积还是周长,都可以通过这些规则的矩形算出。
假设所有矩形的横坐标都是从0开始,那么,扫描线应该如何移动呢?从0开始逐次加1?当然不是。
我们再看一眼上图,扫描线构成规则矩形时,都是在扫描到原矩形的某一条边缘时。因此,真正有效的扫描线,也就是发生在这些边缘上。所以, 从小到大遍历原矩形的边缘,就是扫描线的正确扫描方式。
扫描线是一种离散化的技巧,将大范围的连续的坐标转化成 2n 个离散的坐标。
那么当扫描线扫描到边缘时,前后两条扫描线构成的规则矩形的宽就是扫描线的间距,可是高如何确定呢?
同一边缘处,可能有多个矩形,矩形之间可以是重叠的,也可以是有"镂空"的。那么一条边缘处的有效线段长度如何求取呢?线段树。
当我们获取到边缘处的有效线段长度后,以上图为例:
蓝线下的数字表示该扫描线扫描到的有效线段长度。
左右两条扫描线形成的矩形面积就等于:扫描线间距*有效线段长度。
当我们扫描到一个矩形的左边缘时,我们将其y方向上的线段加入线段树,当扫描到一个矩形的右边缘时,我们将其y方向上的线段从线段树中删除。最后,只需要在遍历边缘时动态查询线段树中有效线段的长度即可。
三、算法实现
LeetCode 850. 矩形面积 II
struct Segtree {
int cover;
int length;
int max_length;
};
class Solution {
public:
int rectangleArea(vector<vector<int>>& rectangles) {
int n = rectangles.size();
for (const auto& rect: rectangles) {
// 下边界
hbound.push_back(rect[1]);
// 上边界
hbound.push_back(rect[3]);
}
sort(hbound.begin(), hbound.end());
hbound.erase(unique(hbound.begin(), hbound.end()), hbound.end());
int m = hbound.size();
// 线段树有 m-1 个叶子节点,对应着 m-1 个会被完整覆盖的线段,需要开辟 ~4m 大小的空间
tree.resize(m * 4 + 1);
init(1, 1, m - 1);
vector<tuple<int, int, int>> sweep;
for (int i = 0; i < n; ++i) {
// 左边界
sweep.emplace_back(rectangles[i][0], i, 1);
// 右边界
sweep.emplace_back(rectangles[i][2], i, -1);
}
sort(sweep.begin(), sweep.end());
long long ans = 0;
for (int i = 0; i < sweep.size(); ++i) {
int j = i;
while (j + 1 < sweep.size() && get<0>(sweep[i]) == get<0>(sweep[j + 1])) {
++j;
}
if (j + 1 == sweep.size()) {
break;
}
// 一次性地处理掉一批横坐标相同的左右边界
for (int k = i; k <= j; ++k) {
auto&& [_, idx, diff] = sweep[k];
// 使用二分查找得到完整覆盖的线段的编号范围
int left = lower_bound(hbound.begin(), hbound.end(), rectangles[idx][1]) - hbound.begin() + 1;
int right = lower_bound(hbound.begin(), hbound.end(), rectangles[idx][3]) - hbound.begin();
update(1, 1, m - 1, left, right, diff);
}
ans += static_cast<long long>(tree[1].length) * (get<0>(sweep[j + 1]) - get<0>(sweep[j]));
i = j;
}
return ans % static_cast<int>(1e9 + 7);
}
void init(int idx, int l, int r) {
tree[idx].cover = tree[idx].length = 0;
if (l == r) {
tree[idx].max_length = hbound[l] - hbound[l - 1];
return;
}
int mid = (l + r) / 2;
init(idx * 2, l, mid);
init(idx * 2 + 1, mid + 1, r);
tree[idx].max_length = tree[idx * 2].max_length + tree[idx * 2 + 1].max_length;
}
void update(int idx, int l, int r, int ul, int ur, int diff) {
if (l > ur || r < ul) {
return;
}
if (ul <= l && r <= ur) {
tree[idx].cover += diff;
pushup(idx, l, r);
return;
}
int mid = (l + r) / 2;
update(idx * 2, l, mid, ul, ur, diff);
update(idx * 2 + 1, mid + 1, r, ul, ur, diff);
pushup(idx, l, r);
}
void pushup(int idx, int l, int r) {
if (tree[idx].cover > 0) {
tree[idx].length = tree[idx].max_length;
}
else if (l == r) {
tree[idx].length = 0;
}
else {
tree[idx].length = tree[idx * 2].length + tree[idx * 2 + 1].length;
}
}
private:
vector<Segtree> tree;
vector<int> hbound;
};
四、加餐练习
LeetCode 218. 天际线问题
class Solution
{
public:
// 观察题目我们可以发现,关键点的横坐标总是落在建筑的左右边缘上。
// 这样我们可以只考虑每一座建筑的边缘作为横坐标
// 这样其对应的纵坐标为:包含该横坐标的所有建筑的最大高度。
// 注:根据观察,建筑的右边缘的高度我们视为0
// 一种做法就是使用线段树:
// 遍历所有的建筑边缘,如果最大高度比起前一个发生变化,则为关键点。
// 第二种扫描线的思路:
// 我们将所有建筑的左右边缘保存至edges数组并升序排序,从小到大进行"扫描"。
// 同时,使用堆优化查找某一下标处建筑的最大高度。
// 具体地:
// 我们知道,buildings都是非降序排序的,因此,我们维护一个指针idx。
// 当遍历到边缘edge时,我们将所有buildings[idx][0]==edge的右边缘和高度入队。
// 即保存buildings[idx][1]和buildings[idx][2],堆顶保存当前扫描到的高度的最大值
// 同时,将堆顶元素中右边缘在edge处或edge左边的出队。
// 此时,再查询堆顶,如果非空,则该元素就是edge处的最大高度,否则,最大高度为0。
vector<vector<int>> getSkyline(vector<vector<int>>& buildings)
{
// 将所有建筑的边缘进行升序排序
vector<int> edges;
for (auto building : buildings)
{
edges.emplace_back(building[0]);
edges.emplace_back(building[1]);
}
sort(edges.begin(), edges.end());
vector<vector<int>> res;
priority_queue<pair<int, int>> pq;
int idx = 0;
for (auto edge : edges)
{
// 边缘与扫描线重合的建筑高度入队
while (idx < buildings.size() && buildings[idx][0] == edge)
{
pq.push({buildings[idx][2], buildings[idx][1]});
++idx;
}
// 将已经被扫描过的建筑高度出队
while (!pq.empty() && pq.top().second <= edge)
{
pq.pop();
}
// 避免当前关键点与前一个构成相同高度的水平线
int maxh = pq.empty() ? 0 : pq.top().first;
if (res.size() == 0 || res.back()[1] != maxh)
res.push_back({ edge, maxh });
}
return res;
}
};