5.扫描线练习题(LC-850.求矩形面积、LC-391)

文章介绍了扫描线算法的概念及其在解决完美矩形和矩形面积计算问题中的应用。通过将矩形边离散化,结合线段树数据结构,可以高效地处理区间修改和查询,从而确定矩形是否能精确覆盖以及计算总面积。文章提供了具体的LeetCode题目作为实例,展示了解决方案的思路和代码实现。
摘要由CSDN通过智能技术生成

扫描线(Sweep Line)

扫描线定义

扫描线法是一种求矩形面积并/周长并的好方法。

扫描线:假设有一条扫描线从一个图形的下方扫向上方(或者左方扫到右方),那么通过分析扫描线被图形截得的线段就能获得所要的结果。该过程可以用线段树进行加速。

面积的求法:

面积的求法其实可以简化为sum截线段长度times扫过的高度。这也是扫描线算法最基础的应用。具体:


我们可以画出一根线来,扫过这个多边形的所有面积。

这根线从最左边的边开始,每遇到一条边,就停下来计算之前扫到的面积,即为距上一条边的距离与扫描线落在图形上的长度之积,再加到之前的和上面。

用动画来做就是这样的:

**我们怎么才能知道每一次遇到一条边之后扫描线落在图像上的长度是多少 ?**如果我们每一次都去枚举、去找,我们程序的时间复杂度就达不到我们的要求了。

我们只需要计算每一段所对应的在 x 轴上的长度就行了。

我们可以使用线段树这一强大的数据结构来帮助我们更快地进行区间修改。
image.png

标记:
我们在遇到某个矩形的一条边的时候,我们需要看一下这两个东西:

  1. 它的长度;
  2. 他是起始边还是终止边。

如果是起始边的话,我们把他这条边所包含的所有区块都打上一个标记;如果是终止边的话,我们就把之前起始边打上的标记去掉。这样,只要有标记就是存在,不管有多少个;没有标记就是没有被覆盖,忽略不计。

所以,我们的线段树节点需要存储这个区段被标记的次数。同时,我们还需要存储基本的线段树信息,还有它的长度。

同时,我们用来存储矩形边的位置我们也需要改一下,使之能够存储这条边的上端点和下端点,还要标记这条边是起始边还是终止边。
最终我们使用动画模拟一下就是这个样子的:

391. 完美矩形

难度困难249

给你一个数组 rectangles ,其中 rectangles[i] = [xi, yi, ai, bi] 表示一个坐标轴平行的矩形。这个矩形的左下顶点是 (xi, yi) ,右上顶点是 (ai, bi)

如果所有矩形一起精确覆盖了某个矩形区域,则返回 true ;否则,返回 false

示例 1:

img

输入:rectangles = [[1,1,3,3],[3,1,4,2],[3,2,4,4],[1,3,2,4],[2,3,3,4]]
输出:true
解释:5 个矩形一起可以精确地覆盖一个矩形区域。 

示例 2:

img

输入:rectangles = [[1,1,2,3],[1,3,2,4],[3,1,4,2],[3,2,4,4]]
输出:false
解释:两个矩形之间有间隔,无法覆盖成一个矩形。

示例 3:

img

输入:rectangles = [[1,1,3,3],[3,1,4,2],[1,3,2,4],[2,2,4,4]]
输出:false
解释:因为中间有相交区域,虽然形成了矩形,但不是精确覆盖。

提示:

  • 1 <= rectangles.length <= 2 * 104
  • rectangles[i].length == 4

提示:

  • 1 <= rectangles.length <= 2 * 104
  • rectangles[i].length == 4
  • -105 <= xi, yi, ai, bi <= 105

题解:https://leetcode.cn/problems/perfect-rectangle/solution/gong-shui-san-xie-chang-gui-sao-miao-xia-p4q4/

扫描线解法

将每个矩形 rectangles[i]看做两条竖直方向的边,使用 (x, y1, y2) 的形式进行存储(其中 y1 代表该竖边的下端点,y2 代表竖边的上端点),同时为了区分是矩形的左边还是右边,再引入一个标识位,即以四元组 (x, y1, y2, flag) 的形式进行存储。

一个完美矩形的充要条件为:对于完美矩形的每一条非边缘的竖边,都「成对」出现(存在两条完全相同的左边和右边重叠在一起);对于完美矩形的两条边缘竖边,均独立为一条连续的(不重叠)的竖边。

图(红色框的为「完美矩形的边缘竖边」,绿框的为「完美矩形的非边缘竖边」):

在这里插入图片描述

  • 绿色:非边缘竖边必然有成对的左右两条完全相同的竖边重叠在一起;
  • 红色:边缘竖边由于只有单边,必然不重叠,且连接成一条完成的竖边。
class Solution {
    public boolean isRectangleCover(int[][] rectangles) {
        int len = rectangles.length * 2, ids = 0;
        int[][] re = new int[len][4];
        //初始化re数组,组成[横坐标,纵坐标下顶点,纵坐标上顶点,矩形的左边or右边标志]
        for(int[] i:rectangles){
            re[ids++] = new int[]{i[0],i[1],i[3],1};
            re[ids++] = new int[]{i[2],i[1],i[3],-1};
        }
        //排序,按照横坐标进行排序,横坐标相等就按纵坐标排序
        Arrays.sort(re,(o1,o2)-> o1[0]!=o2[0]?o1[0]-o2[0]:o1[1]-o2[1]);
        //操作每一个顶点,判断是否符合要求
        for(int i = 0; i < len;){
            //如果该边是矩形的左边界,就加入left
            List<int[]> left = new ArrayList<>();
            //如果该边是矩形的左边界,就加入right
            List<int[]> right = new ArrayList<>();
            //标志该边是不是 矩形的左边
            boolean flag = i == 0;
            // 找到所有等于i的边的右边界
            int x = i;
            while(x < len && re[x][0] == re[i][0]) x++;
            while(i < x){ //判断该横坐标的 边是不是符合要求
                // 判断当前坐标是矩形的左边还是右边,操作对应的边集合
                List<int[]> list = re[i][3] == 1 ? left : right;
                if(list.isEmpty()){
                    list.add(re[i++]);
                }else{
                    int[] pre = list.get(list.size() - 1);
                    int[] cur = re[i++];
                    // 有重叠,直接返回false
                    if(cur[1] < pre[2]) return false;
                    // 如果正好接触,则直接将前一个的点的上顶点变成当前点的上顶点
                    if(cur[1] == pre[2]) pre[2] = cur[2];
                    else list.add(cur); // 不接触,可能有空隙
                }
            }
            //判断边是中间边还是边界边
            if(!flag&&x<len){
                //如果是中间边 判断左右是不是相等
                if(left.size()!=right.size()) return false;
                for(int j = 0; j < left.size(); ++j){
                    if(left.get(j)[2]==right.get(j)[2]&&left.get(j)[1]==right.get(j)[1]) continue;
                    return false;
                }
            } else {
                //如果是边界边判断是不是一条
                if (left.size()!=1&&right.size()==0||left.size()==0&&right.size()!=1) return false;
            }
        }
        return true;
    }
}

850. 矩形面积 II

难度困难246

给你一个轴对齐的二维数组 rectangles 。 对于 rectangle[i] = [x1, y1, x2, y2],其中(x1,y1)是矩形 i 左下角的坐标, (xi1, yi1) 是该矩形 左下角 的坐标, (xi2, yi2) 是该矩形 右上角 的坐标。

计算平面中所有 rectangles 所覆盖的 总面积 。任何被两个或多个矩形覆盖的区域应只计算 一次

返回 总面积 。因为答案可能太大,返回 109 + 7

输入:rectangles = [[0,0,2,2],[1,0,2,3],[1,0,3,1]]
输出:6
解释:如图所示,三个矩形覆盖了总面积为6的区域。
从(1,1)(2,2),绿色矩形和红色矩形重叠。
从(1,0)(2,3),三个矩形都重叠。

朴素扫描线模板题

题解:https://leetcode.cn/problems/rectangle-area-ii/solution/gong-shui-san-xie-by-ac_oier-9r36/

class Solution {
    //将所有给定的矩形的左右边对应的 x 端点提取出来并排序,每个端点可看作是一条竖直的线段(红色),
    //问题转换为求解「由多条竖直线段分割开」的多个矩形的面积总和(黄色):
    //由于数据范围只有 200,我们可以对给定的所有矩形进行遍历,统计所有对该矩形有贡献的 y 值线段
    //(即有哪些 rs[i] 落在该矩形中),再对线段进行求交集(总长度),即可计算出该矩形的「高度」,
    // 从而计算出来该矩形的面积。
    int mod = (int)1e9+7;
    public int rectangleArea(int[][] rectangles) {
        //将所有矩形的x坐标存到list中
        List<Integer> list = new ArrayList<>();
        for(int[] info : rectangles){
            list.add(info[0]);
            list.add(info[2]);
        }
        Collections.sort(list);//对list中x坐标从此小到大排序
        long ans = 0;
        for(int i = 1; i < list.size(); i++){
            //每次取出两个相邻x坐标
            //令相邻x坐标距离为len,如果len=0跳过循环
            int a = list.get(i-1),b = list.get(i),len = b-a;
            if(len == 0) continue;
            
            //定义lines存储能够覆盖(x1,x2)的y坐标对(y1,y2)
            List<int[]> lines = new ArrayList<>();
            for(int[] info : rectangles){
                if(info[0] <= a && info[2] >= b){//当矩形覆盖当前x区间,则将y坐标记录下来
                    lines.add(new int[]{info[1],info[3]});
                } 
            }
            //对所有的y坐标对,按照y1,y2,从小到大排序
            Collections.sort(lines, (l1, l2)->{
                return l1[0] != l2[0] ? l1[0] - l2[0] : l1[1] - l2[1];
            });
            
            //定义tot存储当前x区间下,y区间的并集,l,r为上一个y区间端点
            long tot = 0,l = -1,r = -1;
            for(int[] cur : lines){
                //如果和上次的区间不相交,则将上次区间计入总和,同时更新l,r
                if(cur[0] > r){
                    tot += r-l;
                    l = cur[0];r = cur[1];
                }else if(cur[1] > r){//如果和上次区间相交,则只更新r
                    r = cur[1];
                }
            }
            tot += r-l;//将最后一个区间求和
            ans += tot * len;//面积为区间长度乘以高度和
            ans %= mod;
        }
        return (int)ans;
    }
}

离散化 + 线段树 + 扫描线

线段树将整个区间分割为多个不连续的子区间,子区间的数量不超过 log(width)。更新某个元素的值,只需要更新 log(width) 个区间,并且这些区间都包含在一个包含该元素的大区间内。区间修改时,需要使用懒标记保证效率。

  • 线段树的每个节点代表一个区间;
  • 线段树具有唯一的根节点,代表的区间是整个统计范围,如 [1, N];
  • 线段树的每个叶子节点代表一个长度为 1 的元区间 [x, x];
  • 对于每个内部节点 [l, r],它的左儿子是 [l, mid],右儿子是 [mid + 1, r] 其中 mid = ⌊(l + r) / 2⌋ (即向下取整)。

对于本题,线段树节点维护的信息有:

  1. 区间被覆盖的次数 cnt;
  2. 区间被覆盖的长度 length。

另外,由于本题利用了扫描线本身的特性,因此,区间修改时,不需要懒标记,也无须进行 pushdown 操作。

关于扫描线,可以参考下面这张图。注意,本题代码采用的是从左到右扫描。

class Node{
    int l,r,cnt,length;
}

class SegmentTree{
    private Node[] tr;
    private int[] nums;

    public SegmentTree(){}

    public SegmentTree(int[] nums){
        this.nums = nums;
        int n = nums.length-1;
        tr = new Node[n << 2];//开n的4倍大小
        for(int i = 0; i < tr.length; i++){
            tr[i] = new Node();
        }
        build(1,0,n-1);
    }

    private void build(int u, int l, int r) {
        tr[u].l = l;
        tr[u].r = r;
        if (l != r) {
            int mid = (l + r) >> 1;
            build(u << 1, l, mid);//u*2
            build(u << 1 | 1, mid + 1, r);//u*2+1
        }
    }

    public 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);
    }
    //向上更新,结点里保存矩阵的长度
    private void pushup(int u) {
        if (tr[u].cnt > 0) {//当前区间里覆盖有矩阵
            tr[u].length = nums[tr[u].r + 1] - nums[tr[u].l];
        } else if (tr[u].l == tr[u].r) {
            tr[u].length = 0;
        } else {
            tr[u].length = tr[u << 1].length + tr[u << 1 | 1].length;
        }
    }

    public int query() {
        return tr[1].length;
    }
}

class Solution {
    private static final int MOD = (int) 1e9 + 7;
    public int rectangleArea(int[][] rectangles) {
        int n = rectangles.length;
        int[][] segs = new int[n << 1][4];
        int i = 0;
        // Y 坐标去重排序,离散化
        TreeSet<Integer> ts = new TreeSet<>();
        //[横坐标,纵坐标下顶点,纵坐标上顶点,矩形的左边or右边标志]
        for (var e : rectangles) {
            int x1 = e[0], y1 = e[1], x2 = e[2], y2 = e[3];
            segs[i++] = new int[] {x1, y1, y2, 1};
            segs[i++] = new int[] {x2, y1, y2, -1};
            ts.add(y1);
            ts.add(y2);
        }
        //按横坐标排序
        Arrays.sort(segs, (a, b) -> a[0] - b[0]);
        Map<Integer, Integer> m = new HashMap<>(ts.size());
        i = 0;
        int[] nums = new int[ts.size()];//用来初始化线段树的
        //把所有去重后的y索引放到map表中,记录每个y在线段树中的位置索引
        for (int v : ts) {
            m.put(v, i);
            nums[i++] = v;
        }
        //扫描线,记录大小
        SegmentTree tree = new SegmentTree(nums);//初始并建立线段树
        //线段树包含的信息有:区间被覆盖的次数 cnt 和 区间被覆盖的长度 length。
        long ans = 0;
        for (i = 0; i < segs.length; ++i) {
            var e = segs[i];//依次遍历所有点,按照x从小到大的顺序
            int x = e[0], y1 = e[1], y2 = e[2], k = e[3];
            if (i > 0) { 
                ans += (long) tree.query() * (x - segs[i - 1][0]);
            }
            tree.modify(1, m.get(y1), m.get(y2) - 1, k);
            //矩形的左边标志为1,右边标志为-1,有点像差分数组,记录当前重叠次数
        }
        ans %= MOD;
        return (int) ans;

    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 可以在application.properties或者application.yml文件中配置spring.jpa.hibernate.ddl-auto属性来改变实体类的扫描。配置方式如下: ``` spring.jpa.hibernate.ddl-auto=create-drop ``` 其中create-drop表示在程序启动时创建表,程序停止时删除表。也可以使用其它值,如create、update、validate等。 如果想要指定扫描的实体类,可以使用@EntityScan注解,如下: ``` @EntityScan(basePackages = {"com.example.entity1", "com.example.entity2"}) ``` 这样只会扫描com.example.entity1和com.example.entity2包中的实体类。 ### 回答2: 要改变spring.jpa.hibernate.ddl-auto扫描的实体,可以按照以下步骤进行操作: 1. 首先,在应用的配置文件中找到spring.jpa.hibernate.ddl-auto属性,并将其值设置为none。这样会禁止自动创建、更新和删除数据库表结构。 2. 然后,创建一个命名为HibernateConfig的类,并使用@Configuration注解进行标记。在这个类中,可以使用@EnableJpaAuditing注解来启用JPA的审计功能。 3. 在HibernateConfig类中,创建一个名为entityManagerFactory的方法,并使用@Primary和@Bean注解进行标记。在这个方法中,可以通过LocalContainerEntityManagerFactoryBean来创建并配置一个EntityManagerFactory,并通过设置其packagesToScan属性来指定要扫描的实体类所在的包。 4. 接下来,在配置类中创建一个名为transactionManager的方法,并使用@Primary和@Bean注解进行标记。在这个方法中,可以通过JpaTransactionManager来创建一个事务管理器,并将EntityManagerFactory作为参数传递给它。 5. 最后,在应用的主类中使用@EnableJpaRepositories注解来启用JPA的存储库功能。 通过以上步骤,就可以改变spring.jpa.hibernate.ddl-auto扫描的实体。在配置文件中设置ddl-auto为none,表示禁止自动创建表结构。然后,在配置类中使用packagesToScan属性指定要扫描的实体类所在的包,从而指定要进行实体扫描的范围。最后,通过@EnableJpaRepositories注解来启用JPA的存储库功能,以便能够在应用中使用JPA的CRUD操作。 ### 回答3: 在Spring Boot中,可以通过设置`spring.jpa.hibernate.ddl-auto`属性来指定Hibernate在应用启动时自动创建、更新或验证数据库表结构。该属性默认值为`create-drop`,表示每次启动应用程序时创建数据库表并在应用程序关闭时删除表。 要改变`spring.jpa.hibernate.ddl-auto`属性扫描哪些实体,可以通过以下方式进行操作: 1. **使用@EntityScan注解**:在Spring Boot的主应用程序类上使用`@EntityScan`注解,该注解允许指定要扫描的包或类,以查找实体类。例如,如果要扫描`com.example.entity`包下的实体类,可以在主应用程序类上添加`@EntityScan("com.example.entity")`注解。 2. **使用LocalContainerEntityManagerFactoryBean**:在Spring Boot的配置类中,可以使用`LocalContainerEntityManagerFactoryBean`来自定义EntityManagerFactory的创建过程。通过设置`packagesToScan`属性,可以指定要扫描的实体类所在的包。例如: ```java @Configuration public class JpaConfig { @Bean public LocalContainerEntityManagerFactoryBean entityManagerFactory() { LocalContainerEntityManagerFactoryBean emf = new LocalContainerEntityManagerFactoryBean(); emf.setPackagesToScan("com.example.entity"); // 其他配置... return emf; } } ``` 这样配置后,Hibernate将只扫描指定包下的实体类。 通过上述两种方式,可以改变`spring.jpa.hibernate.ddl-auto`属性扫描哪些实体。可以根据实际需选择适合的方式,以便根据需要自定义实体类的扫描范围。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值