Semorphe

掃描線

把「區間」變成「兩個事件」。 · ⏱ 約 35 分鐘

你會學到三件事

  1. pair 把兩個值綁在一起
  2. 一段區間可以拆成「開始 +1」和「結束 −1」
  3. 排完序掃一遍,O(n log n) 解掉一個 O(n²) 的題

開始之前

題目:n 個人各有進場和離場時間,問同時在場的最多幾人。

直覺做法是「每個時刻數一次」,而時間範圍大的話那是 O(範圍 × n),太慢。

🟢 先把編輯器打開,跟著下面一起做:跟著做

一、pair:把兩個值綁在一起

pair 把兩個值當成一個東西:

🔎 這一段是試一下——打了看結果就好,不要留在程式裡。

1pair<int, int> p = make_pair(1, 2);2cout << p.first << " " << p.second << endl;
12建立配對變數p結構的欄位first" "變數p結構的欄位second換行0回傳印出建立配對 <int(整數),int(整數)>p定義函式 回傳型別int(整數)main使用命名空間std引入函式庫utility引入函式庫iostream(輸入輸出)12
拿什麼
p.first 第一個
p.second 第二個

⚠️ pair 排序時先比 first,一樣才比 second。

這件事是這一課的關鍵:sort 一個 vector<pair<...>> 不用寫比較函式。

(也可以寫 {1, 2} 代替 make_pair(1, 2),兩個都通。)

積木盤上這一顆寫著「建立配對 … …」, 而 p.first 那種取法是「結構 … 的欄位 …」。

二、🔴 一段區間 = 兩個事件

一個人從 a 待到 b。不要想成「一段」,想成兩件事:

時刻 a:人數 +1
時刻 b:人數 −1

於是每個人變成兩個 pair:

🔎 這一段是試一下——打了看結果就好,不要留在程式裡。

1events.push_back(make_pair(a, 1));      // 進場2events.push_back(make_pair(b, -1));     // 離場
變數a1建立配對變數events變數b-1建立配對變數events在尾端加入在尾端加入使用命名空間std引入函式庫utility12

三、排序,然後掃一遍

照著打上這一段,排完之後只走一遍:

1sort(events.begin(), events.end());2int cur = 0, best = 0;3for (int i = 0; i < events.size(); i++) {4    cur = cur + events[i].second;5    if (cur > best) best = cur;6}
變數events的開頭位置(成員形式)變數events的結尾之後的位置(成員形式)000變數events的長度變數cur變數cur變數events變數i陣列的第 [] 格結構的欄位second+變數cur變數best>變數best變數cur把設成如果則把設成計數i從到(不含)重複宣告int(整數)變數cur=,best=把到排序,順序使用命名空間std引入函式庫vector(動態列表)引入函式庫algorithm(演算法)12345

按時間排好,從左掃到右,cur 就是「現在有幾人」。

時刻:   1    3    5    7
事件:  +1   +1   -1   -1
cur:    1    2    1    0
                 ▲
              最大是 2

O(n log n)(排序)+ O(n)(掃一遍),而原本是 O(範圍 × n)。

掃描線的核心不是那個迴圈,是「把區間拆成端點」那一步。 一段連續的東西變成兩個離散的事件,於是它可以被排序。

四、⚠️ 同一時刻的順序

如果一個人 5 點離場、另一個 5 點進場,先算哪個?

pair 排序先比 first(時間)、再比 second。 而 -1 < 1,所以預設是先離場。

⚠️ 題目要哪一種要看清楚,這種差一格的錯很難從答案看出來。

完成的樣子

這一課的參考做法——程式碼每一行的號碼,就印在對應的那塊積木上。

1#include <algorithm>2#include <iostream>3#include <utility>4#include <vector>5using namespace std;6int main() {7    vector<pair<int, int>> events;8    events.push_back(make_pair(1, 1));9    events.push_back(make_pair(5, -1));10    sort(events.begin(), events.end());11    int cur = 0, best = 0;12    for (int i = 0; i < 2; i++) {13        cur = cur + events[i].second;14        if (cur > best) best = cur;15    }16    cout << best << endl;17    return 0;18}
11建立配對變數events5-1建立配對變數events變數events的開頭位置(成員形式)變數events的結尾之後的位置(成員形式)0002變數cur變數cur變數events變數i陣列的第 [] 格結構的欄位second+變數cur變數best>變數best變數cur把設成如果則把設成變數best換行0回傳印出計數i從到(不含)重複宣告int(整數)變數cur=,best=把到排序,順序在尾端加入在尾端加入建立pair<int, int>列表events大小每格填複製自定義函式 回傳型別int(整數)main使用命名空間std引入函式庫vector(動態列表)引入函式庫utility引入函式庫iostream(輸入輸出)引入函式庫algorithm(演算法)12345678910111213161417

換你了

加幾個人進去(例如 (2,4)、(3,6)),確認答案是對的。

再想一個:同樣的手法怎麼算「總共被覆蓋的長度」? 提示:記住上一個事件的時間,cur > 0 的時候累加時間差。

到編輯器做這一題:練習:加幾個人進去,確認答案是對的

做一個

三場會議:1~5、2~6、3~4。印出最多有幾場同時在進行。

🔴 把每一場拆成「開始 +1」和「結束 −1」兩個事件,排序,掃一遍。 ⚠️ 而兩兩比較的做法是 O(n²),這一題資料小看不出來,n 上萬就跑不完了。

到編輯器做這一題:做一個:最多幾場會議同時在開

這一課你做了什麼

如果卡住了

你看到 多半是因為
答案差 1 同時刻的先後順序。看清楚題目要哪一種
cur 變成負的 有 -1 沒有對應的 +1——事件加漏了
sort 排不動 pair 它可以。沒動的話是你排錯範圍
忘了排序 掃描線一定要先排序
在編輯器打開這一課 →