跳到主要內容

演算法

同一個問題,不同的解題思路

主題 · 考題:會議室 II

考題:會議室 II

給一堆會議時段,最少需要幾間會議室?用最小堆積追蹤最早結束的會議,或用掃描線數同時在開的會議數。

解法
8
題目
給一堆以 [start, end) 表示的會議,回傳最少需要幾間會議室。例如 [[0, 30], [5, 10], [15, 20]] → 2。(LeetCode 253 是 Premium 題;2406 是同一題的免費版本,見下方練習題。)
1/15
0481216202401234567
最小堆積裡的結束時間(由小到大)

    先依開始時間排序。堆積裡放每間使用中會議室的結束時間,最上面就是最早空出來的那一間。

    同一份輸入,所有解法
    解法答案比較次數(含排序)額外記憶體Big O
    暴力:每個開始時間數一次3 間640O(n²)
    排序+最小堆積3 間303 格O(n log n)
    掃描線(開始、結束各排一次)3 間4316 格O(n log n)
    亮起來的是這一步執行的程式碼
    class MinHeap {
    private a: number[] = [];
    get size() { return this.a.length; }
    peek() { return this.a[0]; }
    push(x: number) {
    const a = this.a;
    a.push(x);
    for (let i = a.length - 1; i > 0; ) {
    const p = (i - 1) >> 1;
    if (a[p] <= a[i]) break;
    [a[p], a[i]] = [a[i], a[p]];
    i = p;
    }
    }
    pop() {
    const a = this.a, top = a[0], last = a.pop()!;
    if (a.length) {
    a[0] = last;
    for (let i = 0; ; ) {
    const l = 2 * i + 1, r = l + 1;
    let m = i;
    if (l < a.length && a[l] < a[m]) m = l;
    if (r < a.length && a[r] < a[m]) m = r;
    if (m === i) break;
    [a[m], a[i]] = [a[i], a[m]];
    i = m;
    }
    }
    return top;
    }
    }
    function minRoomsBrute(meetings: [number, number][]): number {
    let best = 0;
    for (const [start] of meetings) {
    let busy = 0;
    for (const [s, e] of meetings) {
    if (s <= start && start < e) busy++;
    }
    best = Math.max(best, busy);
    }
    return best;
    }
    function minRoomsHeap(meetings: [number, number][]): number {
    const sorted = [...meetings].sort((a, b) => a[0] - b[0]);
    const ends = new MinHeap();
    let best = 0;
    for (const [start, end] of sorted) {
    if (ends.size && ends.peek() <= start) ends.pop();
    ends.push(end);
    best = Math.max(best, ends.size);
    }
    return best;
    }
    function minRoomsSweep(meetings: [number, number][]): number {
    const starts = meetings.map((m) => m[0]).sort((a, b) => a - b);
    const ends = meetings.map((m) => m[1]).sort((a, b) => a - b);
    let rooms = 0, best = 0, j = 0;
    for (const start of starts) {
    while (j < ends.length && ends[j] <= start) {
    rooms--;
    j++;
    }
    rooms++;
    best = Math.max(best, rooms);
    }
    return best;
    }

    模型假設與範圍

    • Big O 與實測步數以頁面列出的基本操作為單位;視覺化的快照、畫圖、程式高亮和輸出成本另計,不是執行時間 benchmark。

    什麼時候用

    • 面試官想聽到你把問題換個說法:「最少幾間會議室」就是「同一時刻最多幾場會議」。說出這句,堆積和掃描線兩種解法就都出來了。
    • 先講排序+最小堆積:它還能告訴你每場會議分到哪一間;再補充掃描線更省、更短。

    和其他主題的關係

    時間與空間複雜度(Big O)

    操作平均最差
    暴力:每個開始時間數一次
    額外空間 O(1)
    O(n²)O(n²)
    排序+最小堆積
    堆積最多放「答案」那麼多個
    O(n log n)O(n log n)
    掃描線(開始、結束各排一次)
    兩個排序陣列,O(n) 空間
    O(n log n)O(n log n)

    空間:O(n)

    Big O 實測:n 變大時步數怎麼長

    數的是:比較次數(排序法含排序),同一組隨機會議

    Big On = 250n = 500n = 1,000n = 2,000成長倍數:實測(理論)
    暴力:每個開始時間數一次O(n²)62,500250,0001,000,0004,000,000×64 (×64)
    排序+最小堆積O(n log n)3,1676,75015,61533,361×11 (×11)
    掃描線(開始、結束各排一次)O(n log n)3,8578,69619,42742,841×11 (×11)

    和其他做法比

    n = 250n = 500n = 1,000n = 2,000
    暴力:每個開始時間數一次62,500250,0001,000,0004,000,000
    排序+最小堆積3,1676,75015,61533,361
    掃描線(開始、結束各排一次)3,8578,69619,42742,841

    同一組隨機會議,三種解法的比較次數(排序法含排序本身)。n 從 250 到 2,000,暴力法變成 64 倍,堆積只變成 11 倍、掃描線 11 倍。

    真實世界裡的它

    • 變形:會議室 I(能不能全部參加,只要排序後檢查相鄰)、每場會議指定房間(堆積記房號)。
    • 同樣的結構:同時最多幾架飛機在跑道上、雲端主機同時要幾台。

    取捨與陷阱

    • [s, e) 的邊界:一場在 10 點結束、另一場 10 點開始可以共用一間,所以堆積頂端 ≤ 開始時間就能沿用。
    • 每場會議只需要從堆積取出最多一次:取出的是最早空出來的那間,夠用就好,不必把所有已結束的都清掉。

    LeetCode 練習