Preface
本文部分題目摘自 lxl 的資料結構系列課件
由於工程量巨大,難免會出現錯字、漏字的情況,如果影響到了您的閱讀體驗,還請私信我,我會在第一時間修復。
本文建議大家有一定的資料結構基礎後再來閱讀 /heart。
個人感覺掃描線不是難點,難點在於怎麼抽象模型。
首先需要明白一個東西,叫做 \(B\) 維正交範圍
在一個 \(B\) 維直角座標系下,第 \(i\) 維的座標在範圍 \([l_i, r_i]\) 之內,則符合這些限制的點構成的的點集為 \(B\) 維正交範圍。
當每個維度都是兩個端點的限制時,我們管它叫 2B-side 的 \(B\) 維正交範圍。
如果部分維度只有一個端點的限制,我們管它叫 A-side 的 \(B\) 維正交範圍。
如果有維度沒有限制,則顯然,這個維度不參與構成正交範圍。
一維掃描線
當處於一個靜態的二維平面上時,我們不難想到用掃描線來掃描其中的一個維度,用資料結構來維護另一個維度。
在掃描線掃描的過程中(例如從左到右掃),可能會在資料結構上產生一些修改和詢問。
如果資訊可差分,則直接差分,否則需要分治。
如果從序列的角度上來看的話,不難發現掃描線做的實際上是列舉詢問右端點,維護左端點答案。
假設當前的資訊是一個 4-side 的資訊,這個時候如果資訊滿足差分,就可以把 4-side 矩形差分成 3-side,然後這個時候就可以直接做一維掃描線,我們讓掃描線掃描一個 side,剩下所需要維護的資訊就是一個 2-side 的資訊,說人話就是一個區間操作的資訊。
靜態區間查詢類問題
靜態的序列,只有區間查詢。
一般維度只有 \(2\)。
二維數點
給定一個長為 \(n\) 的序列,有 \(q\) 次詢問,每次問你區間 \([l, r]\) 中有多少位置上的數在範圍 \([x,y]\) 之內。
\(1 \le n,q \le 2 \times 10^5,\;1\le l \le r \le n,\; 0 \le x \le y \le 10^9\)。
首先,不難發現所查詢的是一個 4-side 矩形內部的點的個數,這類問題一般叫做「二維數點」,是一類很經典的問題。
首先考慮一下怎麼建系,由於題目直接把座標系懟你臉上了,所以不難看出可以讓一個軸表示值域維,另一個軸表示序列維,然後顯然,序列維上是容易差分的,所以在序列維上差分,將範圍從 4-side 降到 3-side,這個時候就好辦了,掃描線掃描序列維剩下的那一個 side,然後在掃描線上就變成了查詢 \([x,y]\) 上點的個數,不難想到用樹狀陣列解決,時間複雜度 \(O((n+q) \log n)\)。
基礎問題
給定 \(n\) 個矩形,然後有 \(m\) 次詢問,每次詢問給定一個點,問這個點被多少矩形所包含。
範圍懶得給了,反正要求單 \(\log\)。
如果站在詢問的點的角度來看,似乎問題不是很好解決。
不妨這次來試著拆一下貢獻,看看每個矩形都貢獻了什麼,不難發現每個矩形都將這個矩形內的點被套的次數 \(+1\)。
這樣就爽了,考慮直接建系,橫軸就是題目中的 \(x\) 軸,縱軸就是題目中的 \(y\) 軸,然後依舊是熟悉的配方,將 4-side 的矩形給差分成 3-side 的矩形,也就是將每個矩形拆成入邊和出邊。現在用掃描線掃描 \(x\) 軸(當然,掃描 \(y\) 軸也可以),線上上維護一個 2-side 的區間,當掃描到入邊的時候,意味著區間 \(+1\),掃描到出邊的時候,意味著區間 \(-1\),然後每個查詢的點對應到線上就是單點查詢。
時間複雜度 \(O((n+q)\log n)\)。
P1972 [SDOI2009] HH 的項鍊
老典題了。
不妨先嚐試建系,讓一個軸為序列維,讓一個軸為值域維,然後你會發現這個不同的數的個數似乎特別不好維護。
不妨讓點都具有超能力,當掃描線掃描到和他相同的值時,把前驅的貢獻轉移到當前位置(可以認為點直接瞬移到掃描線所在的位置上),這樣的轉化就不難將原問題變成一個 2-side 的區間了,考慮維護一個 now 陣列,表示當前這個點所在的位置。然後掃描序列維(對應詢問 \(r\)),線上上也維護序列維(對應詢問 \(l\)),表示當前位置貢獻了多少點,則不難發現這是一個「單點加/減」和「區間查」的需求,用樹狀陣列維護即可,最後考慮每次向右掃描的時候,更新 now 陣列和線上的樹狀陣列,此時時間複雜度為 \(O((n+q)\log n)\)。
BZOJ 3489 A simple RMQ problem(弱化版)
給定長為 \(n\) 的序列,\(m\) 次查詢區間中有多少值只出現一次。
要求單 \(\log\)。
你不難發現當一個區間內出現兩個相同的數時,這個數就不產生貢獻了。
於是類比上面的「HH 的項鍊」,我們不難想到當我們掃描線掃到一個數時,讓其貢獻為 \(+1\),讓其前驅的貢獻為 \(-1\),讓其前驅的前驅的貢獻為 \(0\),然後剩下的就和「HH 的項鍊」一樣了。
CF522D Closest Equals
考慮直接建系,設橫軸為序列維(對應詢問 \(r\)),縱軸也為序列維(對應詢問 \(l\)),然後我們在橫軸上做掃描線。
現在考慮掃到一個數要做什麼,首先,顯然的是,至少要有一對數才有「距離」這個概念,然後我們在掃描的時候都是在擴張詢問的右端點,所以不妨讓相鄰兩個點中靠左的那個點的位置維護距離,這樣就變成了線上上維護「單點修改,區間查詢最小值」了。
時間複雜度 \(O((n+m)\log n)\)。
P4137 Rmq Problem / mex
考慮建系,設橫軸為序列維,縱軸為值域維,在序列維上做掃描線,線上維護最小值,然後掃描線的移動就是線上的單點修改,查詢就是在掃描線上進行二分,這裡用線段樹維護線上的資訊即可。
時間複雜度 \(O((n+m)\log n)\)。
未知渠道獲得的題
給定一棵 \(n\) 個點的樹,然後有 \(q\) 次查詢,每次查詢區間 \([l,r]\),表示詢問當這個樹僅剩點和邊的編號在 \([l,r]\) 之間時連通塊的個數。
要求單 \(\log\)。
考慮這裡的連通塊個數等於啥,顯然等於「點數 - 邊數」,然後點的個數是簡單的(恆定 \(r - l + 1\)),因為範圍內的點肯定會被算入到貢獻之中。
但是邊不一定,因為有可能它連線了超出 \([l, r]\) 範圍的點,這樣的邊是不能被計算進去的,所以現在我們要做的處理「邊數」這一部分的貢獻。不妨先冷靜下來,考慮影響一個邊是否是有效邊的因素有哪些,不難想到是「邊的兩個端點是否屬於 \([l,r]\)」和「邊的編號是否屬於 \([l,r]\)」,然後發現只要兩個不滿足其一,這個邊就沒有貢獻了。
這不失為一個非常優秀的性質,它允許我們將限制條件轉化為讓 \(l \le \min(u,v,label)\) 且 \(\max(u,v,label) \le r\),其中假設邊 \((u,v)\) 的編號為 \(label\)。
現在考慮建系,設橫軸為 \(\min\) 維,縱軸為 \(\max\) 維,此時對邊的詢問就變成了在一個 2-side 矩形裡面數點。
時間複雜度 \(O(n \log n)\)。
BZOJ 4212 神牛的養成計劃
一個經典的 trick 是對字串建立 Trie 樹,然後轉化到 DFS 序上的點。
此時我們不難想到對模式串正著建個 Trie,記為 \(T_0\),反著建個 Trie,記為 \(T_1\),然後查詢的時候對於串 \(a\) 的限制就是在 \(T_0\) 上跑,對於串 \(b\) 的限制就是在先翻轉,然後在 \(T_1\) 上跑,然後查詢滿足在 \(T_0\) 中 DFS 序範圍為 \([d_a,d_a+siz_a]\),在 \(T_1\) 中 DFS 序範圍為 \([d_b,d_b+siz_b]\) 的有效交的大小。
這個時候我們開始建系,考慮設橫軸為 \(T_0\) 的 DFS 序列,縱軸為 \(T_1\) 上的 DFS 序列,則每個字串的每個字元都對應了平面上的一個點,然後查詢就是在 4-side 的矩形內數點。
由於是強制線上,所以把掃描過程扔到主席樹上,此時依然是單 \(\log\) 的。
時間複雜度 \(O(L_2 + m\log L_1)\)。
UOJ 637. 【美團杯2021】A. 資料結構
Trick Point:
這種查有多少元素的題一般都考慮利用不同值對答案貢獻獨立的性質。
先考慮對每個元素計算一下其對哪些詢問有貢獻,然後使用資料結構批處理貢獻。
考慮每個元素對答案的貢獻,有一種常見方法是考慮對於什麼詢問這個元素對答案沒有貢獻。
如果一個出現了 \(k\) 次的數所影響的範圍可以用 \(O(k)\) 個矩形表示出來,我們也就解決了這個問題,因為 \(\sum k = n\)。
由於一個數 \(x\) 出現多次和出現一次的情況是完全一樣的,所以不妨考慮一下什麼時候這個數它消失了。簡單而快速的,我們知道只有兩個條件:
- 數 \(x\) 全都被 \(+1\) 了。
- 數 \(x-1\) 全沒被 \(+1\)。
現在需要將這些限制條件轉變成平面上的限制條件,考慮建系,設橫軸為序列維(對應詢問 \(l\)),橫軸也為序列維(對應詢問 \(r\)):
-
數 \(x\) 全部被 \(+1\) 了。
簡單的,維護一下數 \(x\) 第一次出現的位置 \(L\) 和最後一次出現的位置 \(R\),那麼顯然當詢問 \([l,r]\) 存在 \(l \le L\) 且 \(r \ge R\) 的時候對區間沒有貢獻。
-
數 \(x-1\) 全沒有被 \(+1\)。
考慮數 \(x-1\) 相鄰兩次出現的位置 \(i,j\),則顯然的是 \([i+1,j-1]\) 這一段內是不存在 \(x-1\) 的。
對應到二維平面上,就是矩形 \([i+1,j-1] \times [i+1,j-1]\)。
換句話說,所有 \(x-1\) 不出現的位置構成了 \(O(cnt_x + 1)\) 個矩形。
然後不難發現,限制 \(1\) 的本質上就是將限制 \(2\) 算出的矩形切掉了一部分,所以不影響漸進意義上的矩形個數。
考慮此時的詢問就變成了平面上的若干點,這時候我們將問題轉化為上面的「基礎問題」,做刪貢獻掃描線即可。
時間複雜度 \(O((n+m)\log n)\)。
百度之星 2021 初賽第三場 1008
給定一個長度為 \(n\) 的序列 \(A\),下標從 \(1\) 到 \(n\),有 \(m\) 次查詢操作,每次給出一個區間 \([l,r]\),求一個子區間 \([l',r']\),滿足 \(l \le l' \le r' \le r\),使得 \([l',r']\) 中的顏色數比 \([l,r]\) 中出現的顏色數少,且其長度 \(r'-l'+1\) 最大。如果 \(l' > r'\),我們認為沒有值在 \([l',r']\) 中出現過。
\(1\le n,m,a_i\le2\times 10^6\),時限 \(8s\)。
首先感謝高效率原題自動機 do_while_true 為我找到了原題連結。
考慮答案會是什麼樣子,不難想到要麼是一個字首,要麼是一個字尾,要麼是一箇中間一部分的子區間。
字首咋做呢,顯然的,只要找到了最靠右出現第一次出現的那個數的位置,在這個位置之前的都是合法的答案,字尾同理。這兩個「掃描線 + 樹狀陣列 + 倍增」都是輕鬆做的,在這裡不多贅述。
至於「中間一部分的子區間」這一類情況,套用 UOJ 637 那道題的 trick,考慮「以 \(r\) 為橫座標,\(l\) 為縱座標」建系,設 \(a\) 為顏色 \(c\) 出現的位置之一,\(b\) 為其後繼,則任何一個嚴格包含 \([a+1,b-1]\) 的區間都可以去除區間 \([a+1,b-1]\) 以外的部分來獲得 \(b - a - 1\) 的貢獻,然後可以這樣做的 \(L,R\) 只有 \(L \in [1,a], R \in [b + 1, n]\),對應到平面上就是一個 2-side 矩形,掃描線上維護「字尾 \(\max(x, a_i)\),單點查」即可,發現這個玩意可能要 Segbeats,進一步觀察性質,發現每一次修改的字尾都不一樣,然後維護的資訊可以對偶成「單點修改,字首 \(\max\)」,所以用樹狀陣列維護即可。
時間複雜度 \(O((n+m)\log n)\)。
UVA1608 Non-boring sequences
唯一的難點在於建系。
假設一個數的前驅、後繼的位置為 \(i,j\),考慮這個數能支配的最長區間是多大,則不難發現是 \([i+1,j-1]\),不然的話就會出現相同的兩個數。
然後你發現如果這樣直接在序列上統計的話似乎不直觀,而且好像還算重了很多部分,所以不妨把它放到二維平面上。具體的講,就是以橫軸為序列維,縱軸也為序列維建系,然後此時一個點所支配的就是一個矩形 \([i+1,j-1] \times [i+1,j-1]\),則此時考慮做「矩形面積並」,然後判斷算得的面積是否與整個平面的面積 \(\dfrac{n(n+1)}{2}\) 相等即可。
時間複雜度 \(O(n\log n)\),注意別忘了這個題還是多測。
經典問題
有一個長度為 \(n\) 的序列,有 \(m\) 次詢問,每次詢問一個區間,問區間上出現奇數次數的數的異或和為多少。
要求線性。
根據異或的性質,發現當一個數異或偶數次自己,貢獻為 \(0\)。
所以原問題可以轉化成區間異或和,考慮做異或字首和即可。
時間複雜度 \(O(n+m)\)。
不那麼經典的問題
有一個長度為 \(n\) 的序列,有 \(m\) 次詢問,每次詢問一個區間,問區間上出現偶數次數的數的異或和為多少。
要求單 \(\log\)。
根據「經典問題」,我們知道了出現奇數次怎麼做,但是感覺偶數次似乎很難做的樣子。
考慮正難則反,由於異或的性質「自己為自己的逆運算」,所以只要能夠統計出「區間內出現的數的異或和」,然後再異或上「區間異或和」,我們就得到了出現偶數次數的數的異或和。
那對於這種區間顏色單貢獻有關的部分,容易想到「HH 的項鍊」,顯然這個題可以類比「HH 的項鍊」,當我們掃描到一個數的時候,讓這個數的前驅的貢獻變為 \(0\),然後做查詢即可。
時間複雜度 \(O((n+m) \log n)\)。
CF1083D The Fair Nut's getting crazy
假設 \(a \le c \le b \le d\),不難發現此時的 \(a\) 為 \(\le c\) 裡面的一段字尾,\(d\) 為 \(\ge b\) 裡面的一段字首。
考慮維護序列 \(A\) 每個位置值的前驅和後繼,記為 \(pre_i\) 和 \(suf_i\),此時進一步縮放 \(a\) 和 \(d\) 的範圍,則 \(a\) 的可能取值被限制在了 \((\max_{i=c}^b pre_i,c]\),\(d\) 被限制在了 \([b,\min_{i = c}^b suf_i)\),現在我們滿足了題目的所有要求,此時答案為 \((c - \max_{i=c}^b pre_i) \times (\min_{i=c}^b suf_i - b)\),將式子拆開,得到 \(c\min - cb - \min\max + b\max\)。
建系,設橫軸為 \(b\) 的可能取值,縱軸為 \(c\) 的可能取值,掃描線在橫軸上從左向右跑,跑的同時維護兩個維護了 \(pre\) 和 \(suf\) 的字尾最大值的單調棧,掃描線向右擴充套件的時候向兩個單調棧分別加入的 \(pre_{b'}\) 和 \(suf_{b'}\), 考慮此時會彈出若干元素,這個時候計算這些元素的貢獻即可,具體地說,就是線段樹上區間更新 \(\min\) 部分和 \(\max\) 部分,簡單的,考慮打兩個標記 \(mntag\) 和 \(mxtag\) 即可,然後每一次做一次全域性查詢加到 \(ans\) 裡面即可。
時間複雜度 \(O(n \log n)\)。
CF793F Julia the snail
考慮離線,設橫軸為詢問右端點,線上維護詢問左端點,設 \(f_i\) 表示從 \(i\) 開始能走到的最高的距離,線上維護數列 \(f\),然後不難發現遇到一個新的繩子時所有 \(f_i\ge l\) 的地方全部都能變為當前的 \(r\),然後詢問變成單點詢問即可,考慮這個東西做「Segment tree beats」即可。
由於沒有區間加,所以時間複雜度 \(O((n + q) \log n)\)。
CF407E k-d-sequence
首先特判 \(d = 0\),做法簡單,找到最長的連續段即可。
現在考慮 \(d \not= 0\),發現一個數列想要滿足題目條件必須滿足以下條件:
- 區間內所有數模 \(d\) 均為一個數。
- 不出現重複的數字。
- \(\dfrac{\max - \min}{d} \le r - l + k\)。
可以將序列分成若干個「模 \(d\) 連續段」,這樣的話第一個限制就消失了。
現在需要對每個連續段考慮限制 \(2,3\) 怎麼做。
考慮掃描線掃描序列右端點,線上維護序列左端點,即線上上維護 \(\dfrac{\max - \min}{d} - r + l - k\),考慮 \(k\) 可以當做常數,\(l,r\) 可以被轉化為區間加,難點在於前面,簡單的,用一個單調棧維護一下 \(\max\) 和 \(\min\) 即可,在彈出的時候做區間加就好了,查詢二分一下即可。
考慮限制 \(2\),假設當前右端點位置為 \(r\),上面的數位 \(a_r\),那麼和 \(a_r\) 相同的位置必定是左端點不能跨越的,這樣使得這個東西變成了一個「首端刪除、末端加入」的操作。
綜上,我們需要線上上維護「區間加、首端刪除、末端加入,區間上二分」這些操作,簡單的,用線段樹搞一下就好了。
時間複雜度 \(O(n \log n)\)。
CF453E Little Pony and Lord Tirek
考慮每一次詢問都會導致一部分直接推平,然後推平後就成了簡單的分段函式型(一次函式與常值函式)了,我們設時間為每個位置的顏色,則此時每一次區間查詢就被我們轉換到區間染色,這樣貢獻被拆成了若干染色段的同時也方便我們維護時間差。
考慮 \(m,r\) 的範圍比較小,仔細算算我們最多最多隻需要維護 \(1e5\) 長度的時間,這樣就好辦了,考慮掃描線掃描序列維,然後分段函式就被我們變成了在時間維上的等差數列加(當函式值 \(\le m\) 時)和區間染色(當函式值 \(\ge m\) 時),然後查詢是一個單點的查詢,所以不難想到進行一個差分,然後將「等差數列加」和「區間染色」轉變成「區間加」和「單點加」,然後將單點查變為「兩次字首和的差」。
時間複雜度 \(O((n+q)\log n)\)。
P5490 【模板】掃描線(矩形面積並)
(好好好,現在才做模板題是吧)
由於出題人直接把座標系懟在我們臉上了,只需要考慮掃描線的部分就好了。
把矩形拆成入邊和出邊,然後在 \(x\) 軸上跑掃描線,此時線上上維護「區間 \(\pm1\),全域性 \(>0\) 位置數」,考慮使用線段樹來維護。
這裡的線段樹略微有一點點技巧,我們可以維護區間 \(\min\) 值和區間 \(\min\) 值出現次數,如果區間 \(\min\) 值為 \(0\) 的話,此時出現次數就是 \(0\) 的出現次數,那麼正數位置數就是拿總長度減去即可,如果區間 \(\min\) 值不為 \(0\),那就更好辦了,說明整個區間都是正數,直接返回總長度。
未知渠道獲取的題
有 \(n\) 個矩形和 \(q\) 次詢問。
每次詢問給你一個矩形,問這個矩形與 \(n\) 個矩形之並的交的面積。
原版要求強制線上,不過個人認為沒有必要,離線就好。
要求單 \(\log\)。
承上啟下的好題了,給下面打個基礎 qwq。
在 P5490 中,我們知道了怎麼做矩形並,首先將矩形差分,然後線上維護「區間 \(\pm 1\),全域性 \(0\) 的個數」。
然後這個題特別有意思,先形式化一下題意,此時查詢操作為「查詢詢問矩形內 \(\gt 0\) 的位置的個數」,考慮直接做不好辦,正難則反,數出「詢問矩形內 \(0\) 的位置的個數」,然後拿詢問矩形的面積一減就好了。
依舊考慮掃描線,由於查詢資訊可差分,直接差分成 3-side 矩形,然後問題變成了維護區間 \(0\) 的個數的歷史和,線段樹簡單做就好。
那怎麼強制線上呢,依舊是簡單的,將掃描過程丟到主席樹上,此時可能需要一個標記永久化,由於原題還需要離散化,所以需要還有一個垃圾分討……反正很噁心就對了/tuu。
注意,這個問題還有一個對偶版本(應該可以這麼叫吧)是這樣的:
有 \(n\) 個矩形和 \(q\) 次詢問。
每次詢問給你一個矩形,問這個矩形與 \(n\) 個矩形的並的面積。
兩者本質上等價,只不過一個是需要拿詢問矩形的面積減一下,一個直接就是答案,如果哪場模擬賽出了這個問題,可以直接爆錘出題人 /cf
可供練習的題:
區間子區間問題
給你一個序列,每次詢問區間有多少個子區間滿足某個條件。
遇到這類問題,通常以兩軸都為序列維建系(橫軸為 \(l\),縱軸為 \(r\)),把區間轉化為二維平面上的點 \((l,r)\),把詢問區間轉化為矩形。
不過由於區間有個性質是 \(l\le r\),在平面上反饋過來的就是隻有一個半平面才有貢獻,所以我們所說的「矩形」具體的講應該是一個「三角形」的樣子,不過我們還是可以把它看成 2-side 矩形。
問題就被轉化為查詢 \(y\) 軸上的一個區間從 \(x = 1\) 到 \(x = now\) 這段時間中,總共有多少位置滿足條件,使用一個能夠維護歷史資訊的資料結構維護即可。
如果剛開始不能理解的話(像我一樣),可以嘗試這樣理解:
我們對 \(y\) 軸上的每個位置 \(i\) 維護當前是否合法的值 \(a_i\)。
然後再給每個位置 \(i\) 引入一個計數器 \(cnt_i\)。
每次掃描線向右側移動的時候,就是在進行全域性 \(cnt_i \gets cnt_i +a_i\) 的操作。
P3246 [HNOI2016] 序列
一個比較經典的問題,如果看懂上面內容的話就可以一眼秒了。
首先將區間轉化為平面上的點,即建系,橫座標為 \(l\),縱座標為 \(r\),然後考慮每一個最小值支配的區域,假設當前序列的最小值 \(a_{\min}\) 的位置為 \(x\),則能夠發現任何包含了 \(x\) 位置的區間其貢獻都是這個數,對應到平面上就是一個\(l \le x\) 且 \(r \ge x\) 的 2-side 矩形。
如果你笨一點的話,可以使用分治將所有矩形預處理出來,如果你不想被別人說笨蛋的話,也可以使用單調棧維護。
不難發現查詢是一個 4-side 矩形,考慮差分,將其變成 3-side 矩形,發現線上需要維護一個「區間覆蓋,區間查詢歷史版本和」這樣的操作,線段樹維護即可。
當然這個問題存在更牛逼的「 強制線上 + \(O(1)\) 查詢」,且也是一個挺好的 trick,不出意外的話以後會聊到。
時間複雜度 \(O((n + q)\log n)\)。
CF997E Good Subsegments
(被析合樹橄欖了)
轉化一下題意先:
給定一個長度為 \(n\) 的排列 \(P\),每次查詢區間 \([l,r]\) 中有多少子區間 \([l',r']\) 滿足 \(\max_{i=l'}^{r'}p_i-\min_{i=l'}^{r'}p_i = r' - l'\)。
考慮套用上面寫的套路,將每個區間表示為二維平面上的點 \((l',r')\)。
首先初始化 \((l',r')\) 的權值為 \(r' - l'\),顯然,對 \(l \in [1,n]\) 做矩形減,對 \(r\in [1,n]\) 做矩形加。
呆一點的,在序列上進行最值分治(你用單調棧也行),具體地說,每次分治的序列中的最大/最小值都是分支中心,然後 \(\max\) 進行矩形加,\(\min\) 進行矩形減。
此時,問題被轉化為給定一個平面,進行 \(O(n)\) 次矩形加減,問一個矩形內部有多少 \(0\)。根據前面的推論,顯然詢問矩形是一個 2-side 矩形。然後使用掃描線和線段樹沿著橫/縱軸掃描整個平面,並把前面的矩形加減拆成線上序列加減(這個算平凡的)。
由於矩形內元素一定非負,所以套用「矩形面積並」時的線段樹維護套路,即可計算 \(0\) 的個數。
問題來了,我們在掃描線後的 2-side 矩形詢問,需要查詢一個區間在字首時間中 \(0\) 的個數,這個線段樹打個歷史標記也可以做,不過需要細細的討論一下,注意細節。
時間複雜度 \(O((n+m)\log n)\)。
EC final 2020 G. Prof. Pang's sequence
問題轉化:
區間有多少子區間顏色數為奇數 \(\iff\) 區間每個子區間顏色數\(\bmod 2\) 的和。
依舊是考慮將區間轉化為平面上的點,建系,讓橫軸為序列維(對應詢問 \(r\)),縱軸也為序列維(對應詢問 \(l\)),掃描線在橫軸上跑。
考慮掃描的過程中遇到一個值 \(a_r\),考慮找到 \(a_r\) 的前驅 \(a_p\),則對於左端點在 \([p+1, r]\) 的所有區間來說,\(a_r\) 是一種新的顏色,否則不應發生變化。
現在問題就簡單了,由於我們線上的每個位置只會是 \(0/1\) 的取值,所以我們在掃描線上維護一個「區間取反,區間和」這樣的一個東西,查詢就變成每依次區間查詢,然後貢獻給對應的查詢矩形。
不過這樣查詢顯然是要死掉的,所以差分一下,就變成了查詢區間歷史和,仿照上一個題用線段樹簡單維護即可。
時間複雜度 \(O((n+q)\log n)\)。
P8421 [THUPC2022 決賽] rsraogps / P9335 [Ynoi2001] 雪に咲く花
全新的視角。
離線做掃描線,將詢問變成矩形,由於詢問資訊可差分,直接在每個位置 \(i\) 上維護詢問資訊的字首和 \(s_i\),詢問答案減一下就好了。
現在考慮怎麼維護這個字首和,考慮掃描線向右移動 \(1\),如果這個時候 \(A_i,B_i,C_i\) 均未發生變化,那麼 \(s_i\) 的變化量肯定與上次移動保持一致,於是不難想到將 \(s_i\) 變成一個一次函式的形式 \(k_ix+b_i\),現在要修改的就只剩下 \(A_i,B_i,C_i\) 發生變化的部分,不難發現 \(A_i,B_i,C_i\) 只會發生 \(O(\log V)\) 次變化,總共發生 \(O(n\log V)\) 次變化,所以直接暴力修改就好。
時間複雜度 \(O(n \log V + m)\)。
可供練習的題:
對一維分治
有些時候,我們所維護的資訊不能差分,這個時候就需要祭出我們的分治。
考慮對一維度分治後,此時將一個 4-side 矩形變成兩個 3-side 矩形(考慮從中間劈開,然後變成兩個開口相對的 3-side 矩形),跑兩邊掃描線,就可以做到只有插入沒有刪除了。
由於分治每一次處理所有跨過分支中心的矩形,然後在向下繼續分治,所以複雜度會多一個 \(\log\),複雜度證明請類比歸併排序。
P1609 [Ynoi2009] rprmq1
由於這個題實在是太毒瘤了,這裡就不放題解了,大家自己做做就好 /dk
二維掃描線
我們發現,一維掃描線是對某個維度排序,每次查詢 \([1,l]\) 的資訊,然後沿著從 \(1\) 到 \(n\) 的路徑掃描全部的詢問。
那啥是二維掃描線呢?套用一維掃描線的行為,我們嘗試得到二維掃描線的行為:
每次詢問 \([l,r]\) 的資訊,以一種方式排序,使得經過所有詢問,並且複雜度可接受。
實際上就是莫隊,這個可以看看 Alex_Wei 的莫隊學習筆記,這裡不多贅述。
自由度
考慮一維掃描線 \(+\) 一維資料結構可以處理二維靜態問題。
然而實際上普通的維護序列問題實際上也屬於兩個維度。
因為是按照給定的操作序列操作的,這裡不乏出現時間的維度。
所以每一次操作都可以看做是一個 3-side 矩形,然後時間一維,序列一維,在時間維上做掃描線,就將二維靜態問題轉化為了一維動態問題。
於是這啟發我們:
掃描線序列維,資料結構時間維
由於序列上的動態操作可以看做成是序列一維,時間一維。
所以在有些情況下我們直接沿著時間維度跑不好處理,這個時候我們就可以考慮在序列維度上做掃描線,然後線上維護時間維。
這種問題一般都是線上上查詢一個單點的資訊,當然,形勢好的話查詢區間資訊也是可以的。
Comet OJ - Contest #14 D / P8512 [Ynoi Easy Round 2021] TEST_152
直觀的看這個題似乎特別不好處理。
不妨從貢獻的角度看這個題,把「經過操作序列上 \([x,y]\) 的操作後形成的最終序列上每個位置的和」給拆成「每個時間點對最終序列的貢獻的和」。
這會產生兩個性質:
- 某個時間產生的貢獻會隨著時間的流逝逐漸消失,換句話說,時間點 \(i\) 產生的貢獻是不增的。
- 時間點 \(i\) 的貢獻是無後效性的,他不會受到前面的時間的影響,他只會被後面的時間所影響,這樣就意味著執行了 \([1,y]\) 操作和執行了 \([x,y]\) 操作後,時間點 \(x\) 的貢獻一致。
現在容易想到建系,橫軸為操作序列維,縱軸為時間維,然後輔助維護一個執行完 \([1,y]\) 操作的 \(c\) 陣列。
考慮掃描線在橫軸上跑,線上維護每個時間點對當前 \(c\) 陣列的貢獻,然後查詢就是線上的一個區間和。
現在考慮掃描線右移會發生什麼,假使遇到了操作 \((l',r',v')\),則此時對 \(c\) 陣列進行操作就是 \(c_i\gets v'\quad(i\in [l',r'])\),在增量的視角中就是 \(c_i\) 增加了 \(v'-c_i\),然後線上上減少那部分時間點的貢獻,最後增加當前時間點的貢獻。顯然,線上一個樹狀陣列就夠了。
由於在我們給 \(c\) 陣列賦值的時候是一個顏色段均攤,所以時間複雜度還是 \(O((n+m)\log n)\)。
P5524 [Ynoi2012] NOIP2015 充滿了希望
感覺正著掃做操作 \(1\)、\(2\) 可能是個天坑,於是考慮倒著做掃描線,也就是固定左端點,自由右端點。
考慮一個操作 \(3\) 在前面被一個操作 \(2\) 覆蓋了,則左邊面的所有操作對當前操作來說都無效了,也就是每個操作 \(3\) 只會被覆蓋一次。
再考慮操作 \(1\) 對操作 \(3\) 的影響,發現如果操作 \(3\) 已經被操作 \(2\) 覆蓋了,則此時的操作 \(1\) 是無用的;如果沒被覆蓋,則說明在未來我們的 \(x,y\) 的意義是反著的,此時若有操作 \(2\) 覆蓋了 \(y\) 等於在操作 \(1\) 後面覆蓋了 \(x\),這一個性質啟發了我們遇到操作 \(1\) 時應該直接交換,這樣才不影響正確性。
然後站在操作 \(2\) 的角度上觀察,發現有可能一次操作 \(2\) 會在同一個位置覆蓋掉多個操作 \(3\),這啟發我們用 vector
維護每個位置的操作 \(3\)。
更加深入的,發現交換操作等價於「兩次單點染色」,發現待覆蓋的區域和一次覆蓋的區域可以顏色端均攤,用 ODT 維護這個顏色端均攤就好。
綜上,不難得出掃描線左移是我們需要幹什麼:
- 遇到操作 \(1\) 時:直接交換兩個位置的
vector
,ODT 上交換兩個位置的顏色。 - 遇到操作 \(2\) 時:ODT 上區間染色 \(1\),然後對每一個顏色為 \(0\) 的位置依次處理完詢問,並清空
vector
。 - 遇到操作 \(3\) 時:ODT 單點染色 \(0\),然後將當前操作編號壓入對應位置的
vector
中。
每次查詢的時候就是一個區間和,直接樹狀陣列維護就好,時間複雜度 \(O((m+q)\log n)\)。
P7560 [JOISC 2021 Day1] フードコート
建系,橫軸為序列維,縱軸為時間維,掃描線在橫軸上跑。
此時操作序列中的修改變成了橫著的線,詢問變成了豎著的線,橫向差分一下,就變成了「單點修改、區間查詢」。
思考如何做查詢(也就是做操作 \(3\)),假設我們是在 \(i\) 時刻做的操作 \(3\),那麼只需要找到 \(i\) 之前最近一次佇列空的時候 \(t\),在時間 \([t, i]\) 之間的每一次 pop
必然都是有效的,否則的話中間必然還要再空一次,不滿足「最近」這一個條件,這樣查詢操作就變成了在這一個區間裡面二分了。
考慮如何刻畫這個最近一次佇列空,設 push
\(t\) 個元素為單點 \(+t\),pop
\(t\) 個元素為單點 \(-t\),此時距離 \(i\) 時刻最近一次佇列空的時刻就是 \([1,i]\) 中最靠右的最大字尾和的位置,證明如下:
先證明此時空了:由於我們是最大字尾和,所以這之前必定是空的,否則我們就可以透過左端點向右擴張來增大我們的字尾和。
在證明此後不空:假設 \(x\) 時刻是最大字尾和左端點, \(y\) 時刻佇列空了,\(x \lt y\),那麼必然出現了 \([x,y]\) 的和 \(\lt 0\),我們的最大字尾和一定不會去選擇一個負數的字首,所以該情況不可能出現。
由於會出現單點 \(-t\) 這種牛魔東西不好二分,所以不如直接維護字尾和,這時候單點 \(\pm t\) 變成字首 \(\pm t\),然後查詢的時候注意別忘記消減掉後面時間帶來的影響。如果此時我們查出來的 \(\lt k\),那麼可以直接跑路了,因為此時佇列必定沒有 \(k\) 個元素。
現在我們已經找到了最近一次佇列空的時間 \(t\),思考如何在 \([t,i]\) 上二分,首先查出 \([t,i]\) 中的 pop
量,然後給這個 pop
量加上 \(k\),表示要找第 \(k\) 個位置,在 \([t,i]\) 上找到第一次 \(\ge\) pop
量的位置,然後讀取這個位置的屬性,即為答案,時間複雜度 \(O((n + q)\log n)\)。
當然,如果你不是很會怎麼單 \(\log\) 線上段樹的一個區間上二分,你可以像我一樣來一個笨的方法,就是先區間查出 \([1, t-1]\) 位置上的
push
和,然後把他丟進pop
量裡就可以了。
別忘了還原消減操作!!!
P8518 [IOI2021] 分糖果
建系,橫軸為序列維,縱軸為時間維,套用 P7560 的套路,只要我們找到了最後一次觸碰上界或者下界的時刻 \(t\),在 \(t\) 時刻之後的部分就變成了一個簡單的求和,或者找到一個刻畫 \(t\) 時刻之後部分貢獻的方法。
那麼目前亟待解決的問題就是如何描述「最後一次觸碰上/下界」這一個東西。
設當前正在被操作的原序列 \(A\) 位置為 \(j\),時間序列為 \(time\),定義「制裁」為「當前操作結束後觸碰到上/下界時,對\(a_j\) 取 \(\min\) 或 \(\max\) 」的行為,定義「觸碰」為「當前操作結束後,位置 \(a_j \gt c_j\) 或 \(a_j \lt 0\)」。
先忽略上界,考慮什麼時候會碰到下界,不難發現在時間序列的最小字首和 \([1,x]\) 中的 \(x\) 上,證明如下:
假設 \(h\) 時刻被制裁,那麼如果 \(t\) 時刻還想被制裁,則必須要保證時間序列上 \(t\) 位置上的數 \(<\) \(-\max(0, \sum_{i=h+1}^{t-1} time_i)\),否則一定不會被制裁。
這就意味著如果你選上了一段和為非負數的區間,那麼必然要選其後面的那一個被制裁的地方,此時才能保證當前字首和更小,否則一定不會選那一段區間。
現在考慮加上上界,不難發現下面這個性質:
- 若 \(i\) 時刻觸碰上界,\(j\) 時刻觸碰下界,則時間序列上 \([i,j]\) 區間的和一定 \(\lt -c\)。
- 若 \(i\) 時刻觸碰下界,\(j\) 時刻觸碰上界,則時間序列上 \([i,j]\) 區間的和一定 \(\gt c\)。
綜上,得「相鄰的上下界觸碰事件」之間區間的和的絕對值一定 \(\gt c\)。
現在我們的「最後一次觸碰上/下界」一定在最靠左滿足這一條件的區間的左端點或者右端點,然而左端點只會有一種情況(就是有可能在觸碰下界後一段或觸碰上界後一段的區間和的絕對值 \(\gt c\)),對於這種情況我們取一下當前序列的字首最大/小值就好了。
最後就剩怎麼找我們所需要的那個區間,簡單的,考慮線段樹維護「字首最小和、字首最大和、區間和」,然後絕對值最大子段和就是「字首最大和 \(-\) 字首最小和」,修改是單點修改,查詢時線上段樹上二分即可。
時間複雜度 \(O((n+q)\log q)\)。
UOJ 515. 【UR #19】前進四
你會發現這個東西長得很像我們的樓房重建,那麼自然不難想到兩個 \(\log\) 的做法,不過這裡的期望時間複雜度為單 \(\log\)。
考慮離線,建系,設橫軸為序列維,縱軸為時間維,考慮倒著在序列維上做掃描線,線上維護時間。
轉換角度觀察這個問題,發現每一次單點修改都影響了從「本次修改」到「下一次修改」的整個時間,故線上上維護取 \(\min\) 修改,然後發現查詢等價於查詢截止到這個時間,一共發生了多少次成功的取 \(\min\) 操作,換句話說,就是截止到當前時間、當前位置的字尾最小值一共變了幾次。
上面這一車東西考慮做「Segment tree beats」,時間複雜度 \(O((n+Q)\log n)\)。
計算幾何中的掃描線
迴歸掃描線的本質,實際上就是拿一條線去掃平面來獲得所需要的資訊。
因此較為顯然的,我們也可以將掃描線用在計算幾何中。
通常的套路是維護掃描線與幾何圖形截點的位置,與幾何圖形下一個拐點的位置。
由於我的計算幾何很菜,所以只能說兩個比較簡單的題目。
交點檢測
給定平面上 \(n\) 條線段,輸出所有交點的位置。
保證輸出量 \(\le 10^5\)。
掃描線掃描 \(x\) 軸,線上維護縱軸,也就是每個線段與掃描線的交點。
這樣有了一個較好的性質,就是如果線段 \(l\) 要與其他線段產生交點,必定先與在掃描線上的相鄰線段有交點。
那麼此時維護掃描線上相鄰線段什麼時候發生交點,計算交點並在掃描線上交換兩個線段的位置。
由於是線段,所以還需要維護線段的加入事件和刪除事件。
綜上,用平衡樹維護一下上述操作就好了,設交點數為 \(o\),則時間複雜度為 \(O((n + o)\log n)\)。
平面圖點定位
給定平面上 \(n\) 條直線,將平面劃分為了一個平面圖
你需要建出這個平面圖。
類比交點檢測,發現如果出現了交點,則說明這個的一個區域被封閉了。
這樣一直做就可以找到所有區域以及相鄰區域。
設區域數為 \(o\),則時間複雜度為 \(O((n + o)\log n)\)。
可供練習的題:
樹上掃描線
考慮在序列上的時候我們的詢問會轉化為 \(O(1)\) 個矩形。
現在問題上樹,不難想到將樹上問題轉化為序列上的問題,故進行樹鏈剖分。
這裡利用了一個樹鏈剖分的一個非常優秀的性質:「樹上的任意一條路徑劃分成不超過 \(O(\log n)\) 條連續的鏈」。
由於這一個性質,我們將拆分出來的鏈分別做矩形,此時最多會出現 \(O(\log n)\) 個矩形。
如果考慮路徑上點之間的關係的話,則最多會出現 \(O(\log^2 n)\)。
然後我們就在原先複雜度基礎上多了一個或者兩個 \(\log\),複雜度在大部分情況下依舊是可以接受的。
未知渠道獲取的題
定義一個點對 \((i,j)(i \not= j)\) 是好的當且僅當:
\[\exists k \in N^{*},\qquad \lfloor\dfrac{i}{k}\rfloor = j \]給定一棵 \(n\) 個點的樹,樹上點的權值互不相同,點權的值域為 \(m\),共有 \(q\) 次詢問。
每次詢問給定一個路徑 \((x,y)\),問路徑上多少個點對 \((u,v)\) 滿足 \((a_u, a_v)\) 是好的。
\(1 \le n,m,q \le 5\times 10^4\),時限 \(2.5\) 秒。
考慮整除分塊,預處理出所有符合條件的點對。
建系,設橫縱座標都為序列維(dfn 維),則點對變成平面上的點。
對於每個路徑,預處理所有可能的詢問矩形,即兩個鏈組合在一起組成一個詢問矩形,由於樹鏈剖分的性質,不難發現矩形數是 \(O(q\log^2 n)\) 的。
然後現在的問題就變成一個簡單的二維數點,時間複雜度 \(O(q \log ^ 3 n + n \sqrt m \log n)\),足以透過該題,由於常數極小,把標算給錘了。
可供練習的題: