博客園同步
前置知識
你首先要學會的:
- RMQ(ST表)
- 分塊
- 線段樹
- 二進制,位運算
前記
我們把 RMQ 和分塊 所解決的問題搬出來:
- 在長度爲 n 的數組上,T 組詢問 [l,r] 區間的 和(差,異或等有結合律的算術)。
顯然我們已經有了一個 O(nlogn)−O(1) 的算法。
思考
現在做一個 簡單 的加強,n,T≤2×107.
你會發現預處理的時間不夠了。
很顯然,我們考慮樸素的分塊怎麼做。
將數組分爲若干個長度爲 b 的塊,對於每個塊,處理塊內答案,前綴塊的答案,後綴塊的答案。
當 b=n 時 取到最平均的答案,可以 O(n)−O(n),但顯然不行。
那考慮 RMQ 呢?用 fi,j 表示 [i,i+2j−1] 區間的答案,倍增處理即可。顯然是 O(nlogn)−O(1).
這兩個算法都無法通過 2×107 如此龐大的數據。我們思考一個新的算法。
基於分塊
首先考慮分塊基礎上的優化。同樣 處理塊內答案,前綴塊的答案,後綴塊的答案,這裏是 O(n) 的,考慮如何優化詢問。
詢問分兩種情況:
顯然,第一種情況我們可以把 橫跨的整塊 進行前綴計算,然後轉爲 對兩側不完整快的計算,即剩餘兩個部分各自包含在一個完整的塊內。這樣做是 O(1) 的,但如何計算第二種方案?
考慮如何處理一個塊內的方案,如果按照 分塊的暴力思想,那麼 O(n) 就奠定了。顯然這不是我們想要的。
現在的問題轉爲,在長度爲 n 的數組上,T 組詢問 [l,r] 區間的 和(差,異或等有結合律的算術)。
咦?這不是分塊嗎?
人見人愛的 分塊套分塊 又重出江湖了?
我們可以設法將 n 長的塊進行再分塊,分爲 n 長的塊,然後再不斷往下分 ⋯⋯ 直到塊長爲 1 爲止。
首先,似乎這樣的時間複雜度很穩,O(n+n+n+⋯⋯)=O(n),但空間上無法承受,需要處理的區間太多。
既然分塊失敗了,我們考慮在分塊的基礎上,思考新的算法。
Sqrt tree 的建樹
俗語云:“智商不夠,數據結構來湊”。
現在我們想的一種 基於遞歸,搜索 的算法,能否有數據結構與其接軌呢?
顯然,我們可以用 樹 來實現。
對長度爲 k 的區間,將其分裂爲 k 個長 k 的子區間,即有 k 個兒子。葉子節點的長度爲 1 或 2.
第一層的節點維護 [1,n] 的答案。
假想一下:如果這棵樹只建立 2 層,可以認爲和 樸素分塊 沒有任何區別。
顯然我們建完這棵樹後,應當考慮複雜度怎樣。
整棵樹的高度是 O(loglogn) 的,每層區間總長爲 n,建樹的時間複雜度爲 O(nloglogn),可以接受。而空間上也可以接受。
那麼問題來了:在這樣類似於 線段樹 的數據結構上,如何詢問呢?
Sqrt tree 的詢問
在樹高爲 O(loglogn) 的樹上,按照線段樹的思想,顯然單次查詢是 O(loglogn) 的。這樣並不優。
如何優化?
Sqrt tree 的詢問優化
其實瓶頸在於如何快速確定樹高。樹高很好確定,直接二分就行。那麼這樣就變成了 O(logloglogn).
對於 n=2×107,這個數已經變成了近似 2,幾乎 可以視爲常數。
我們考慮如何把這個數徹底地降爲 O(1),以免夜長夢多!
Sqrt tree 的詢問再優化
精髓來了。
上面我們已經做到了 O(nloglogn)−O(logloglogn) 的算法,我們試圖做到一個 O(nloglogn)−O(1).
思考一下:RMQ 的預處理,如果你在詢問的時候暴力倍增,不也是 O(logn) 嗎?最後我們用 二進制 的特效完成了從 O(logn) 到 O(1) 的跳躍,讓 RMQ 徹徹底底的戰勝了 線段樹(只是在兩者都能解決的領域)。
現在我們仍要運用這個黑科技,二進制的運算從來不庸俗。
所以,按照 OI Wiki 的說法,
非常形象。如果你覺得這些太枯燥,我們來簡單解釋一下。
首先假設所有區間的長度都可以表示爲 2t(t爲自然數) 的形式,對於不滿足的區間我們可以在後面添上一些 不影響運算 的數值,在 + 中可以是 0. 由於區間的增大最多 ×2 不到,因此不影響複雜度。
這樣我們可以來用二進制了。首先我們把端點寫成二進制的形式,由於每個塊長度一樣,端點呈 等差 形式,因此 每個塊的兩個端點的二進制有且僅有後 k 位不同。所以顯然的,我們預處理的時候可以求出 k,並可以 O(1) 計算 當前區間兩個端點的二進制值。
如何判斷當前區間是否涵蓋在一個塊裏?顯然這個問題就變成,當前取件兩個端點的二進制是否只有後 k 位不同。這個問題不難解決。我們只需要將區間兩端進行 異或操作,判斷是否 ≤2k−1 即可。
那麼如何快速找到樹高呢(即對應再第幾層)?對當前 [l,r] 計算最高位上的 1 的位置,並處理 i∈[1,n] 中 i 的最高位上 1 的位置。這樣可以 O(1) 計算樹高,O(1) 查詢啦!
這就是 Sqrt tree,可以實現 O(nloglogn)−O(1).
可能你會說,O(nloglogn) 對於 n=2×107 會跑到 108 左右,不穩。
O(nlogn)−O(1) 就穩了?
O(n)−O(n) 就穩了?
顯然 Sqrt tree 已經是此類問題最優的算法,沒有之一!
Sqrt tree 的修改
現在出題人意外地發現你用 不帶修改 的簡單 Sqrt tree 模板切題了!
出題人非常生氣,毫不客氣地加上了這樣一句話:
那麼如何修改呢?
直接在 Sqrt tree 上暴力?
Sqrt tree 的單點修改
首先考慮單點如何修改 ax=val。
暴力的話,我們需要更改樹上含 x 的區間,顯然我們需要重新計算的區間爲:
O(n+n+n+⋯⋯)=O(n)
但是你覺得這樣很滿足嗎?那出題人給你個修改都要 O(n),豈不是要讓 O(nT) 的暴力通過了?
Sqrt tree 的單點修改優化
類似於線段樹吧,可以打 lazy_tag,不必完全暴力的。
這裏引進 Index 的概念,記錄每個區間被修改的塊編號。
那麼 O(n) 很穩。但是這樣詢問也增加了複雜度。
Sqrt tree 的區間修改
考慮如何將 [l,r] 的所有數都改成 x.
我們已經有了以下的算法(修改 - 查詢):
O(nloglogn)−O(1)
O(n)−O(loglogn)
下面會一一解釋這兩種實現。
區間修改的第一種實現
第一種實現中,把第一層 被 [l,r] 完全覆蓋的區間 打上標記。
兩側的塊修改一部分,沒有辦法,只能暴力,O(nloglogn).
查詢的話,直接去 Index 裏查詢,O(1).
區間修改的第二種實現
每個節點都會被打上標記,那麼每次詢問要把祖先的標記更新一遍,就會形成 O(n)−O(loglogn) 的複雜度。
總結
Sqrt tree 在不帶修改的情況下效率比 線段樹 要高很多,但是實現的功能不如 線段樹 多(比方說線段樹可以維護 LIS);
帶修改時,RMQ 無疑最優,其次是 線段樹,然後是分塊和 Sqrt tree.
OI 中不常考,但希望大家掌握。
參考資料
Sqrt Tree - OI Wiki
課後習題
洛谷 P3793 由乃救爺爺