---
# System prepended metadata

title: 【6-5】掃描線演算法

---

# 【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 子柚筆