扫描线及其应用

Posted 白龙码~

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了扫描线及其应用相关的知识,希望对你有一定的参考价值。

文章目录

扫描线

一、问题引入

对于给定坐标的几个矩形,如何求取它们覆盖的面积?或者,如何求取它们构成的不规则图形的周长?

最暴力的方式就是将所有矩形的面积加起来,再减去相邻矩形重合的面积。但是这种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;
    
;

以上是关于扫描线及其应用的主要内容,如果未能解决你的问题,请参考以下文章

扫描线算法(天际线问题)

[H扫描线] lc218. 天际线问题(扫描线求轮廓+边界情况+好题+算法技巧)

LeetCode 218. 天际线问题(扫描线)/ 1818. 绝对差值和/ 1846. 减小和重新排列数组后的最大元素

综合笔试题难度 4.5/5,扫描线的特殊运用(详尽答疑)

合并天际线,分而治之

Leetcode:The Skyline Problem