sqrt-data-structure

iorit發表於2024-03-07

根號資料結構

本文只是對 nzhtl1477 的課件內例題寫寫自己的理解,侵刪。


最基礎的動態分塊

1.區間加,區間求小於 \(x\) 的數的個數

每塊內維護 OV(有序表),整塊查詢 lower_bound,散塊暴力查詢。

修改的話散塊可以重構,或者你注意到修改的位置在 OV 裡拿出來也是有序的,歸併一下也行。

整塊直接打標記即可,查詢記得帶上標記。塊長取 \(\sqrt{n\log n}\) 時複雜度 \(\mathcal O(m\sqrt{n\log n})\)

2.(Ynoi2017 由乃打撲克)區間加,區間求 \(k\) 小值

\(k\) 小值的話可以二分,然後就變成了求區間小於 \(x\) 的數的個數,就是上面那個東西。

然而直接 \(\mathcal O(m\log n\sqrt{n\log n})\) 不夠優秀。

設塊長為 \(B\),修改顯然是 \(\mathcal O(B+\frac n B)\) 的,每次二分的整塊詢問是 \(\mathcal O(\frac n B\log n)\) 的。

問題在於二分內的散塊詢問,是 \(\mathcal O(B)\) 的。

我們考慮將兩個散塊歸併到一起,變成一個塊,和整塊一起詢問。這樣全部複雜度就是 \(\mathcal O(B+\frac n B+\frac n B\log^2n)\)

\(B=\sqrt n\log n\) 有最優複雜度 \(\mathcal O(n\sqrt n\log n)\)


根號平衡

3.\(\mathcal O(1)\) 單點修改,\(\mathcal O(\sqrt n)\) 求區間和。

維護整塊內和即可。

4.\(\mathcal O(\sqrt n)\) 單點修改,\(\mathcal O(1)\) 求區間和。

維護塊內前 \(x\) 個數的和以及前 \(x\) 個塊的和,求區間和用兩個字首和減一下就好了。

5.\(\mathcal O(\sqrt n)\) 區間加,\(\mathcal O(1)\) 求單點值。

每塊維護標記就好了。

6.\(\mathcal O(1)\) 區間加,\(\mathcal O(\sqrt n)\) 求單點值。

原陣列差分一下就變成了上面一個。

7.集合,\(\mathcal O(1)\) 插入,\(\mathcal O(\sqrt n)\)\(k\) 小值。

可以假設值域 \(\mathcal O(n)\)(離散化)

考慮值域分塊,維護每個塊內數的個數,求 \(k\) 小值直接暴力跳塊即可。

8.集合,\(\mathcal O(\sqrt n)\) 插入,\(\mathcal O(1)\)\(k\) 小值。

考慮集合排個序然後序列分塊,插入一個數先跳塊找到它插入的地方。

插入時會導致後面的數後移一位,你發現只有 \(\sqrt n\) 個數改變了它所屬的塊(從這個塊的末尾到下個塊的開頭)。

那麼塊內用雙端佇列維護,查詢找到對應的塊查對應下標(雙端佇列支援隨機訪問)。

9.(CodeChef Chef and Churu)

長為 \(n\) 的序列,給定 \(m\) 個函式,每個函式為序列中第 \(l_i\) 到第 \(r_i\) 個數的和。

支援單點修改序列,求區間的函式值的和。

對函式序列分塊,塊記憶體函式值的和。散塊再對原序列分塊,\(\mathcal O(1)\) 查詢區間和,查詢就是 \(\mathcal O(\sqrt n)\) 的了。

修改考慮這個 \(x\) 對所有函式的貢獻,列舉每個函式塊,我們需要知道這個塊內有多少個函式包含 \(x\)

這個簡單,每個塊內預處理,用線段樹或者分塊給 \([l_i,r_i]\)\(1\) 就好了。

那麼就搞定了,時空複雜度 \(\mathcal O(n\sqrt n)\),也許可以離線做到線性空間。

10.(P3863 序列)

長度為 \(n\) 的序列,\(m\) 次操作,支援:區間加 \(k\),求 \(a_p\) 在過去多少時間內不小於 \(y\),時間隨著操作流逝。

對序列掃描線,從 \(1\) 掃到 \(n\),遇到一個區間修改的 \(l\) 就把它的時間到 \(m\) 全部加上 \(k\),遇到 \(r+1\) 就減。

查詢就是求時間 \([1,t]\) 內不小於 \(y\) 的個數,也就是例題 \(1\)

複雜度 \(\mathcal O(n\sqrt{m\log m})\)


簡單莫隊演算法

11.求區間每個數出現次數平方和。

直接莫隊,平方和展開來,維護出現次數的陣列。

12.(AHOI2013 作業)查詢區間中值在 \([a,b]\) 內的不同數個數。

莫隊,維護出現次數陣列和一個值為 \(0/1\) 的值域分塊。

如果出現次數變為 \(0\) 了或者從 \(0\) 變為非 \(0\) 了,就修改分塊中對應位置。

查詢直接求區間和,用例 \(3\) 的方法即可平衡複雜度。

13.(Ynoi2016 這是我自己的發明)給一棵樹,支援換根,求 \(x,y\) 子樹內各選一個點點權相等的點對數。

子樹變成 dfn 區間上的操作,換根就是可能把 dfn 反轉,可以不用管。

那麼就是求兩個區間內相等的點對數。考慮差分:

\[F(l_1,r_1,l_2,r_2)=F(1,r_1,1,r_2)-F(1,l_1-1,1,r_2)-F(1,r_1,1,l_2-1)+F(1,l_1-1,1,l_2-1) \]

這樣每一個詢問只有兩個維度,可以莫隊了。直接維護兩個桶即可。

14.(BZOJ3920 Yunna的禮物)求區間中出現次數 \(k_1\) 小的數中第 \(k_2\) 小的。值域大。

莫隊,出現次數 \(k_1\) 小顯然可以直接維護一個值域分塊,找到這個出現次數。

然後介紹一個科技:高維離散化。我們對於序列中出現過的所有數,如果它在序列中出現了 \(y\) 次,

我們對每個次數維護一個 vector,在次數 \(1\sim y\) 對應的 vector 裡全部塞一個 \(x\)

對於每個 vector 我們分別做離散化,這樣我們得到了每個數在每種出現次數的離散化值。

我們在莫隊時對每個出現次數維護一個值域分塊,這個出現次數每新增一個數就把它的離散化值加進值域分塊。

這樣查詢時就可以在對應值域分塊裡找 \(k\) 小值了。

當然也可以把所有離散化值串起來然後串起來維護值域分塊,似乎更好寫。

15.(BZOJ4241 歷史研究)求區間中數值乘上出現次數的最大值。

可以回滾莫隊,也可以不回滾莫隊。

答案只可能是一個數乘上它的出現次數(廢話)

把這些值 (\(x,2x,3x,\cdots,x\times y\)) 放在一起離散化,那麼就變成了插入,求最大值。

用值域分塊維護即可。

16.(Ynoi2015 盼君勿忘)每次給模數,求區間所有子序列,將相同值去重後的和的和。

對於一個數 \(x\),如果它在區間中出現了 \(k\) 次,區間長度為 \(len\),它對這次詢問的貢獻是 \(x2^{len-k}(2^k-1)\)

\(2^{len-k}\) 表示除了 \(x\) 的數隨便選,\(2^k-1\) 表示所有 \(x\) 至少要選 \(1\) 個。

貢獻可以拆成 \(x2^{len}-x2^{len-k}\)。第一部分每次做快速冪就好了。

考慮第二部分:對於第二部分,考慮一個結論:不同的出現次數最多隻有 \(\sqrt n\) 種。

那麼對於每種出現次數分別統計 \(x\) 的和就好了,查詢直接 \(\mathcal O(\sqrt n)\) 查詢。

為了做到 \(\mathcal O(1)\) 快速冪,要每次根號預處理光速冪。

17.(HNOI2016 大數)數字串,每次求區間中有多少子串是質數 \(p\) 的倍數。\(p\) 一開始給定。

子串可以差分一下,字尾相減再除掉一個 \(10\) 的冪。

\(p=2,5\) 特判,然後相當於統計有多少個字尾模 \(p\) 相等。直接莫隊即可。

18.求區間眾數。

莫隊,維護出現次數陣列。加入直接加並更新最大值就好了。

為了支援刪除,維護一下每個出現次數的數的數量就好了。

19.求區間有多少個子區間,滿足區間內所有數出現次數為偶數,值域 \(26\)

把每個數 \(x\) 變成 \(2^x\),題目條件等價於子區間異或和的 \(\text{popcount}=0/1\)

區間異或和變成字首異或和的異或,再莫隊,每次列舉異或和(只有 \(27\) 個)就好了。

20.求區間逆序對個數。

莫隊,變成求區間內有多少個數小於 \(x\)

由於每次轉移都要查詢,不能直接值域分塊。

考慮差分,變成 \([1,r]\) 內小於 \(x\) 的數個數減去 \([1,l)\) 內小於 \(x\) 的數個數。

你可能會考慮主席樹,但是這裡用主席樹複雜度會壞。那麼用一個叫可持久化分塊的東西就好了。

upd:忘記寫二次離線的 sol 了

這題還有一種做法。仍然是莫隊,假設向右轉移,每次轉移即要詢問某個區間 \([l,r]\) 大於 \(a_r\) 的數的個數。

這個資訊可以差分,差分成 \([1,r]\) 中大於 \(a_r\) 的個數減去 \([1,l-1]\) 中大於 \(a_r\) 的個數。

前者平凡,考慮後者。我們可以掃描線掃 \(l-1\),把所有的 \(r\) 塞到 \(l-1\) 裡,用值域分塊維護,\(\mathcal O(n)\) 次修改 \(\mathcal O(n\sqrt n)\) 次查詢。

現在空間是帶根號的。我們可以最佳化:你注意我們只處理右端點的移動,每段移動中 \(l\) 不變 \(r\) 連續。

那麼沒必要把 \(r\) 一個一個存進去,存下移動的區間即可。

當然你要用同樣的方法處理向左移動的部分。複雜度 \(\mathcal O(n\sqrt n)\)。這就是二次離線莫隊。

21.求區間內 \(|a_i-a_j|\) 的最小值。

有 polylog 做法(剛考完),這裡只說根號做法。

考慮莫隊,那麼就是要插入刪除一個數,求前驅後繼。

這個如果用平衡樹之類的複雜度會帶 log,並不是我們想要的。

考慮回滾莫隊,變成只刪除的,用值域連結串列維護,值域分塊維護答案。

用根號查詢最小值,\(\mathcal O(1)\) 修改的值域分塊即可。

本質上就是把插入改成了撤銷刪除,這個是可以 \(\mathcal O(1)\) 的,而前者不可以。

22.求區間中值相同的位置差最大值。

回滾莫隊,變成只加入。那麼維護每個值最左端和最右端的位置即可。

撤銷要記錄之前的位置,一直跳過去。

23.(BZOJ4358 permu)排列,求區間中最長的值域區間,滿足值域區間內數都在區間內出現過。

莫隊,對於每個值域連續段,在左右端點分別記錄它的另一個端點。

這個難以刪除,那回滾成只加入的,插入時就是可能將兩段合併成一段。直接維護答案就可以了。

24.(Cnoi2019 數字遊戲)排列,求區間 \([l,r]\) 有多少子區間的值都在 \([x,y]\) 內。

對值域區間莫隊,這樣相當於單點修改 \(0/1\),求區間中有多少個子區間全是 \(1\)

維護每個極長 \(1\) 段,這段對答案的貢獻就是 \(\frac{len(len+1)}2\)

如何維護極長 \(1\) 段?可以用上一題的方法,也可以搞個分塊維護。


靜態分塊

一般來說,能用靜態分塊做的莫隊都能做,但是靜態分塊可以解決強制線上的題。

25.求區間眾數,強制線上。

合併眾數的話有一個性質:將集合 \(B\) 中元素加入集合 \(A\),新集合的眾數要麼是 \(A\) 原先眾數,要麼是 \(B\) 中某個數。

如果 \(B\) 較小,我們可以列舉它裡面的數,檢驗每個數是否成為眾數。

回到這題,對序列分塊,預處理每兩塊之間的眾數。

查詢時,整塊已經預處理(即為集合 \(A\)),我們列舉散塊(集合 \(B\))內的數檢驗它是否成為眾數。

這裡要快速查詢區間內某個數出現次數,你可能又會考慮主席樹,可是複雜度會壞。老樣子使用可持久化分塊即可。

其實也可以維護每個數在每個塊中出現次數的字首和,查詢時差分即可。

但是!以上方法空間都是 \(\mathcal O(n\sqrt n)\) 的,不夠優秀。

想一想,我們現在要求的是散塊中每個數出現次數是否大於當前眾數出現次數。

那麼每個值開一個 vector 存它的出現下標,每個下標存它所在 vector 中的位置。

那麼如果當前眾數出現次數為 \(k\),若散塊中某個數 \(x\) 向後的 \(k\) 個相同數都在 \([l,r]\) 中,\(x\) 即為新的眾數。

(因為 \(x\) 出現了至少 \(k+1\) 次。)

你發現向後找第 \(k\) 個位置是 \(\mathcal O(1)\) 的,這個做法就是時間 \(\mathcal O(n\sqrt n)\),線性空間的!

26.求區間逆序對,強制線上。

貢獻拆成整塊對整塊,散塊對散塊,整塊對散塊。

整塊對整塊:預處理,設 \(f_{l,r}\) 表示 \(l\sim r\) 塊中逆序對數。

可以容斥得到:\(f_{l,r}=f_{l+1,r}+f_{l,r-1}-f_{l+1,r-1}+g_{l,r}\)

其中 \(g_{l,r}\) 表示塊 \(l\),塊 \(r\) 兩個塊之間逆序對個數(不含兩塊內部貢獻)。

\(g\) 可以歸併預處理,這部分複雜度為 \(\mathcal O(n\sqrt n)\)

散塊對整塊:直接預處理每塊某一段前(後)綴和前 \(i\) 個塊的逆序對個數,詢問時差分。

怎麼預處理呢?先求出前(後)綴和第 \(i\) 個塊的貢獻,這個可以歸併後 \(\mathcal O(1)\) 擴充,最後求一遍字首和即可。

散塊對散塊:兩端散塊內部的貢獻可以先預處理,兩個散塊之間的貢獻可以歸併。

最後時空複雜度都是 \(\mathcal O(n\sqrt n)\)

27.求區間內 \(|a_i-a_j|\) 的最小值,強制線上。

同樣拆成三個部分。

散塊對散塊:歸併處理。

散塊對整塊:預處理 \(f_{i,j}\) 表示 \(i\) 到它所在塊的末尾與第 \(i+1\) 個塊到第 \(j\) 個塊的答案,

\(g_{i,j}\) 表示 \(i\) 所在塊的開頭到 \(i\) 與第 \(j\) 個塊到第 \(i-1\) 個塊的答案。

如下圖,表示 \(f\) 預處理的東西:

如何預處理?和上一題類似,歸併後用雙指標,可以做到均攤 \(\mathcal O(1)\) 擴充。

剩下的整塊對整塊同樣是預處理,這裡可以直接利用 \(f,g\) 進行遞推。

複雜度依舊是 \(\mathcal O(n\sqrt n)\)


根號分治

28.無向圖有點權,支援單點加,求某點相鄰點的權值和。

對度數根號分治:詢問如果是度數小於根號的點直接暴力,否則我們預先存下所有度數大於根號的點的權值。

修改的時候,列舉所有度數大於根號的點,如果與這個點相鄰就修改以下這個大點記錄的權值和。

複雜度線性根號。

29.求全域性最小的 \(|i-j|\) 滿足 \(a_i=x,a_j=y\)

對顏色集合大小根號分治,一開始預處理每個大顏色和所有其他顏色的詢問答案。

如何預處理?考慮值域分塊,將這這種大顏色的所有點放進值域分塊裡,

查詢就是求 \(\le x\) 最小和 \(\ge x\) 最大的數,容易處理。

如果詢問的有一邊是大集合我們直接輸出預處理的東西,否則暴力歸併一下即可。

複雜度還是根號。

30.(IOI2009)樹,帶點權 \(a\)。求有多少對 \((u,v)\) 滿足 \(u\)\(v\) 的祖先且 \(a_u=x,a_v=y\)

還是對顏色集合大小根號分治。\(u\)\(v\) 的祖先等價於 \(dfn_v\in[dfn_u,dfn_u+siz_u-1]\)

對於每個大於根號的集合預處理它作為 \(a_u\) 以及 \(a_v\) 時到所有其他集合的答案。

預處理方法類似,現在變成了 \(\mathcal O(n)\) 次單點加,\(\mathcal O(n\sqrt n)\) 次區間查詢;

或者 \(\mathcal O(n\sqrt n)\) 次單點加,\(\mathcal O(n)\) 次區間查詢。用值域分塊即可解決。

小集合到小集合的詢問,可以考慮用類似的方法,把他們按 \(dfn\) 歸併排序,有 \(\mathcal O(\sqrt n)\) 次單點修改,\(\mathcal O(\sqrt n)\) 次區間查詢。隨便搞個字首和什麼的都可以。

31.(SHOI2006)集合,支援加數,求 \(\max\{x\bmod y|x\in S\}\)。值域小。

\(y\) 根號分治。對於小的 \(y\) 記錄答案,修改就直接一個個改。

對於大的 \(y\)\(y\) 的倍數個數比較小。我們列舉 \(ky\),要查詢 \([ky,(k+1)y)\) 中最小的 \(x\)

容易用值域分塊處理。修改 \(\mathcal O(\sqrt n)\),詢問 \(\mathcal O(1)\)

32.(Ynoi2015 此時此刻的光輝)求區間乘積的約數個數。

莫隊,維護一下每個質數的指數和,加 \(1\) 乘起來。

這樣會叫。每個數大概有 \(10\) 個不同質因子,每次轉移要搞 \(10\) 下,過不去。

考慮設定閾值 \(B\),對於每個小於 \(B\) 的質因子,做點字首和就可以 \(\mathcal O(1)\) 查詢指數和。

大於 \(B\) 的怎麼辦?還是莫隊,但是這次要處理的變少了,只有 \(log_BV-1\) 個。

沒寫,據說取 \(B=1000\) 能過。來點 PR 分解質因數,此時複雜度是 \(\mathcal O(nV^{\frac14}+n\frac{B}{\log B}+2n\sqrt n)\)

33.(Ynoi2018 未來日記)區間 \(x\) 變成 \(y\),區間 \(k\) 小值,值域與 \(n\) 同階。

值域小,考慮值域分塊。每次查詢我們列舉一個值域塊,試圖 \(\mathcal O(1)\) 得到:

  • 序列上 \([l,r]\) 內數在這個值域塊內的數的個數。

  • 以及 \([l,r]\) 內等於 \(x\) 的數的個數。

再序列分塊,序列上的散塊可以每次拉出來搞點桶。整塊,考慮維護

\(b_{i,j}\) 表示序列前 \(i\) 塊內在值域塊 \(j\) 內的個數,\(c_{i,j}\) 表示序列前 \(i\) 塊內 \(=j\) 的個數。每次查詢就拿兩個字首和相減。

考慮修改。對於某一塊,把 \(x\) 修改成 \(y\),如果原先 \(y\) 就存在,會導致塊內顏色數減少 \(1\)

每塊的顏色數只有可能散塊修改的時候加 \(1\)。那麼每次如果 \(y\) 存在就重構此塊,此塊總的重構次數不超過根號次。

重構複雜度顯然是根號。那麼這部分的複雜度就是 \(\mathcal O(n\sqrt n)\)

如果原先 \(y\) 不存在呢?簡單搞點對映關係就好了。

散塊修改直接重構就行。複雜度 \(\mathcal O(n\sqrt n)\)