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;  }