MyCalendar 中重疊判定邏輯的解說

Greedy & IntervalsPriority 2 of 5 — Niche — read once, revisit only if a company is known to askNiche 更新於 Sep 18, 2026
Section priorityPriority 5 of 5 — Must know — expect it in almost every loopMust knowPriority 4 of 5 — High value — a gap here costs you roundsHigh valuePriority 3 of 5 — Worth knowing — usually a variant of a must-know patternWorth knowingPriority 2 of 5 — Niche — read once, revisit only if a company is known to askNicheMarked on the sections that carry it — unmarked sections are background/reference.

範圍只講一件事 — 區間重疊的判定式:閉區間 [a,b] vs 半開區間 [a,b),以及每一題 LC 各需要哪一種。 另見intervals.md — 會判定重疊之後該怎麼用;scanning_line.mddifference_array.md

  • (LC 729)

這份文件解說 MyCalendar 類別中下面這段程式碼的邏輯:

java
if (start < date.get(1) && end > date.get(0)) {
    return false;  // There's an overlap
}

LeetCode 題目清單

目的

這段邏輯是要檢查新的預約是否和行事曆上已有的預約重疊。如果有重疊,函式會回傳 false,表示這筆預約不能成立。

圖解

想像一條時間軸,每個事件都有開始和結束時間。新事件用 startend 表示,既有事件則用 date.get(0)(開始)和 date.get(1)(結束)表示。

各種情況:

1. 沒有重疊 — 新事件完全在既有事件之前

text
New:      |-----|   
Existing:          |-----|
  • 條件end <= date.get(0)
  • 說明:新事件在既有事件開始之前就結束了,所以沒有重疊。

2. 沒有重疊 — 新事件完全在既有事件之後

text
New:              |-----|
Existing:  |-----|
  • 條件start >= date.get(1)
  • 說明:新事件在既有事件結束之後才開始,所以沒有重疊。

3. 有重疊 — 新事件和既有事件部分重疊

text
New:       |-------|
Existing:     |------|
  • 條件start < date.get(1)end > date.get(0)
  • 說明:新事件在既有事件結束前就開始、又在既有事件開始後才結束,於是部分重疊。

4. 完全被包住 — 新事件的頭尾都落在既有事件之內

text
New:         |---|
Existing:   |-------|
  • 條件start < date.get(1)end > date.get(0)
  • 說明:兩個條件都成立,代表新事件整個落在既有事件的範圍內。

5. 新事件把既有事件吞掉(更早開始、更晚結束)

text
New:     |-----------|
Existing:   |-----|
  • 條件start < date.get(1)end > date.get(0)
  • 說明:新事件比既有事件更早開始、更晚結束,把它整個蓋住。

拆解這個條件:

  • start < date.get(1):檢查新事件是不是在既有事件結束之前就開始了。
  • end > date.get(0):檢查新事件是不是在既有事件開始之後才結束。

只要兩個條件同時成立就有重疊,函式回傳 false,表示這筆預約不能成立。


重疊判定式速查

範圍:這一節鎖定的是判定式(有沒有重疊?重疊多少?用哪一種慣例?)。 完整的區間演算法在別的文件: intervals.md(合併/貪婪排程/雙指標求交集)、 scanning_line.md(最大同時數、掃描事件)、 difference_array.md(大量區間加值)。

0) 概念 — 兩種區間慣例

每一道區間題目都默默選了其中一種慣例。選錯會產生只在「端點剛好貼合」那個測資才爆出來的差一錯誤

慣例 寫法 端點 a1 == b0 代表 題目常見的講法
閉區間 [a0, a1] 頭尾都包含 重疊(兩者共用 a1 這個點) 「inclusive」、陣列索引 left..right|x - y| <= t
半開區間 [a0, a1) 結尾不包含 不算重疊(首尾相接沒問題) 「10 點結束的預約和 10 點開始的預約不衝突」
text
Closed:      A = [1, 3]        B = [3, 7]
             |-----●           ●-------|         ● shared point 3  ->  OVERLAP

Half-open:   A = [1, 3)        B = [3, 7)
             |-----○           ●-------|         ○ excluded        ->  NO OVERLAP

0-1) 標準重疊判定 Priority 5 of 5 — Must know — expect it in almost every loop

把這兩行背起來。區間題目其他所有東西都建在它們之上。

text
closed     [a0, a1] ∩ [b0, b1] ≠ ∅   <=>   a0 <= b1 && b0 <= a1
half-open  [a0, a1) ∩ [b0, b1) ≠ ∅   <=>   a0 <  b1 && b0 <  a1

為什麼是這種寫法(而不是分情況討論):因為它就是「不相交」的否定。 兩個區間不相交,若且唯若其中一個在另一個開始前就結束 — a1 < b0 || b1 < a0(閉區間)/ a1 <= b0 || b1 <= a0(半開區間)。 對它做 De Morgan 就得到上面那兩行。這也是為什麼你根本不需要這份文件前面那 5 種情況的拆解 — 那 5 種情況會塌成同一個判定式。

面試時值得講出來的性質

  • 這個判定式是對稱的 — 參數誰先誰後都一樣。
  • 不需要排序。(如果區間本來就依開始時間排好,只要 b0 <= a1 就夠了。)
  • 文件開頭那段 MyCalendarstart < date.get(1) && end > date.get(0) 正是半開區間的寫法,只是兩個比較的順序對調而已。

0-2) 端點貼合 — 怎麼判斷題目要哪一種慣例

依序問下面這幾題,第一個有答案的就算數:

  1. 題目有沒有直接寫?[left, right)」、「end exclusive」、「half-open」→ 半開區間。 「inclusive」、「0 <= left <= right < n」→ 閉區間。
  2. 是預約/會議/時間軸類的題目嗎? 幾乎一定是半開區間 — 真實的行事曆本來就允許 09:00–10:00 的會議接著排 10:00–11:00。(LC 729 / 731 / 732 / 715 / 218 / 699。)
  3. 是陣列索引範圍,或是含端點的數值視窗嗎? 閉區間 — 端點要算進去,所以長度都會多一個 + 1。(LC 303 / 304 / 307 / 220 / 1438 / 497 / 1348。)
  4. 還是不確定?那就拿退化情況去問。(a, b)(b, c) 給面試官:「這兩個算不算衝突?」一句澄清就能消掉一整類差一錯誤。

正規化技巧(推薦):對整數來說,閉區間 [a, b] 和半開區間 [a, b + 1) 完全等價。在輸入時轉一次,之後全部都用半開區間的判定式。這樣就不用在合併/掃描/算長度的程式碼裡到處補 + 1

0-3) 交集、聯集、長度與間隙的公式

text
intersection      = [ max(a0, b0),  min(a1, b1) ]        # valid iff the overlap test passes
overlap length    = max(0, min(a1, b1) - max(a0, b0))    # continuous / half-open measure
overlap int count = max(0, min(a1, b1) - max(a0, b0) + 1) # closed, counting integer points
union (bounding)  = [ min(a0, b0),  max(a1, b1) ]        # a TRUE union only if they overlap/touch
gap (if disjoint) = max(0, max(a0, b0) - min(a1, b1))

陷阱union 只有在兩個區間重疊或相接時才是真正的聯集。兩個不相交的區間丟進去,它會回傳外框,把中間那個洞悄悄吃掉 — 這正是你忘記加重疊防護時,合併迴圈會壞掉的原因。

陷阱:兩種長度公式混用。閉區間 [1, 3] 含有 3 個整數(1、2、3);半開區間 [1, 3) 的測度是 2。一題只選一種慣例,然後從頭到尾守住。


1) 一般形式

1-1) 基本操作 — 重疊判定工具箱

java
// java
// IDEA: canonical overlap / intersection / union predicates for 1D ranges.
//       Closed    [a0, a1] -> both ends INCLUDED, so "touching" (a1 == b0) IS an overlap
//       Half-open [a0, a1) -> end EXCLUDED,       so "touching" (a1 == b0) is NOT an overlap
public class RangeOps {

    // time = O(1), space = O(1)
    // closed [a0, a1] vs [b0, b1]  -> touching endpoints count as overlap
    static boolean overlapClosed(int a0, int a1, int b0, int b1) {
        return a0 <= b1 && b0 <= a1;
    }

    // time = O(1), space = O(1)
    // half-open [a0, a1) vs [b0, b1)  -> touching endpoints are NOT an overlap
    static boolean overlapHalfOpen(int a0, int a1, int b0, int b1) {
        return a0 < b1 && b0 < a1;
    }

    // time = O(1), space = O(1)
    // intersection range; meaningful only when the matching overlap test above is true
    static int[] intersection(int a0, int a1, int b0, int b1) {
        return new int[] { Math.max(a0, b0), Math.min(a1, b1) };
    }

    // time = O(1), space = O(1)
    // overlap MEASURE for half-open / continuous ranges (0 when disjoint or only touching)
    static int overlapLength(int a0, int a1, int b0, int b1) {
        return Math.max(0, Math.min(a1, b1) - Math.max(a0, b0));
    }

    // time = O(1), space = O(1)
    // overlap COUNT of integer points for closed ranges (note the "+ 1")
    static int overlapCountClosed(int a0, int a1, int b0, int b1) {
        return Math.max(0, Math.min(a1, b1) - Math.max(a0, b0) + 1);
    }

    // time = O(1), space = O(1)
    // union bounding range; a true union only when the two ranges overlap or touch
    static int[] union(int a0, int a1, int b0, int b1) {
        return new int[] { Math.min(a0, b0), Math.max(a1, b1) };
    }

    // time = O(1), space = O(1)
    // gap between two disjoint ranges (0 when they overlap or touch)
    static int gap(int a0, int a1, int b0, int b1) {
        return Math.max(0, Math.max(a0, b0) - Math.min(a1, b1));
    }
}
python
# python
# IDEA: canonical overlap / intersection / union predicates for 1D ranges.
#       Closed    [a0, a1] -> both ends INCLUDED, so "touching" (a1 == b0) IS an overlap
#       Half-open [a0, a1) -> end EXCLUDED,       so "touching" (a1 == b0) is NOT an overlap

# time = O(1), space = O(1)
def overlap_closed(a0, a1, b0, b1):
    return a0 <= b1 and b0 <= a1

# time = O(1), space = O(1)
def overlap_half_open(a0, a1, b0, b1):
    return a0 < b1 and b0 < a1

# time = O(1), space = O(1)
# meaningful only when the matching overlap test above is true
def intersection(a0, a1, b0, b1):
    return (max(a0, b0), min(a1, b1))

# time = O(1), space = O(1)
# continuous / half-open measure (0 when disjoint or only touching)
def overlap_length(a0, a1, b0, b1):
    return max(0, min(a1, b1) - max(a0, b0))

# time = O(1), space = O(1)
# closed ranges: number of shared integer points (note the "+ 1")
def overlap_count_closed(a0, a1, b0, b1):
    return max(0, min(a1, b1) - max(a0, b0) + 1)

# time = O(1), space = O(1)
# a TRUE union only when the two ranges overlap or touch
def union(a0, a1, b0, b1):
    return (min(a0, b0), max(a1, b1))

# time = O(1), space = O(1)
def gap(a0, a1, b0, b1):
    return max(0, max(a0, b0) - min(a1, b1))

1-2) 二維 — 矩形重疊就是把一維判定做兩次

核心想法:兩個軸對齊的矩形重疊,若且唯若它們的 x 範圍重疊「且」y 範圍也重疊。沒有另外一條二維公式要背 — 每個軸各自套用一維判定式就好。同樣的拆解方式可以推廣到任意維度。

java
// java
// IDEA: 2D overlap = AND of two independent 1D tests, one per axis.
//       rect = [x1, y1, x2, y2] (bottom-left, top-right), half-open on both axes.

// time = O(1), space = O(1)
static boolean rectOverlap(int[] r, int[] s) {
    return r[0] < s[2] && s[0] < r[2]      // x-ranges overlap
        && r[1] < s[3] && s[1] < r[3];     // y-ranges overlap
}

// time = O(1), space = O(1)
// intersection area; 0 when the rectangles only touch along an edge or a corner
static long rectOverlapArea(int[] r, int[] s) {
    long w = Math.max(0, Math.min(r[2], s[2]) - Math.max(r[0], s[0]));
    long h = Math.max(0, Math.min(r[3], s[3]) - Math.max(r[1], s[1]));
    return w * h;
}
python
# python
# IDEA: 2D overlap = AND of two independent 1D tests, one per axis.
#       rect = (x1, y1, x2, y2) (bottom-left, top-right), half-open on both axes.

# time = O(1), space = O(1)
def rect_overlap(r, s):
    return r[0] < s[2] and s[0] < r[2] \
       and r[1] < s[3] and s[1] < r[3]

# time = O(1), space = O(1)
# 0 when the rectangles only touch along an edge or a corner
def rect_overlap_area(r, s):
    w = max(0, min(r[2], s[2]) - max(r[0], s[0]))
    h = max(0, min(r[3], s[3]) - max(r[1], s[1]))
    return w * h

注意(LC 497):當一個矩形代表的是整數格點、而且含邊界時,它在兩個軸上都是閉區間,所以點的數量是 (x2 - x1 + 1) * (y2 - y1 + 1) — 不是面積 (x2 - x1) * (y2 - y1)。選錯會讓隨機取樣產生偏差。


2) LC 範例 — 每一題各需要哪一種判定式?

依題目強迫你採用的慣例分組。

半開區間 [start, end) — 允許端點貼合

LC 題目 需要的判定式 為什麼是半開
729 My Calendar I s < e2 && s2 < e 20 點結束的預約和 20 點開始的預約不衝突
731 My Calendar II 同一個判定式,但允許重疊深度 ≤ 2 被禁止的是三重預約,不是端點相接
732 My Calendar III 所有點上的最大重疊深度 算 k 重預約,首尾相接不增加深度
715 Range Module 半開區間的重疊,但相接時要合併 [1,3)[3,5) 必須融合成 [1,5)queryRange 才會對
218 The Skyline Problem 每棟建築都是半開 共用同一條 x 邊的建築不算同時存在
699 Falling Squares [left, left + side) 只在邊緣接觸的方塊不會疊起來
850 Rectangle Area II 二維半開(兩個軸取 AND) 邊緣接觸貢獻 0 面積

閉區間 [start, end] — 端點要算,長度多一個 + 1

LC 題目 需要的判定式 為什麼是閉區間
1348 Tweet Counts Per Frequency t >= start && t <= end 分塊邊界兩端都含
497 Random Point in Non-overlapping Rectangles 兩個軸都是閉區間 落在邊界上的點也是合法的取樣結果
303 Range Sum Query - Immutable 閉的索引範圍 [left, right] prefix[right + 1] - prefix[left]
304 Range Sum Query 2D - Immutable 兩個軸都是閉區間 列/行的界都含端點;二維前綴和要補 + 1 位移
307 Range Sum Query - Mutable 閉的索引範圍 同樣是含端點的界,底層換成樹狀陣列/線段樹
220 Contains Duplicate III 數值視窗 [x - t, x + t],閉區間 |nums[i] - nums[j]| <= t 兩端都含
1438 Longest Continuous Subarray With Absolute Diff ≤ Limit 視窗 max - min <= limit,閉區間 limit 這個值本身是被允許的

2-1) MyCalendar 家族的變形筆記

文件開頭那段程式碼是深度 1 的情況。這個家族的擴展方式,就是把允許的重疊深度調大;判定式本身從來沒變過,變的只是你拿它來做什麼:

  • LC 729(深度 1) — 新預約只要和任何一筆既有預約重疊就拒絕。 用半開判定式暴力做,每筆 O(n);改用 ordered map 是 O(log n)
  • LC 731(深度 2) — 另外維護一份到目前為止所有交集的清單。只有當新預約和某個交集重疊時才拒絕。注意交集就是上面公式給的 [max(s, s2), min(e, e2)]
  • LC 732(最大深度) — 別再兩兩比對了;改成掃描:start+1end-1,一路追蹤最大值。到這裡,兩兩比對的判定式就撐不住了(O(n^2)O(n log n))。

掃描線做法見 scanning_line.md,行事曆預約的模板見 intervals.md

2-2) 參考題目

  • LC 729 My Calendar I / LC 731 My Calendar II / LC 732 My Calendar III — 預約深度 1 / 2 / 最大值
  • LC 715 Range Module — 半開區間,相接時必須合併
  • LC 218 The Skyline Problem / LC 699 Falling Squares — 在 ordered set 上做半開區間掃描
  • LC 850 Rectangle Area II — 用每軸的一維判定式湊出二維重疊
  • LC 497 Random Point in Non-overlapping Rectangles — 閉的格點,數量是 (x2-x1+1)*(y2-y1+1)
  • LC 1348 Tweet Counts Per Frequency — 閉的時間分塊
  • LC 303 / 304 / 307 Range Sum Query (Immutable / 2D / Mutable) — 閉的索引範圍
  • LC 220 Contains Duplicate III / LC 1438 Longest Continuous Subarray With Absolute Diff ≤ Limit — 閉的數值視窗