# 【6-5】掃描線演算法 掃描線演算法其實並不是我們第一次遇到,在以前,你比較熟悉的叫法應該是線性掃描,看看下面這題吧! ## 一維最大子陣列問題 **題目**:給定長度為 n 的陣列,求所有連續子矩陣的最大可能總和。(空陣列以 0 計算) ### 解題思路 由於子陣列是連續的,我們進行線性掃描,考慮子陣列是否加入該元素,有以下情況: * 加入該元素後總和超過最大值 → 加入該元素,更新最大值 * 加入該元素後總和小於最大值,但大於該元素本身 → 加入該元素 * 加入該元素後,總和小於該元素本身 → 捨棄前方子陣列,從該元素開始重新建構 最後就能得到最大值。 ```cpp= #include <bits/stdc++.h> using namespace std; int main() { int n; cin >> n; vector<int> a(n); for (int i = 0; i < n; i++) cin >> a[i]; int cur = 0; // 當前最大子陣列和 int ans = INT_MIN; // 最大值 for (int x : a) { cur = max(x, cur + x); // 延續或重新開始 ans = max(ans, cur); // 更新最大值 } cout << ans << endl; } ``` * 時間複雜度:$O(n)$ 掃描線演算法也是同理,只不過運用在二維上,通常會有以下步驟: 1. 定義事件 2. 儲存並排序事件 3. 掃描並維護 4. 根據題目需求處理 接下來也會使用這四個步驟來解析幾種常見的題型。 ## 最近點對問題 **題目**:給定平面上 $n$ 個點,求曼哈頓距離最近的兩個點。 ### 定義事件 平面上有 $n$ 個點,每個點都有座標(x,y),我們可以簡單定義事件為 x 座標。 ### 儲存並排序事件 建立一個陣列儲存點座標,並對所有點按照 x 座標小到大排序。 ### 掃描並維護 我們由左而右進行掃描,維護最小距離,而距離的算法根據題目定義,本題是曼哈頓距離。 維護一個活躍集合(有可能產生最小距離的點集合),如果有一個左方的點,其與掃描線的距離已經超過最小距離,則該點不可能與右方的點找到更小的距離了(這句話很重要,多看幾次)。 ### 根據題目需求處理 對活躍集合剩下的點,由上而下掃描在最小距離內的點(同理上方對 x 的維護),更新最小距離。 等掃描結束以後,就能得到最小距離。 <details> <summary>範例程式</summary> ```cpp= #include <bits/stdc++.h> using namespace std; #define x first #define y second int manhattan(pair<int,int> a, pair<int,int> b) { return abs(a.x - b.x) + abs(a.y - b.y); } int main() { int n; cin >> n; vector<pair<int,int>> pts(n); for (int i = 0; i < n; i++) cin >> pts[i].x >> pts[i].y; // 按 x 排序(掃描線從左到右) sort(pts.begin(), pts.end()); set<pair<int,int>> active; // y, x,活躍集合 int ans = INT_MAX; int left = 0; for (auto p : pts) { // 移除掃描線左側不可能形成更小距離的點 while (left < n && p.x - pts[left].x > ans) { active.erase({pts[left].y, pts[left].x}); left++; } // 在活躍集合中檢查 y 座標在最小距離範圍內的點 auto it_low = active.lower_bound({p.y - ans, -1}); auto it_high = active.upper_bound({p.y + ans, INT_MAX}); for (auto it = it_low; it != it_high; ++it) { ans = min(ans, manhattan(p, {it->second, it->first})); } // 將當前點加入活躍集合 active.insert({p.y, p.x}); } cout << ans << "\n"; } ``` * 時間複雜度:$O(n\text{ log } n)$ </details> --- ## 2D Maximal Points **題目**:給定 $n$ 個平面點,找出所有「Maximal Points」:不存在其它點 q 的座標均大於或等於 p 的對應座標,則稱點 p 為 Maximal Points。 ### 定義事件 平面上有 $n$ 個點,每個點都有座標(x,y),我們可以簡單定義事件為 x 座標。 ### 儲存並排序事件 建立一個陣列儲存點座標,並對所有點按照 x 座標小到大排序。 ### 掃描並維護 從右往左掃描,也就是由 x 大到小掃描每個點,保證了當前點的 x 必定大於或等於其餘左側點。 ### 根據題目需求處理 因為 Maximal Points 滿足不存在其它點 q 的座標均大於或等於 p 的對應座標,由掃描已經滿足 x 必定成立,再滿足 y 值大於其餘右側點,就代表該點是 Maximal Point。 * 如果 y > 當前最大 y → 是 Maximal Point → 更新最大 y 值 <details> <summary>範例程式</summary> ```cpp= #include <bits/stdc++.h> using namespace std; #define x first #define y second int main() { int n; cin >> n; vector<pair<int,int>> pts(n); for (int i = 0; i < n; i++) cin >> pts[i].x >> pts[i].y; // 按 x 由大到小排序 sort(pts.begin(), pts.end(), [](const pair<int,int> &a, const pair<int,int> &b){ return a.x > b.x; }); vector<pair<int,int>> res; int maxY = INT_MIN; for (auto p : pts) { if (p.y > maxY) { res.push_back(p); // 是 maximal point maxY = p.y; // 更新當前最大 y } } cout << res.size() << endl; for (auto p : res) cout << p.x << " " << p.y << endl; } ``` * 時間複雜度:$O(n\text{ log } n)$ </details> --- ## 矩形重疊問題 題目:給定平面上 $n$ 個矩形的左下角與右上角,每個矩形都有一種顏色,且彼此不相同,又不同矩形組合重疊(不含相鄰)會產生出新顏色,保證不同組合的矩形重疊出來的顏色必定是新的,問平面上的矩形共產生幾種顏色(單獨一個矩形也有顏色,但背景不算一個顏色)。 ### 定義事件 平面上有 $n$ 個矩形,每個矩形都有上下左右四條邊,假想一條掃描線由左而右掃過去,會先碰到一個矩形的左邊界,再碰到一個矩形的右邊界,同理由上而下掃描。 既然會有這種特性,我們不妨將邊界定義成一個事件,當掃描線掃過後可能產生以下兩種結果: 1. 碰到的是矩形的左邊界:加入一個新矩形 2. 碰到的是矩形的右邊界:移除一個舊矩形 ### 儲存並排序事件 我們將一個矩形的左下角(x1,y1)、右上角(x2,y2)分別存入陣列中,因為我們的事件是 x 座標。並且增加一個變數負責記錄這是左邊界還是右邊界。 這邊舉例使用 1 表示左邊界,-1 表示右邊界,而 index 是我們自定義的矩形編號: ``` x1 y1 y2 index 1 x2 y1 y2 index -1 ``` 之後再利用自定義比較函數根據 x 座標由小排到大就可以進行掃描了。 ### 掃瞄並維護 我們由左而右進行掃描,如果還沒離開這個矩形,就又遇到一個新的矩形的左邊界,那這兩個矩形就有可能會重疊。 我們把這些有可能發生重疊的矩形,也就是目前區間活躍的矩形記錄下來,定義一個變數 `prevX` 代表上一個事件的 x 座標,而我們維護的區間就是 `[prevX, x)`。 ### 根據題目需求處理 題目要求顏色的總數,也就是有幾種矩形的重疊組合(包含自己),我們可以對於活躍的矩形由上而下再進行一次掃描,套用相同的維護方式,不過這次如果有發生重疊,就直接把活躍的矩形 `index` 存入 `set` 裡面自動去重,最後 `set` 的大小就是答案了。 <details> <summary>範例程式</summary> ```cpp= #include <bits/stdc++.h> using namespace std; struct Rect { int x1, y1, x2, y2; // 左下角(x1,y1)、右上角(x2,y2) }; struct Event { int x, y1, y2, id, type; // type = +1 (enter), -1 (exit) bool operator<(const Event& other) const { return x < other.x; // 依照 x 小到大排序 } }; int main() { ios::sync_with_stdio(0); cin.tie(0); int N; // N 個矩形 cin >> N; vector<Rect> rects(N); for (int i = 0; i < N; i++) { cin >> rects[i].x1 >> rects[i].y1 >> rects[i].x2 >> rects[i].y2; } // 儲存事件 vector<Event> events; for (int i = 0; i < N; i++) { auto [x1,y1,x2,y2] = rects[i]; events.push_back({x1, y1, y2, i, +1}); // 左邊界 在 x1 events.push_back({x2, y1, y2, i, -1}); // 右邊界 在 x2 } sort(events.begin(), events.end()); // 排序事件 set<vector<int>> colorSets; // 存顏色組合 set<int> activeSet; // 當前活躍矩形 int prevX = events[0].x; // 上一個事件,也就是左區間 int idx = 0; // 由左而右掃描事件 while (idx < (int)events.size()) { int x = events[idx].x; // 由上而下掃描活躍區間的矩陣,嘗試更新重疊組合 if (x > prevX && !activeSet.empty()) { for (int y = 0; y < 200; y++) { vector<int> cover; for (int id : activeSet) { if (rects[id].y1 <= y && y < rects[id].y2) { cover.push_back(id); } } if (!cover.empty()) { sort(cover.begin(), cover.end()); // 排列方便 set 去重 colorSets.insert(cover); } } } // 維護活躍區間的矩陣(一次把當前 x 的所有事件處理完再往右走) while (idx < (int)events.size() && events[idx].x == x) { auto e = events[idx]; if (e.type == +1) { activeSet.insert(e.id); // 加入 } else { activeSet.erase(e.id); // 移除 } idx++; // 下一個事件 } prevX = x; // 維護左區間 } cout << colorSets.size() << endl; return 0; } ``` * 時間複雜度:$O(n\text{ log } n)$ </details> ### 練習題 同上題目:[A0077-計算重疊顏色問題](https://code.dali.tc.edu.tw/problem/A0077) --- 聯絡方式:codecodefunny@gmail.com 最後編修時間:2025/09/06 子柚筆