# 前言

掃描線就如他的名字一樣,通常是由一個方向掃到另一個方向,然後就只需要判斷新掃到的地方來做判斷,通常會搭配排序。

# 這一章篇幅不長,因為掃描線本身更像是一種觀察問題的角度,而不是一套獨立的演算法 —— 它常常是貪心(1-5)或雙指針(1-3)的具體實現方式。理解它的關鍵在於抓住「掃描線」這個比喻本身。

# 什麼是掃描線

想像一條線(可以是時間軸、數線、或平面上的一條垂直線)從左掃到右。掃描過程中,我們只需要維護「目前掃到的位置」相關的資訊,不需要每次都重新檢視所有資料 —— 這正是它能把複雜度降下來的原因。

掃描線通常搭配三個步驟:

  1. 把所有「事件」按座標排序(例如區間的起點、終點)
  2. 依序處理每個事件,掃描線移動到該事件的位置
  3. 維護一個資料結構,回答「掃描線目前位置」相關的查詢

這個套路能生效,前提跟雙指針一樣是單調性:一旦掃描線往右移動,就不會再回頭,所以每個事件只會被處理一次,整體複雜度通常是 O(n log n) (排序)加上 O(n) (掃描)。


# 例題:監看華山練功場(AP325)

華山派有 n 個弟子,每個弟子的練功時間都不盡相同,第 i 個弟子到練功場所練功的時間是區間 [s (i),t (i))。最近華山頗不平靜,掌門岳不群要求令狐沖找一些弟子練功時順便監看練功場,對於想要監看的時間區間 [x,y),請問他最少只要找幾位弟子,這些弟子的練功時間就可以涵蓋整個 [x,y)。

輸入說明:第一行是個正整數 n,第二行是兩個整數 x 與 y,接著的 n 行每一行有兩個整數 s (i) 與 t (i),同行相鄰兩數之間空白區隔。n 不超過 1e5,0≤x<y≤1e9,且對所有 i,0≤s (i)<t (i)≤1e9。

輸出說明:練功時間可以涵蓋 [x,y) 的最少的弟子數。如果無解輸出 -1。

範例輸入 #1 範例輸出 #1
5
1 10
0 3
1 5
5 7
8 9
6 10
3
範例輸入 #2 範例輸出 #2
5
1 10
0 3
1 5
5 7
8 9
8 10
-1

這一題其實也不用想的太難找 y 最大那邊不用什麼技巧傻傻的掃過去一遍就好了,掃描線的題目大概就是這種感覺。

# 解題思路

這是 ** 區間覆蓋(interval covering)** 的經典模型,貪心規則是:從目前已覆蓋到的位置 x 開始,在所有起點 ≤ x 的候選裡,挑選結尾最遠的那個。這正是貪心(見 1-5)跟掃描線的結合 —— 掃描線負責「依序看到哪些候選變得可用」,貪心負責「在可用的候選裡挑最好的」。

// [自己的解法]
#include <bits/stdc++.h>
using namespace std;
#define N 100005
bool cmp(pair<int,int> a,pair<int,int> b){
    if(a.first != b.first){
        return a.first < b.first;
    }else{
        return a.second <= b.second;
    }
}
int n,x,y;
pair<int,int> p[N];
int main(){
    cin>>n;
    cin>>x>>y;
    for(int i=0;i<n;i++)
        cin>>p[i].first>>p[i].second;
    sort(p,p+n,cmp);// 由小到大排序
    int i=0,ans=0;
    while(x<y){
        int temp=i;
        while(i<n && p[i].first <= x){
            // 從監看起點小於等於 x 的人中挑選 y 最大的人
            if(p[temp].second > p[i].second) temp=i;
            i++;
        }
        //p [temp].first > x 這行是為了排除 x 到 y 的區間中間有沒被包在範圍的狀況
        // 後方的則是判斷已經沒有人可以選了可還是有區間沒被包道
        if(p[temp].first > x || (i>=n && p[temp].second < x)) {
            cout<<"-1";
            return 0;
        }
        ans++;
        x=p[temp].second;
    }
    cout<<ans;
}

這裡的「掃描線」體現在 while(i<n && p[i].first <= x) 這一段: i 只會往前移動,永遠不會回頭,每個候選人只會被檢查一次,這就是掃描線省時間的地方。搭配貪心規則「挑結尾最遠的那個」,整體複雜度是排序的 O(n log n) 加上一次線性掃描 O(n) 。


# 掃描線的常見應用場景

雖然這章只有一個主要例題,但掃描線的思路在很多地方都會用到,這裡列出幾個常見場景,幫助之後辨認:

  • 區間覆蓋 / 區間排程:如本章例題,或 1-5 提過的活動選擇問題
  • 求多個區間的聯集長度:把所有起點標記 +1、終點標記 -1,排序後掃過去,用一個計數器判斷目前是否被覆蓋
  • 平面幾何問題:例如求矩形聯集面積、最近點對問題,讓一條垂直線橫掃整個平面,搭配一個資料結構(如 set、線段樹)動態維護掃描線上的資訊
  • 時間序列的事件處理:把「進入」和「離開」都當成事件,按時間排序後依序處理

# 判斷可不可以用掃描線

如果問題可以被描述成「一連串按座標排序的事件」,而且處理每個事件只需要知道當下的狀態(不需要回頭看已經處理過的事件的細節),那就很適合用掃描線。反過來說,如果每個事件的處理都需要跟所有其他事件比較,那掃描線可能就幫不上忙,要考慮別的資料結構。


# 小結

掃描線的核心心法只有一句話:排序後往一個方向掃,只維護當下需要的資訊。它本身不是一個獨立於貪心、雙指針之外的新技巧,而是這些技巧在處理「一維座標上的事件」時的具體展現。

下一章要講分治演算法 —— 把問題切成兩半分別解決,再合併答案。跟掃描線「一路往前不回頭」的線性思路不同,分治是「遞迴地一分為二」的樹狀思路。

# 練習題

(之後補上)

更新於 閱讀次數 次