Sqrt tree 學習筆記

博客園同步

前置知識

你首先要學會的:

  • RMQ(ST)\text{RMQ}(ST \text{表})
  • 分塊
  • 線段樹
  • 二進制,位運算

前記

我們把 RMQ\text{RMQ} 和分塊 所解決的問題搬出來:

  • 在長度爲 nn 的數組上,TT 組詢問 [l,r][l,r] 區間的 和(差,異或等有結合律的算術)

顯然我們已經有了一個 O(nlogn)O(1)\mathcal{O}(n \log n) - \mathcal{O}(1) 的算法。

思考

現在做一個 簡單 的加強,n,T2×107n,T \leq 2 \times 10^7.

你會發現預處理的時間不夠了。

很顯然,我們考慮樸素的分塊怎麼做。

將數組分爲若干個長度爲 bb 的塊,對於每個塊,處理塊內答案,前綴塊的答案,後綴塊的答案

b=nb = \sqrt{n} 時 取到最平均的答案,可以 O(n)O(n)\mathcal{O}(n) - \mathcal{O}(\sqrt{n}),但顯然不行。

那考慮 RMQ\text{RMQ} 呢?用 fi,jf_{i,j} 表示 [i,i+2j1][i , i + 2^j-1] 區間的答案,倍增處理即可。顯然是 O(nlogn)O(1)\mathcal{O}(n \log n) - \mathcal{O}(1).

這兩個算法都無法通過 2×1072 \times 10^7 如此龐大的數據。我們思考一個新的算法。

基於分塊

首先考慮分塊基礎上的優化。同樣 處理塊內答案,前綴塊的答案,後綴塊的答案,這裏是 O(n)\mathcal{O}(n) 的,考慮如何優化詢問。

詢問分兩種情況:

  • 橫跨多個塊
  • 包含在一個塊內

顯然,第一種情況我們可以把 橫跨的整塊 進行前綴計算,然後轉爲 對兩側不完整快的計算,即剩餘兩個部分各自包含在一個完整的塊內。這樣做是 O(1)\mathcal{O}(1) 的,但如何計算第二種方案?

考慮如何處理一個塊內的方案,如果按照 分塊的暴力思想,那麼 O(n)\mathcal{O}(\sqrt{n}) 就奠定了。顯然這不是我們想要的。

現在的問題轉爲,在長度爲 n\sqrt{n} 的數組上,TT 組詢問 [l,r][l,r] 區間的 和(差,異或等有結合律的算術)

咦?這不是分塊嗎?

人見人愛的 分塊套分塊 又重出江湖了?

我們可以設法將 n\sqrt{n} 長的塊進行再分塊,分爲 n\sqrt{\sqrt{n}} 長的塊,然後再不斷往下分 \cdots \cdots 直到塊長爲 11 爲止。

首先,似乎這樣的時間複雜度很穩,O(n+n+n+)=O(n)\mathcal{O}(n + \sqrt{n} + \sqrt{\sqrt{n}} + \cdots \cdots) = \mathcal{O}(n),但空間上無法承受,需要處理的區間太多。

既然分塊失敗了,我們考慮在分塊的基礎上,思考新的算法。

Sqrt tree\texttt{Sqrt tree} 的建樹

俗語云:“智商不夠,數據結構來湊”。

現在我們想的一種 基於遞歸,搜索 的算法,能否有數據結構與其接軌呢?

顯然,我們可以用 來實現。

對長度爲 kk 的區間,將其分裂爲 k\sqrt{k} 個長 k\sqrt{k} 的子區間,即有 k\sqrt{k} 個兒子。葉子節點的長度爲 1122.

第一層的節點維護 [1,n][1,n] 的答案。

假想一下:如果這棵樹只建立 22 層,可以認爲和 樸素分塊 沒有任何區別。

顯然我們建完這棵樹後,應當考慮複雜度怎樣。

整棵樹的高度是 O(loglogn)\mathcal{O(\log \log n)} 的,每層區間總長爲 nn,建樹的時間複雜度爲 O(nloglogn)\mathcal{O(n \log \log n)},可以接受。而空間上也可以接受。

那麼問題來了:在這樣類似於 線段樹 的數據結構上,如何詢問呢?

Sqrt tree\text{Sqrt tree} 的詢問

在樹高爲 O(loglogn)\mathcal{O}(\log \log n) 的樹上,按照線段樹的思想,顯然單次查詢是 O(loglogn)\mathcal{O}(\log \log n) 的。這樣並不優。

如何優化?

Sqrt tree\text{Sqrt tree} 的詢問優化

其實瓶頸在於如何快速確定樹高。樹高很好確定,直接二分就行。那麼這樣就變成了 O(logloglogn)\mathcal{O}(\log \log \log n).

對於 n=2×107n = 2 \times 10^7,這個數已經變成了近似 22幾乎 可以視爲常數。

我們考慮如何把這個數徹底地降爲 O(1)\mathcal{O}(1),以免夜長夢多!

Sqrt tree\text{Sqrt tree} 的詢問再優化

精髓來了。

上面我們已經做到了 O(nloglogn)O(logloglogn)\mathcal{O}(n \log \log n) - \mathcal{O}(\log \log \log n) 的算法,我們試圖做到一個 O(nloglogn)O(1)\mathcal{O}(n \log \log n) - \mathcal{O}(1).

思考一下:RMQ\text{RMQ} 的預處理,如果你在詢問的時候暴力倍增,不也是 O(logn)\mathcal{O}(\log n) 嗎?最後我們用 二進制 的特效完成了從 O(logn)\mathcal{O}(\log n)O(1)\mathcal{O}(1) 的跳躍,讓 RMQ\text{RMQ} 徹徹底底的戰勝了 線段樹(只是在兩者都能解決的領域)。

現在我們仍要運用這個黑科技,二進制的運算從來不庸俗。

所以,按照 OI Wiki\text{OI Wiki} 的說法,

非常形象。如果你覺得這些太枯燥,我們來簡單解釋一下。

首先假設所有區間的長度都可以表示爲 2t(t)2^t (t爲自然數) 的形式,對於不滿足的區間我們可以在後面添上一些 不影響運算 的數值,在 ++ 中可以是 00. 由於區間的增大最多 ×2\times 2 不到,因此不影響複雜度。

這樣我們可以來用二進制了。首先我們把端點寫成二進制的形式,由於每個塊長度一樣,端點呈 等差 形式,因此 每個塊的兩個端點的二進制有且僅有後 kk 位不同。所以顯然的,我們預處理的時候可以求出 kk,並可以 O(1)\mathcal{O}(1) 計算 當前區間兩個端點的二進制值

如何判斷當前區間是否涵蓋在一個塊裏?顯然這個問題就變成,當前取件兩個端點的二進制是否只有後 kk 位不同。這個問題不難解決。我們只需要將區間兩端進行 異或操作,判斷是否 2k1\leq 2^k-1 即可。

那麼如何快速找到樹高呢(即對應再第幾層)?對當前 [l,r][l,r] 計算最高位上的 11 的位置,並處理 i[1,n]i \in [1,n]ii 的最高位上 11 的位置。這樣可以 O(1)\mathcal{O}(1) 計算樹高,O(1)\mathcal{O}(1) 查詢啦!

這就是 Sqrt tree\text{Sqrt tree},可以實現 O(nloglogn)O(1)\mathcal{O}(n \log \log n) - \mathcal{O}(1).

可能你會說,O(nloglogn)\mathcal{O}(n \log \log n) 對於 n=2×107n = 2 \times 10^7 會跑到 10810^8 左右,不穩。

O(nlogn)O(1)\mathcal{O}(n \log n) - \mathcal{O}(1) 就穩了?

O(n)O(n)\mathcal{O(n) - \mathcal{O}(\sqrt{n})} 就穩了?

顯然 Sqrt tree\text{Sqrt tree} 已經是此類問題最優的算法,沒有之一!

Sqrt tree\text{Sqrt tree} 的修改

現在出題人意外地發現你用 不帶修改 的簡單 Sqrt tree\text{Sqrt tree} 模板切題了!

出題人非常生氣,毫不客氣地加上了這樣一句話:

  • 每次操作分詢問和修改兩種。

那麼如何修改呢?

直接在 Sqrt tree\text{Sqrt tree} 上暴力?

Sqrt tree\text{Sqrt tree} 的單點修改

首先考慮單點如何修改 ax=vala_x = val

暴力的話,我們需要更改樹上含 xx 的區間,顯然我們需要重新計算的區間爲:

O(n+n+n+)=O(n)\mathcal{O}(n + \sqrt{n} + \sqrt{\sqrt{n}} + \cdots \cdots) = \mathcal{O}(n)

但是你覺得這樣很滿足嗎?那出題人給你個修改都要 O(n)\mathcal{O}(n),豈不是要讓 O(nT)\mathcal{O}(nT) 的暴力通過了?

Sqrt tree\text{Sqrt tree} 的單點修改優化

類似於線段樹吧,可以打 lazy_tag\text{lazy\_tag},不必完全暴力的。

這裏引進 Index\text{Index} 的概念,記錄每個區間被修改的塊編號。

那麼 O(n)\mathcal{O}(\sqrt{n}) 很穩。但是這樣詢問也增加了複雜度。

Sqrt tree\text{Sqrt tree} 的區間修改

考慮如何將 [l,r][l,r] 的所有數都改成 xx.

我們已經有了以下的算法(修改 - 查詢):

O(nloglogn)O(1)\mathcal{O}(\sqrt{n} \log \log n) - \mathcal{O}(1)

O(n)O(loglogn)\mathcal{O}(\sqrt{n}) - \mathcal{O}(\log \log n)

下面會一一解釋這兩種實現。

區間修改的第一種實現

第一種實現中,把第一層 [l,r][l,r] 完全覆蓋的區間 打上標記。

兩側的塊修改一部分,沒有辦法,只能暴力,O(nloglogn)\mathcal{O}(\sqrt{n} \log \log n).

查詢的話,直接去 Index\text{Index} 裏查詢,O(1)\mathcal{O}(1).

區間修改的第二種實現

每個節點都會被打上標記,那麼每次詢問要把祖先的標記更新一遍,就會形成 O(n)O(loglogn)\mathcal{O}(\sqrt{n}) - \mathcal{O}(\log \log n) 的複雜度。

總結

Sqrt tree\text{Sqrt tree} 在不帶修改的情況下效率比 線段樹 要高很多,但是實現的功能不如 線段樹 多(比方說線段樹可以維護 LIS\text{LIS});

帶修改時,RMQ\text{RMQ} 無疑最優,其次是 線段樹,然後是分塊和 Sqrt tree\text{Sqrt tree}.

OI\text{OI} 中不常考,但希望大家掌握。

參考資料

Sqrt Tree - OI Wiki\text{Sqrt Tree - OI Wiki}

課後習題

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章