391.完美矩形,如果用扫描线算法你会怎么做

  • 2020 年 3 月 11 日
  • 筆記

今天分享一个LeetCode题,题号是391,标题是完美矩形,题目标签是Line Sweep [ 扫描线算法 ],题目难度是困难。

题目描述

我们有 N 个与坐标轴对齐的矩形, 其中 N > 0, 判断它们是否能精确地覆盖一个矩形区域。

每个矩形用左下角的点和右上角的点的坐标来表示。例如, 一个单位正方形可以表示为 [1,1,2,2]。( 左下角的点的坐标为 (1, 1) 以及右上角的点的坐标为 (2, 2) )。

示例 1

示例 1:

rectangles = [    [1,1,3,3],    [3,1,4,2],    [3,2,4,4],    [1,3,2,4],    [2,3,3,4]  ]    返回 true。5个矩形一起可以精确地覆盖一个矩形区域。  

'◡'

示例 2

示例 2:

rectangles = [    [1,1,2,3],    [1,3,2,4],    [3,1,4,2],    [3,2,4,4]  ]    返回 false。两个矩形之间有间隔,无法覆盖成一个矩形。  

'◡'

示例 3

示例 3:

rectangles = [    [1,1,3,3],    [3,1,4,2],    [1,3,2,4],    [3,2,4,4]  ]    返回 false。图形顶端留有间隔,无法覆盖成一个矩形。  

'◡'

示例 4

示例 4:

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

解题

这道题的题目标签是扫描线算法,那我们就按扫描线算法把这道题倒出来。

先简单介绍扫描线算法,扫描线平行于任一坐标轴扫过平面,随着坐标轴的变量而移动,变量的变化方向可负可正。意思是既可以从左到右扫描,也可以从右到左扫描;既可以从上到下扫描,也可以从下到上扫描。

所以俺这里就采用平行于y轴,从左到右扫描坐标图。

然后,再看输入示例,比如是这样的 [1,1,3,3], [3,1,4,2], [3,2,4,4], [1,3,2,4], [2,3,3,4] , 我们可以把它设定成下面这样的:

每个矩形的两个关键点

俺选中了每个矩形的左下和右下的两个端点,当然也可以选每个矩形的左上和右上的两个端点。

因为我们得到的红黑端点是乱序的,要给它先按x轴排序。

注意,如果两个端点的横坐标相等,我们要先判断红黑色点,如果是黑色点就排前面,如果是红色点就排后面,相等于就给它们分组了。不过黑色点和红色点怎么分组呢?

我们可以把黑色点的高度变成负数,红色点的高度还是原值,这样就可以分组了,同时也为后面的出入堆进行了有利的判断。

如果在同一横坐标有多个黑色点的话,就按照y轴排序,另一组的红色点也是。

如果某编程语言的数据结构有可重复且有序的集合的话,可以采用这个集合;如果没有,就想办法实现。

在Java没有这样的集合,但是可以利用TreeSet的Comparator内部类实现,在Comparator内部类重写一个比较方法,其中Triplet保存的是一个红色点或黑色点。

TreeSet<Triplet> triplets = new TreeSet<>((o1, o2) -> {      if (o1.x != o2.x) return o1.x - o2.x;      if (o1.h * o2.h < 0) return o1.h < 0 ? -1 : 1; // 正数 和 负数要分类,负数在左,正数在右      if (o1.y != o2.y) return o1.y - o2.y;      return 1; // 保证重复且有序 ,例如这个用例:[[0,0,4,1],[0,0,4,1]]  });  

这样,triplets就按照要求已经排好序了。

然后进行扫描线的移动,移到第一个矩形的左边界的时候,我们要到这个高度入堆。同时将这同一横坐标的高度全部入堆,然后把堆里的高度全部累加起来,得到一个临时高度。如果临时高度和投影高度不一致的话,则可以直接返回false。

投影

同时,同时也可以创建投影的上界和下界,只要是超出这个范围,那是不满足完美矩形的,可以直接返回false。

我们还要注意临时上界,如果下一个红色点的纵坐标要小于临时上界的话,也可以直接返回flase。

临时上界

如果临时上界等于下一红色点的纵坐标的话,则说明这两个矩形是没有覆盖的,接着将临时上界更新为该横坐标最高的高度。

如果扫描线在不同的横坐标,遇到第一个黑色点的话,则将临时上界更新为该横坐标第一个黑色点的纵坐标,从最低处开始。

依次类推,直到坐标图上矩形的最右边界,这个边界可以不用判断了,因为最右边界的上一边界是满足的,而且每一个都是矩形,左边界满足,右边界如果没有新的矩形的话自然也会满足,所以判断到最右边界还没有返回false,则可以直接返回true。

动画:扫描线移动过程

视频大小:1.15M,比Gif格式要小,可放心看

Java代码,从左到右扫描
import java.util.*;    class Solution {      private class Triplet {          int x;          int y;          int h;            Triplet(int x, int y, int h) {              this.x = x;              this.y = y;              this.h = h;          }      }        public boolean isRectangleCover(int[][] rectangles) { // 左上角 右下角坐标          // 将 rectangles 转换 三元组 [x y h]          TreeSet<Triplet> triplets = new TreeSet<>((o1, o2) -> {              if (o1.x != o2.x) return o1.x - o2.x;              if (o1.h * o2.h < 0) return o1.h < 0 ? -1 : 1; // 正数 和 负数要分类,负数在左,正数在右              if (o1.y != o2.y) return o1.y - o2.y;              return 1; // 保证重复且有序 ,例如这个用例:[[0,0,4,1],[0,0,4,1]]          });          for (int[] rectangle : rectangles) {              triplets.add(new Triplet(rectangle[0], rectangle[1], rectangle[3] - rectangle[1])); // 左端点的高度为正              triplets.add(new Triplet(rectangle[2], rectangle[1], rectangle[1] - rectangle[3])); // 右端点的高度为负          }          // 创建优先队列          Queue<Integer> queue = new PriorityQueue<>();            // 边界上下 宽度          int high = 0, low = 0, width = 0;          // 记录横坐标的变化          int axis = triplets.first().x;            // 以第一个左边界作为标准            low = triplets.first().y;          for (Triplet triplet : triplets) {              if (axis == triplet.x) {                  high = triplet.y + triplet.h;              } else break;          }          width = high - low;            // 临时宽度; 临时上界          int cur_w = 0, cur_h = Integer.MIN_VALUE;            // 正数入堆 负数出堆          for (Triplet triplet : triplets) {              // 判断 是否出界              if (triplet.x < low) return false;              if (triplet.y + triplet.h > high) return false;                if (axis != triplet.x) {                  if (cur_w != width) return false;                  cur_h = triplet.y;                  axis = triplet.x;              } else if (triplet.y < cur_h) {                  return false;              }                if (triplet.h > 0) cur_h = triplet.y + triplet.h;                if (triplet.h > 0) queue.add(triplet.h);              else if (triplet.h < 0) queue.remove(-triplet.h);              cur_w += triplet.h;          }          return true;      }  }  
Java代码,从下到上扫描
public boolean isRectangleCover(int[][] rectangles) {      TreeSet<Triplet> triplets = new TreeSet<>((o1, o2) -> {          if (o1.y != o2.y) return o1.y - o2.y;          if (o1.h * o2.h < 0) return o1.h < 0 ? -1 : 1; // 正数 和 负数要分类,负数在左,正数在右          if (o1.x != o2.x) return o1.x - o2.x;          return 1; // 保证重复且有序 ,例如这个用例:[[0,0,4,1],[0,0,4,1]]      });      for (int[] rectangle : rectangles) {          triplets.add(new Triplet(rectangle[0], rectangle[1], rectangle[2] - rectangle[0])); // 左端点的高度为正          triplets.add(new Triplet(rectangle[0], rectangle[3], rectangle[0] - rectangle[2])); // 右端点的高度为负      }      // 创建优先队列      Queue<Integer> queue = new PriorityQueue<>();      int l = 0, r = 0, width = 0;      // 记录纵坐标的变化      int axis = triplets.first().y;        l = triplets.first().x;      for (Triplet triplet : triplets) {          if (axis == triplet.y) {              r = triplet.x + triplet.h;          } else break;      }      width = r - l;      int cur_w = 0, cur_len = Integer.MIN_VALUE;        // 正数入堆 负数出堆      for (Triplet triplet : triplets) {          // 判断 是否出界          if (triplet.x < l) return false;          if (triplet.x + triplet.h > r) return false;            if (axis != triplet.y) {              if (cur_w != width) return false;              cur_len = triplet.x;              axis = triplet.y;          } else if (triplet.x < cur_len) {              return false;          }            if (triplet.h > 0) cur_len = triplet.x + triplet.h;            if (triplet.h > 0) queue.add(triplet.h);          else if (triplet.h < 0) queue.remove(-triplet.h);          cur_w += triplet.h;      }        return true;  }