Sqrt tree 學習筆記


CSDN同步

前置知識

你首先要學會的:

  • \(\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 + \sqrt{n} + \sqrt{\sqrt{n}} + \cdots \cdots) = \mathcal{O}(n) \]

但是你覺得這樣很滿足嗎?那出題人給你個修改都要 \(\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}\)

課后習題

洛谷 \(\text{P3793}\) 由乃救爺爺


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM