【C++】競程筆記(一維掃描線)
【C++】競程筆記(一維掃描線)
程式碼範例參考:NTUCPC Guide,此筆記僅為個人學習用途。
數線問題
例題(APCS 2016 年 3 月第三題):https://zerojudge.tw/ShowProblem?problemid=b966
測資加強版:https://zerojudge.tw/ShowProblem?problemid=f855
1 |
|
拿範例測資 1 跑看看:
1 | 5 |
排序後:(150, 200)、(160, 180)、(190, 210)、(280, 300)、(300, 330)。
合併過程:
(150, 200) 和 (160, 180) 重疊,還是 (150, 200),因為 200 最大。
(150, 200) 和 (190, 210) 重疊,更新為 (150, 210)。
(150, 210) 和 (280, 300) 不重疊,計算 (150, 210) 長度 60,從 (280, 300) 繼續開始前往下一個線段。
(280, 300) 和 (300, 330) 相鄰,更新為 (280, 330)。
最後計算 (280, 330) 長度 50。
總長度 = 60 + 50 = 110。
總之解題思路就是:
- 用 pair 資料結構
- 排序 pair 的 first
- 初始化第一條線段的左右界(
L、R)為vector<pair<int, int>> seg(l = seg[0].first跟r = seg[0].second)。 - 用迴圈跑重疊或相鄰的區間(
seg[i].first <= r,i = 1),以max()更新R,然後繼續跳下一個線段。 - 不重疊或相鄰就直接計算長度(
R - L),繼續前往下個線段。 - 由於重疊或相鄰的區間在計算時只有
max()(因為題目要求不要計算重複的重疊區間),最後要記得計算他們的長度。
不過這是我本人的解法,不算是用掃描線去解的,以下是 NTUCPC Guide 的解法(掃描線):
1 | // from NTUCPC Guide |
掃描線與事件
這種想像自己在走的方法稱為掃描線,就是想像有一條直線掃過去,你就是那條直線。
from NTUCPC Guide掃描對象是很單純的一個序列(一些格子)配上一些區間,或者一條數線配上一些線段,然後很單純的左往右掃,這種題目的作法框架基本上都差不多,首先先找出你關心的當下狀態,然後看看狀態什麼時候會發生改變,狀態改變的事情就被稱為事件,把事件按照發生時間點(通常就是座標)排序後,就可以好好摸擬掃描的過程,如果座標範圍不大,也可以直接開一個長度是 $C$ 的陣列來直接儲存每個整數時間點發生的事件,並一一枚舉所有整數時間點。
from NTUCPC Guide
精簡一下:
掃描線是一種想像自己是條線、從左到右(也可以由下往上)掃過整個序列或數線的解法。
它的核心概念是觀察「狀態」何時改變,並將這些變化視為「事件」。將所有事件依照發生時間(或座標)排序後,就能模擬整個掃描過程。若座標範圍不大,也可用長度為 $C$ 的陣列來記錄每個整數時間點的事件並逐一處理。
那至於啥是事件呢?像是線段的端點(起始點和終點)是事件的一種。
掃描的起點
用掃描線要有起始狀態,如於線段覆蓋的程式中 lst = -1,假設起點為 -1,起點狀態包住目前的線段數為 0。通常只要挑所有端點左邊當作起點即可。
而在對序列掃描時,可以自己加一個 index 是 0 的位置當起點。
from NTUCPC Guide
邊界條件
在線段長度覆蓋的問題中,一線段的 $[l, r]$ 的意思即為數線上 $[l, r]$ 的範圍,只關心到長度,而單一點的狀態不重要,只要在乎每個時間點之間的狀態。進入區間的時間是走過 $l$ 以後,離開區間的時間是走過 $r$ 以後。
同時發生的事件
當掃描線掃到某特定位置上,有不只一個事件要處理時,就會發生同時發生的事件。
假設有兩棟房子:
- A:從 x=2 到 x=5,高度 10。
- B:從 x=2 到 x=6,高度 15。
那通常處理優先順序會如下所示(這部分依據題目要求可自行調整):
- 加入事件(start)先於移除事件(end)。
- 高度高的房子先處理(如:高度從高到低排序)。
- 起點 vs 起點:高的先;終點 vs 終點:低的先。
複雜的事件
複雜的事件就是「區間包含查詢」問題,如包含點的線段數問題。
習題練習
- b526. 先別管這個了,你聽過微鼓勵嗎?:https://zerojudge.tw/ShowProblem?problemid=b526
1 |
|

以上是由範例測資所畫出的圖。
先預設一個起始狀態,假設一開始全部人都是站著的。
那首先第一個區間微鼓勵下去之後,編號 1 ~ 3 的人全都蹲著。
第二個區間做完微鼓勵之後,1 ~ 2 維持不變,編號 3 站起,4 ~ 5 蹲下。
第三個區間做完之後,1 ~ 2 跟 4 ~ 5 站起,僅 3 號蹲下。
所以全部做完後,有 4 個人站著。
在這支程式中有個很重要的解題思路,就是我們不需要去紀錄每個人的最終狀態,而是要紀錄「哪些區間被反轉」。
具體步驟如下所示:
- 將每次操作紀錄為區間反轉,如:微鼓勵
[l, r]→ 在 l 處 +1,r+1 處 -1。 - 對所有點排序、掃描,計算每段區間共幾次反轉。
- 如果該段反轉次數是偶數,則狀態不變;奇數則反轉(站著變蹲下)。
- 統計站著的總人數。
在這支程式中:
- 差分用來記錄變化量。(n 太大了,所以只要記錄有變化的就好)
- cur 變數用來記錄目前區間的累計反轉次數
- prev 作為目前區間的起始位置(題目要求從編號 1 開始)
- standing 紀錄站著的人次
for (auto &[pos, delta] : diff) 做結構化綁定。
long long end = min(pos, n + 1); 表示目前區間的右端點,也是區間的結束位置(這個點是空心的,不含在內)。
if (cur % 2 == 0 && prev < end) 因為預設的狀態就是站著的,所以 cur % 2 == 0 表示反轉狀態不變,一樣站著,prev < end 為了避免處理長度為 0 的區間。
那題目要求站著的人數,所以接下來就 standing += end - prev;,表示這段區間的人都是站著的。




