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