leetcode 掃描線專題 06-leetcode.836 rectangle-overlap 力扣.836 矩形重疊

老马啸西风發表於2024-11-18

題目

矩形以列表 [x1, y1, x2, y2] 的形式表示,其中 (x1, y1) 為左下角的座標,(x2, y2) 是右上角的座標。

矩形的上下邊平行於 x 軸,左右邊平行於 y 軸。

如果相交的面積為 正 ,則稱兩矩形重疊。

需要明確的是,只在角或邊接觸的兩個矩形不構成重疊。

給出兩個矩形 rec1 和 rec2。如果它們重疊,返回 true;否則,返回 false 。

示例 1:

輸入:rec1 = [0,0,2,2], rec2 = [1,1,3,3]
輸出:true

示例 2:

輸入:rec1 = [0,0,1,1], rec2 = [1,0,2,1]
輸出:false

示例 3:

輸入:rec1 = [0,0,1,1], rec2 = [2,2,3,3]
輸出:false

提示:

rect1.length == 4

rect2.length == 4

-10^9 <= rec1[i], rec2[i] <= 10^9

rec1 和 rec2 表示一個面積不為零的有效矩形

小結

感覺有一個順序的問題。

應該先學習一下 T836 + T223 + T850 可能再做這一題就會比較自然。

v1-投影

思路

判斷兩個矩形是否重疊的通用條件是透過其投影區間是否有交集:

兩矩形沿 x 軸投影有重疊,且兩矩形沿 y 軸投影有重疊。

二維

矩形投影到座標軸上,就變成了區間。稍加思考,我們發現:兩個互相重疊的矩形,它們在 x 軸和 y 軸上投影出的區間也是互相重疊的。這樣,我們就將矩形重疊問題轉化成了區間重疊問題。

區間重疊是一維的問題,比二維問題簡單很多。

我們可以窮舉出兩個區間所有可能的 6 種關係:

一維

可以看到,區間的 6 種關係中,不重疊只有兩種情況,判斷不重疊更簡單。

假設兩個區間分別是 [s1, e1] 和 [s2, e2] 的話,區間不重疊的兩種情況就是 e1 <= s2 和 e2 <= s1。

重疊

公式化為:

x 軸投影相交:rec1[2] > rec2[0] && rec1[0] < rec2[2]
y 軸投影相交:rec1[3] > rec2[1] && rec1[1] < rec2[3]

解法

class Solution {
    public boolean isRectangleOverlap(int[] rec1, int[] rec2) {
        // 判斷 x 軸和 y 軸投影是否有交集
        boolean xOverlap = rec1[2] > rec2[0] && rec1[0] < rec2[2];
        boolean yOverlap = rec1[3] > rec2[1] && rec1[1] < rec2[3];
        
        // 兩個投影都有交集才算重疊
        return xOverlap && yOverlap;
    }
}

小結

投影這種解法看起來很簡單,實際上很巧妙,但是有時候我們不見得能想到。

v2-重疊面積

思路

如果兩個矩形重疊,那麼重疊的面積一定大於0.

於是問題變成如何計算重疊的矩形面積?

二維

如果有重疊,重疊的部分也一定是一個矩形

交集面積的計算方式是基於兩個矩形的重疊區域的 左下角和右上角的座標 來推算的。

具體來說,兩個矩形的交集矩形(如果存在)也是一個矩形,它的邊界由兩個矩形的邊界決定。

計算重疊面積

  • 第一個矩形 rec1 = [x1, y1, x2, y2],表示其左下角座標為 (x1, y1),右上角座標為 (x2, y2)

  • 第二個矩形 rec2 = [x3, y3, x4, y4],表示其左下角座標為 (x3, y3),右上角座標為 (x4, y4)

如果兩個矩形有交集,那麼交集的矩形的左下角和右上角的座標可以透過取兩者的最大和最小值來確定:

  • 交集矩形的左下角(x1', y1') = (max(x1, x3), max(y1, y3))

  • 交集矩形的右上角(x2', y2') = (min(x2, x4), min(y2, y4))

判斷交集是否存在

交集矩形的面積只有在其寬度和高度都大於 0 時才存在。

如果交集矩形的寬度或高度小於等於 0,說明兩個矩形沒有交集。

因此,交集矩形的寬度和高度的計算方式如下:

  • 寬度:width = x2' - x1'
  • 高度:height = y2' - y1'

交集矩形的面積則為:

  • 面積 = width * height,但是如果寬度或高度小於等於 0,面積就為 0。

因此,計算交集的面積時,我們需要確保 width > 0height > 0,否則交集的面積為 0。

程式碼示例

public class Solution {
    public boolean isRectangleOverlap(int[] rec1, int[] rec2) {
        // 計算交集矩形的左下角和右上角
        int x1 = Math.max(rec1[0], rec2[0]);
        int y1 = Math.max(rec1[1], rec2[1]);
        int x2 = Math.min(rec1[2], rec2[2]);
        int y2 = Math.min(rec1[3], rec2[3]);
        
        // 計算交集的寬度和高度
        int width = x2 - x1;
        int height = y2 - y1;
        
        // 如果交集的寬度和高度都大於 0,說明有重疊
        if (width > 0 && height > 0) {
            return true;
        }
        
        // 沒有重疊
        return false;
    }
}

效果

0ms 100%

小結

這個感覺比方法 1 的投影應該更容易想到一些。

而且可以在後續的 T223 T3047 中擴充使用。

v3-掃描線

掃描線

使用掃描線(sweepline)方法解決這個問題也是一種非常常見且高效的方式。

掃描線的基本思路是將矩形視為一系列的 線段,並在 x 軸方向上依次“掃描”這些線段,檢查是否有重疊。

思路

  1. 將矩形的邊界轉換為事件

    • 每個矩形有 兩條邊,一個是左邊界(x1),一個是右邊界(x2)。這兩條邊界構成了掃描線的“事件”。
    • 對於每個矩形,生成兩個事件:
      • 左邊界事件:表示矩形開始時,增加一個 y 區間(從 y1y2)。
      • 右邊界事件:表示矩形結束時,移除一個 y 區間(從 y1y2)。
  2. 事件排序

    • 所有的事件按 x 座標排序。如果 x 座標相同,優先處理右邊界事件(因為在同一位置時,應該先移除結束的矩形,再處理新的開始)。
  3. 掃描線處理

    • 掃描線從左到右掃描這些事件。當掃描到一個 左邊界事件 時,新增相應的 y 區間;當掃描到一個 右邊界事件 時,移除相應的 y 區間。
    • 在每次新增或移除事件時,檢查當前的 y 區間是否存在重疊。如果存在重疊,那麼矩形就重疊。
  4. 區間重疊檢查

    • 在掃描線過程中,維護一個 活動的 y 區間集合,該集合記錄了當前所有矩形在 y 軸上的區間。
    • 每次新增新的 y 區間時,檢查當前 y 區間是否與已經存在的區間重疊。如果重疊,則說明存在矩形重疊。

詳細步驟

  1. 定義事件

    • 每個矩形 rec = [x1, y1, x2, y2] 會生成兩個事件:
      • 左邊界事件 (x1, y1, y2, 1) 表示矩形開始(1 表示是左邊界)。
      • 右邊界事件 (x2, y1, y2, -1) 表示矩形結束(-1 表示是右邊界)。
  2. 排序事件

    • 按照 x 座標排序,如果兩個事件的 x 座標相同,右邊界事件優先。

為什麼要這樣做?

因為在掃描線的過程中,當我們遇到同一個 x 座標時,我們希望先處理右邊界事件,再處理左邊界事件。

這是為了避免在處理過程中出現誤判的情況。

比如,當一個矩形的右邊界和另一個矩形的左邊界在同一個 x 座標上時,我們應該先“移除”第一個矩形的 y 區間,然後再“新增”第二個矩形的 y 區間。

  1. 掃描並更新活動區間
    • 用一個資料結構(比如 TreeSet)維護當前的活動區間。每次掃描到一個事件時,更新該區間,檢查是否存在重疊。

如何判斷是否重疊:

private boolean hasOverlap(TreeSet<int[]> activeIntervals) {
    int prevEnd = Integer.MIN_VALUE;  // 初始化 prevEnd 為一個極小值
    for (int[] interval : activeIntervals) {
        // 如果當前區間的起始點小於前一個區間的終點,說明有重疊
        if (interval[0] < prevEnd) {
            return true;  // 有重疊,返回 true
        }
        prevEnd = interval[1];  // 更新 prevEnd 為當前區間的終點
    }
    return false;  // 如果沒有任何重疊,返回 false
}

實現

import java.util.*;

public class Solution {
    public boolean isRectangleOverlap(int[] rec1, int[] rec2) {
        // 如果 rec1 和 rec2 的 x 範圍沒有交集,直接返回 false
        if (rec1[2] <= rec2[0] || rec1[0] >= rec2[2]) {
            return false;
        }

        // 生成事件列表
        List<int[]> events = new ArrayList<>();
        
        // 對 rec1 和 rec2 的邊界生成事件
        // 左邊界事件 [x, y1, y2, type],type = 1 表示左邊界,-1 表示右邊界
        events.add(new int[]{rec1[0], rec1[1], rec1[3], 1});  // rec1 左邊界
        events.add(new int[]{rec1[2], rec1[1], rec1[3], -1}); // rec1 右邊界
        events.add(new int[]{rec2[0], rec2[1], rec2[3], 1});  // rec2 左邊界
        events.add(new int[]{rec2[2], rec2[1], rec2[3], -1}); // rec2 右邊界

        // 按 x 座標排序,x 相同的話,優先處理右邊界事件
        events.sort((a, b) -> a[0] == b[0] ? b[3] - a[3] : a[0] - b[0]);
        
        // 活動區間維護
        TreeSet<int[]> activeIntervals = new TreeSet<>((a, b) -> a[0] == b[0] ? a[1] - b[1] : a[0] - b[0]);

        for (int[] event : events) {
            int x = event[0];
            int y1 = event[1];
            int y2 = event[2];
            int type = event[3];

            if (type == 1) { // 左邊界事件,加入活動區間
                activeIntervals.add(new int[]{y1, y2});
            } else { // 右邊界事件,移除活動區間
                activeIntervals.remove(new int[]{y1, y2});
            }

            // 檢查活動區間是否有重疊
            if (hasOverlap(activeIntervals)) {
                return true;
            }
        }
        
        return false;
    }

    // 檢查活動區間是否有重疊
    private boolean hasOverlap(TreeSet<int[]> activeIntervals) {
        int prevEnd = Integer.MIN_VALUE;
        for (int[] interval : activeIntervals) {
            if (interval[0] < prevEnd) {
                return true;  // 有重疊
            }
            prevEnd = interval[1]; // 更新 prevEnd
        }
        return false;  // 沒有重疊
    }
}

效果

1ms 擊敗2.08%

小結

整體上而言,我比較喜歡重疊面積的方式,這種比較好想到,而且可以擴充套件。

當然掃描線也是一種很通用的解法。

這一題的巧思屬於維度投影的解法,很巧妙。

開源地址

為了便於大家學習,所有實現均已開源。歡迎 fork + star~

https://github.com/houbb/leetcode

掃描線專題

leetcode 掃描線專題 06-掃描線演算法(Sweep Line Algorithm)

leetcode 掃描線專題 06-leetcode.218 the-skyline-problem 力扣.218 天際線問題

leetcode 掃描線專題 06-leetcode.252 meeting room 力扣.252 會議室

leetcode 掃描線專題 06-leetcode.253 meeting room ii 力扣.253 會議室 II

leetcode 掃描線專題 06-leetcode.1851 minimum-interval-to-include-each-query 力扣.1851 包含每個查詢的最小區間

leetcode 掃描線專題 06-leetcode.223 rectangle-area 力扣.223 矩形面積

leetcode 掃描線專題 06-leetcode.3047 find-the-largest-area-of-square-inside-two-rectangles 力扣.3047 求交集區域的最大正方形面積

leetcode 掃描線專題 06-leetcode.391 perfect-rectangle 力扣.391 完美矩形

leetcode 掃描線專題 06-leetcode.836 rectangle-overlap 力扣.836 矩形重疊

leetcode 掃描線專題 06-leetcode.850 rectangle-area 力扣.850 矩形面積 II

參考資料

https://leetcode.cn/problems/4sum/

https://leetcode.cn/problems/rectangle-overlap/solutions/155825/tu-jie-jiang-ju-xing-zhong-die-wen-ti-zhuan-hua-we/

相關文章