• 扫描线及其应用


    扫描线

    一、问题引入

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

    image-20220917171944593

    最暴力的方式就是将所有矩形的面积加起来,再减去相邻矩形重合的面积。但是这种O(n2)的算法显然没有那么优雅。如何优雅地解决这个问题呢?我们引入扫描线算法。

    二、算法描述

    image-20220917173701209

    想象有一根竖直的线从左向右进行扫描,如图蓝色部分所示:

    image-20220917194402351

    OI Wiki上是从下往上扫描,本质上是一样的:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MQeZDf7H-1663490250819)(https://pic.leetcode-cn.com/1663294002-IWgpYx-scanning.svg)]

    每当扫描到一个矩形的边缘时,前后两条扫描线就可能与原来的不规则图形构成一个规则的矩形。如此一来,不管是不规则图形的面积还是周长,都可以通过这些规则的矩形算出。

    假设所有矩形的横坐标都是从0开始,那么,扫描线应该如何移动呢?从0开始逐次加1?当然不是。

    我们再看一眼上图,扫描线构成规则矩形时,都是在扫描到原矩形的某一条边缘时。因此,真正有效的扫描线,也就是发生在这些边缘上。所以, 从小到大遍历原矩形的边缘,就是扫描线的正确扫描方式。

    扫描线是一种离散化的技巧,将大范围的连续的坐标转化成 2n 个离散的坐标。

    那么当扫描线扫描到边缘时,前后两条扫描线构成的规则矩形的宽就是扫描线的间距,可是高如何确定呢?

    image-20220917195242288

    同一边缘处,可能有多个矩形,矩形之间可以是重叠的,也可以是有"镂空"的。那么一条边缘处的有效线段长度如何求取呢?线段树

    当我们获取到边缘处的有效线段长度后,以上图为例:

    image-20220917200435088

    蓝线下的数字表示该扫描线扫描到的有效线段长度。

    左右两条扫描线形成的矩形面积就等于:扫描线间距*有效线段长度。

    当我们扫描到一个矩形的左边缘时,我们将其y方向上的线段加入线段树,当扫描到一个矩形的右边缘时,我们将其y方向上的线段从线段树中删除。最后,只需要在遍历边缘时动态查询线段树中有效线段的长度即可。

    三、算法实现

    LeetCode 850. 矩形面积 II

    struct Segtree {
        int cover;
        int length;
        int max_length;
    };
    
    class Solution {
    public:
        int rectangleArea(vector>& 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> 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(tree[1].length) * (get<0>(sweep[j + 1]) - get<0>(sweep[j]));
                i = j;
            }
            return ans % static_cast(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 tree;
        vector hbound;
    };
     
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99

    四、加餐练习

    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> getSkyline(vector>& buildings) 
        {
            
            // 将所有建筑的边缘进行升序排序
            vector edges;
            for (auto building : buildings)
            {
                edges.emplace_back(building[0]);
                edges.emplace_back(building[1]);
            }
            sort(edges.begin(), edges.end());
    
            vector> res;
            priority_queue> 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;
        }
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
  • 相关阅读:
    适用于大中型银行的云原生技术体系建设方案
    三分钟数据持久化:Spring Boot, JPA 与 SQLite 的完美融合
    一起Talk Android吧(第四百零六回:管理画布canvas)
    软件测试之项目实战,必须知道的事与测试面试项目测试流程......
    Contention Based Energy Efficient Wireless Sensor Network – A survey
    华为云云耀云服务器L实例评测|了解配置和管理L型云服务器
    【毕业设计】医学大数据分析 - 心血管疾病分析
    Failed to remove multipath map 320b508ca45022b80
    安卓讲课笔记5.5 Fragment入门
    我只用CV大法就实现了小程序的tabBar
  • 原文地址:https://blog.csdn.net/Wyf_Fj/article/details/126919472