本篇將介紹些 CP 值較高的資料結構
其中 C 代表的是 coding 複雜度,
或把 CP 譯作競程
RMQ 全名 Range Maximum/Minimum Query
給定長度 的數列 ,查詢任意區間極(最大/最小)值
樸素的枚舉區間會有 的查詢複雜度
不失一般性,下面只以找最大值為例
定義狀態 表示數列從 位置開始,長度 區間 所有數中最大值
例如 則 ,位置從 開始
最大值可從更小的長度 兩個區間比較得來,起點分別在 與
則狀態轉移為
而邊界為長度 區間
建構 Sparse Table 的時間複雜度為
如此能利用 求得區間 的最大值
注意這裡區間表示方式是左閉右開符號
ZEROJUDGE d539 區間 MAX
TIOJ 1603 胖胖殺蚯事件
線段樹是各種區間問題的絕招,相較其他延伸應用 本節只是冰山一角
建議先熟悉第三週的分治法以及第五週的合併排序法再閱讀本節
先考慮一個非常眼熟(?)的問題
給定數列 ,有 個操作包括:更新數列上的值或是查詢一個解
針對更新哪些值,以及查詢哪些解,精確的分類可分為:
而所謂要求的解(答案)可能有:
線段樹,顧名思義就是以保存很多區間解的一顆樹,例如數列長度 :
(注意這裡區間表示方式是左閉右開符號)
先明確的以簡單的問題設計簡單的線段樹
給定長度 的數列 ,有 個操作包括:
例如 , 筆詢問為:
將 加上 則
將 加上 則
詢問區間 的值,則區間總和為
線段樹的節點保存該區間的解,以及左右子節點的位置
根據給定數列把初始線段樹造出來
以 為例,線段樹會長這樣
區間旁的數代表總和,例如區間 的總和為
將數列中某位置 的數加上
以前個樹為例,將 加上 :
每次只往包含 位置的區間遞迴下去,複雜度為
查詢區間 的總和
以前個樹為例,詢問區間 的總和:
這樣的遞迴查詢,每一層最多只會往下遞迴兩個節點,所以複雜度為
利用反證法能證明當某層往下遞迴超過兩個節點會出現矛盾
HDU OJ 1166 敌兵布阵
CODEFORCES 339D Xenia and Bit Operations
*CODEFORCES 1253E Antenna Coverage
變得棘手一些的問題
給定長度 的數列 ,有 個操作包括:
例如 , 筆詢問為:
將 加上 則
詢問 的值,則區間總和為
可採用前面提的"單點更新/區間查詢"線段樹,
欲更新區間內所有數,對每數都單點更新,但總複雜度為
同理,這樣寫也可以做到區間更新:
以 為例將 所有數加上 :
可是複雜度仍然為
顧名思義,就是懶
懶是工程師的美德
由於更新區間的總和同時也將其子節點一併更新,這麼勤快肯定會花不少時間
如果當前區間的數全都得更新,可先偷懶做個 tag 值表示下方的子孫節點還未更新上 tag,
等到真正需要查(遞迴)到其子孫節點,在將 tag 往下推給子節點,以完成更新
節點保存該區間的解及 tag,以及左右子節點的位置
pull()
將左右子區間的總和加起來push()
將該區間的 tag 給左右子區間,並完成子節點的更新以 為例將 所有數加上 :
其中左上角的值代表 tag 值。
與沒有 lazy tag 的區間更新相比,走訪的區間少了許多
以前個樹為例,詢問區間 的總和:
觀察到,在查詢過程中會將 tag 往下 push 給子區間
POJ 3468 A Simple Problem with Integers
CODEFORCES 52C Circular RMQ
又叫做 Fenwick Tree
翻譯為二元索引樹,可看作是線段樹的特化版本
整數可由一些 的冪 組成,且每種冪次可只用一次
例如 ,二進制表示為
每個數字可由這種方式唯一的表示。換句話說,兩個不同數字就得用不同方式表現
也就是說用 表達成 也沒問題
那麼用 表達同一件事也可以
指的是 用二進制表達時最低位的 表示的 的冪
以二進制表達:
以二進制表達:
以二進制表達:
證明二補數系統下運算 x&-x
是 的 lowbit
設 證明
有了以上的基礎,可以回到正題了
給定長度 的數列 ,有 個操作包括:
若只有查詢操作,透過前綴和 做 能得 區間和
但還得考慮更新操作,所以普通的前綴和是不行的
每次單點更新就得重新計算前綴和,複雜度會過高
根據前面基礎,用 for(; x >= 1; x-=lowbit(x)) sum += BIT[x]
求得 區間和
例如 那麼 sum
會得到 BIT[7] + BIT[6] + BIT[4]
其實就是把 區間和表達成
兩者結合就是:BIT[
] + BIT[
] + BIT[
]
而其中 BIT[x]
就能構造為 的總和
例如 的總和就是
BIT
這種奇特區間定義能表示成一棵樹,例如 的樹:
(觀察可知,這棵樹是把線段樹的右子樹通通移除的結果)
而若要更新數列的數,也要把包含此數的區間一併更新
例如更新位於位置 的數,則 要更新
也會包含 位置,也得更新,依此類推
單點更新
複雜度為 ,由於每次 lowbit 會至少進 1 位
求前綴和
複雜度為 ,因為每次會去掉一個 bit
練習:
CODEFORCES 356A Knight Tournament
CODEFORCES 459D Pashmak and Parmida's problem
CODEFORCES 1042D Petya and Array
TIOJ 1175 Longest Increasing Subsequence
TIOJ 1080 A.逆序數對
給定長度 的數列 ,有 個操作包括:
應用差分(Difference)技巧令 且
且透過 建立 Binary Indexed Tree
則若是將所有數 加上 ,其中
那麼 與 會變成:
也就是說 增加了 以及 增加
而其餘的 都未增加任何值,因為差分定義讓 相消了
則利用兩次單點更新
做 update(L, d)
及 update(R+1, -d)
就能完成區間更新了
根據差分定義推得 ,所以可利用區間查詢
做 pre(i)
能單點查詢 的值
給定長度 的數列 ,有 個操作包括:
根據差分定義有
能推得
能推得
依此類推得
於是,再多維護一個以數列 建立的 Binary Indexed Tree 就能求前綴和了