數據結構專題


兜兜轉轉,回到初中最喜歡的數據結構知識點,卻發現自己已然成為一個門都沒入的菜逼,甚至連抄板子都不會了。

去年(今年?) CTT 的時候就因為毫無數據結構技巧被搞掉 ~40pts ,做 ioi 的時候又反復被數據結構暴打,打模擬賽的時候又被數據結構暴打……

這大概就是只做 CF 和 AT 的后果吧。

因為自己做可能會無從下手,所以緊跟 cmd 的步伐做題。

以下基本上默認 \(n,Q,??\) 都是同階的。以下基本都沒寫代碼。

莫隊二次離線

自然是從區間逆序對講起。

左右端點是對稱的,加入刪除也是對稱的,所以只考慮在左邊加元素的情況。

假設現在的區間右端點是 \(R\) ,左端點從 \(r+1\) 移動到 \(l\) 。那么就會帶來一組詢問 \(f(R,l,r)\) ,表示對於每個 \(i\) 求出 \((i,R]\) 中有多少個 \(<a_i\) 的數。

先預處理出 \(pre_i\) 表示 \([1,i]\) 中有多少個 \(<a_i\) 的數,然后 \(f(R,l,r)\) 就轉化為對於每個 \(i\) 求出 \([1,R]\) 中有多少個 \(<a_i\) 的數。

\(i\) 的總個數是 \(O(n\sqrt n)\) ,但是 \(R\) 只有 \(O(n)\) 種,所以可以對 \(R\) 掃描線,用 \(O(\sqrt n)\) 修改 \(O(1)\) 查詢的分塊維護即可。

時間復雜度 \(O(n\sqrt n)\) ,空間復雜度 \(O(n)\)

P7601 [THUPC2021] 區間本質不同逆序對

同樣用莫隊二次離線的技巧來處理。

\(pre_i,nxt_i\) 表示 \(a_i\) 上一個和下一個出現位置。

還是考慮移動左端點。加入一個 \(a_l\) 時,考慮會新增哪些本質不同的逆序對。

如果 \((a_l,a_i)\) 構成逆序對,那么 \(a_i\) 目前最靠右的出現位置必須夾在 \([l,nxt_l)\) 之間。但是“目前最靠右的出現位置”比較迷惑,經過腦洞之后可以轉化為在 \([l,R]\) 中出現過,而在 \([nxt_l,R]\) 中沒出現過。

注意到 \(a_l=a_{nxt_l}\) ,所以轉化成詢問 \(f(l,R)\) 表示 \([l,R]\) 中有多少個不同的 \(<a_l\) 的數。

只能三維數點:

\[\begin{align*} &\sum_{i} [l\le i\le R][pre_i< l][a_i<a_l]\\ =&\sum_{i} [1\le i\le R][pre_i< l][a_i<a_l]-\sum_{i< l} [a_i<a_l] \end{align*} \]

\(R\) 掃描線,就需要一個 \(O(\sqrt n)\) 修改, \(O(1)\) 查詢的二維數點數據結構。

要不是這題我還真不敢相信這能做……

(嫖個 cmd 的圖)

我們需要在修改的時候處理好很多東西,才能 \(O(1)\) 詢問。

首先是紅色的大塊,需要維護它們的二維前綴和。因此大塊的個數不能超過 \(n^{0.5}\) ,所以它們的大小是 \(n^{0.75}\times n^{0.75}\) 。它們把地圖分成了 \(n^{0.25}\times n^{0.25}\) 的大坐標系。

然后是藍色的中塊,對於大坐標系中的同一行或同一列,需要維護它們的前綴和。因此同一行不能超過 \(n^{0.5}\) 個中塊,而一行有 \(n^{0.25}\) 個大塊,所以一個大塊中只能有 \(n^{0.25}\) 個中塊,所以大小是 \(n^{0.5}\times n^{0.75}\)

最后是綠色的小塊,對每個大塊中的小塊維護二維前綴和。容易發現大小最小只能取到 \(n^{0.5}\times n^{0.5}\)

於是就留下了寬度為 \(n^{0.5}\) 的黃色區域還沒處理。

注意到 \((l,a_l)\) 一共只有 \(n\) 種不同的坐標,並且兩個 \(l\) 的坐標的兩維都一定不同……嗎?兩個 \(a\) 是可能會撞的,但是在保持 \(pre,nxt\) 不變的情況下對 \(a\) 稍作調整即可。

所以一個 \((pre_i,a_i)\) 只會被 \(n^{0.5}\)\((l,a_l)\) 的黃色區域包含,暴力修改即可。

P5113 Sabbat of the witch

(口胡的一個做法,不過經過了 cmd 檢驗)

分塊?分塊!

直接對序列分塊,一次操作被拆成 \(O(\sqrt n)\) 個整塊操作和 \(O(1)\) 個零散塊操作。下面對每個塊分別分析,不過實際操作的時候要同時進行。

考慮這個塊到目前為止的時間線,會有若干個整塊賦值,中間夾着一些零散塊賦值。

對於連續的零散塊賦值(稱為一段),維護每個位置從晚到早經歷的賦值操作,存在鏈表里。一段里至少有一個零散塊賦值,而總零散塊賦值次數是 \(O(n)\) ,所以這里的空間復雜度是 \(O(n\sqrt n)\)

加入一個賦值操作時,要么暴力給一些位置疊上一層 buff (零散塊賦值),要么隔開新的一段。

刪除一個賦值操作時,如果刪的是零散塊賦值那么就把這次操作標記一下,然后(對被刪除的賦值操作所在的段)掃一遍每個位置求出最近一次還沒被刪除的賦值操作。如果是整塊賦值,首先觀察它隔開的兩段是否都非空。如果其中一邊是空的那就無事發生,否則減少了段數,可以暴力 \(O(\sqrt n)\) 把兩段的鏈表合並。

進行操作的時候不難對每個段維護每個位置現在是某個零散塊的值還是底下的整塊的值,也不難維護整個塊的和。零散塊查詢就直接無腦暴力。

時空復雜度均為 \(O(n\sqrt n)\)

P3604 美好的每一天

莫隊二次離線的基礎題目。

顯然可以轉化為區間中有多少個 \(i,j\) ,使得 \(|pre_i\oplus pre_j|\le 1\) 。無腦莫隊即可做到 \(O(n\sqrt n|\Sigma|)\)

使用莫隊二次離線,在加入一個點的時候 \(O(|\Sigma|)\) 修改,然后 \(O(1)\) 查詢,即可 \(O(n\sqrt n+n|\Sigma|)\) 。似乎因為 \(2^{|\Sigma|}\) 太大了還需要給后者帶一個離散化的 \(\log n\)

P7126 [Ynoi2008] rdCcot

分析一個 \(C\) 連通塊的性質。經過隨機游走,猜想一個連通塊可以抽出一個樹形結構,每個點只連向能走到的最淺的點。邊權可以減掉任意個 \(eps\) ,所以相同深度的點可以任意排序。

考慮這樣是否會使得兩個本來直接有邊相連的點不在同一連通塊。發現讓深度較深的那個點跳一步之后仍然與另外一個點距離相差不超過 \(C\) ,所以跳若干步之后必定走到同一個根。

所以只需要對根計數即可,也就是沒有出邊的點。

\(l_i,r_i\) 表示 \(i\) 往左往右第一個連出去的點,轉化為求 \(l,r\) 。點分治然后亂搞即可。

P6779 [Ynoi2009] rla1rmdq

所有點都只能往上走,而在出現祖先關系之前原樹上的所有點都只會被遍歷一次。

而當兩個點有祖先關系時,這兩個點任意時刻都只會有一個是有用的。

分塊,每個塊預處理出虛樹,則所有點都只會在塊內的虛樹上跳。對每個塊的虛樹預處理出 \(O(n\log n)-O(1)\) 的查詢 \(k\) 級祖先。

然后在虛樹上遍歷就很容易知道現在的哪些點是有用的,以及再跳幾步會又出現一對祖先關系。每個整塊維護塊內最小值。

對於零散塊的修改可以直接 \(O(\sqrt n)\) 暴力重構。

對於整塊,只需要打上一個 tag ,然后暴力讓所有當前有用的點往上走一步,求出新的最小值。因為對於每個塊,原樹上的點只會被走到一次,所以這里暴力走的復雜度是對的。如果此時出現了祖先關系就 \(O(\sqrt n)\) 重構。每多一個關系就會少一個點,所以這里重構的復雜度也是 \(O(n\sqrt n)\)

總復雜度 \(O(n\sqrt n)\)

不過實際上沒有必要建虛樹,只需要每次跳的時候都給當前點 mark 一下,如果跳到已經有 mark 的點就把自己刪掉即可。注意被刪掉的點可能通過零散塊操作會活過來。

P7124 [Ynoi2008] stcm

對於菊花圖,可以用類似線段樹分治的做法,操作次數 \(n\log n\)

先重鏈剖分,把輕兒子按順序加一遍再刪一遍,就把重鏈處理完了,可以把重鏈上的點都加進集合中。對每條重鏈都這樣做,復雜度是所有輕邊的 \(size\) 之和。有 \(f(n)=\max_\limits {1\le a<n} f(a)+f(n-a)+\min(a,n-a)\) ,歸納證明 \(f(n)={1\over 2}n\log n\)\(\log\) 是以 2 為底):

不妨假設 \(a\ge {1\over 2}n\) ,則 \(\min(a,n-a)=n-a\)

\[f'(a)={1\over 2} (\log a+1)\\ f'(n-a)=-{1\over 2} (\log (n-a)+1)\\ (n-a)'=-1 \]

\[g'(a)={1\over 2}\log{a\over n-a} -1 \]

發現 \(a\to n\) 時並不優秀,所以只能是 \(a={1\over 2}n\) 時取到最大值。歸納假設成立。

然后對所有輕兒子建哈夫曼樹,用和線段樹分治一模一樣的做法即可。

把所有哈夫曼樹套在一起的深度應該是 \(O(\log n)\) ,不過常數具體是什么不太清楚。

P5064 [Ynoi2014] 等這場戰爭結束之后

先把操作樹建出來。

由第 \(k\) 大想到整體二分,但是遞歸到子區間的時候操作樹的大小無法縮減,沒法做。

那么把二分改成值域分塊,塊大小為 \(B\) 。用可撤銷並查集求出每個詢問的答案所在的塊,復雜度 \(O({n^2\log n/B})\)

然后簡單的想法就是枚舉答案所在塊內的每一個點,判斷是否與自己連通。這樣的復雜度是 \(O(nB\log n)\) ,總復雜度 \(O(n\sqrt n\log n)\)

考慮優化第一部分:並查集的瓶頸在於跳父親而不在於合並,所以可以一次做 \(O(\log n)\) 個塊,並查集維護連通塊中 \(O(\log n)\) 個值。這樣就平衡了跳父親的復雜度和合並的復雜度。總復雜度 \(O(n^2/B+nB\log n)=O(n\sqrt{n\log n})\) ,但空間是 \(O(n\log n)\)

第二部分也可以繼續優化:對操作樹分塊,使得每個點所在的塊根離自己距離不超過 \(\sqrt n\) 。對每個塊根暴力預處理出走到這里的時候的連通性,然后每個詢問就是在塊根的基礎上加 \(\sqrt n\) 條邊,可以 BFS 得到新的連通性。這樣第二部分的復雜度就被優化到了 \(O(nB)\) ,總復雜度 \(O(n\sqrt n)\)

不過還是不懂樹分塊的做法怎么把第一部分的空間優化成 \(O(n)\)

P6105 [Ynoi2010] y-fast trie

先把每個數模 \(C\) ,那么相加之后最多減掉一個 \(C\)

\(C\) 的情況顯然就是直接取兩個最大值,判掉。

把值域分成 \([0,C/2),[C/2,C)\) 兩段。稱第一段的數為小數,第二段為大數,不會在取兩個小數,而如果取兩個大數也必然是取最小值,判掉。

然后就只關心小數大數之間匹配的情況了。不妨令小數指向匹配的大數。用線段樹維護每個小數匹配到最優的大數得到的結果。

用線段樹維護每個存在的數 \(x\) 在另外一邊匹配最大的 \(< C-x\) 的數之后能得到的最大值。

插入一個大數時,會使得小數的一個區間從匹配自己的前驅變成匹配自己,也就是區間加。刪除的時候從自己變為前驅,就是區間減。插入刪除小數則直接查詢前驅即可。

然而沙雕出題人卡空間,而如果改成平衡樹大概就會被卡時間,所以不太行。

冷靜一下,每個大數會獲得一個區間的小數的青睞,我們維護這些區間,以及每個大數匹配到最優的小數的結果,存在優先隊列里。

插入刪除大數的時候就是合並或拆分區間,很容易做。插入刪除小數時則是對某個大數進行微調,也很容易做。

這時候好像和正解也沒什么太大區別了。

P5398 [Ynoi2018] GOSICK

雖然 \(5\times 10^5\) 但還是要勇敢地莫隊,因為別的都看起來沒什么救……

那么自然要二次離線莫隊,變成查詢 \([1,r]\) 中有多少個能和 \(a_i\) 有貢獻的。

\(r\) 掃描線,然后要給 \(a_r\) 的倍數和約數 +1 。

約數肯定是沒有問題的,但是 \(a_r<\sqrt n\) 的時候枚舉倍數的復雜度變得不可承受。

那么把 \(<\sqrt n\) 的數作為約數的貢獻單獨拉出來考慮。對於一個詢問,變成

\[\sum_{x<\sqrt n} (cnt_{x,r}-cnt_{x,l-1})\sum_{i=l}^r [x|a_i] \]

對每個 \(x\) 預處理前綴和即可。復雜度 \(O(n\sqrt n)\)

P5069 [Ynoi2015] 縱使日薄西山

lxl 竟然有 \(O(n\log n)\) 只出 \(10^5\) 的題?

注意到如果 \(a_i>a_{i-1},a_i\ge a_{i+1}\) ,那么這個關系永遠不會改變,所以 \(a_i\) 的減小只能是自己造成的。而 \(a_i\) 減到 0 的時候 \(a_{i-1},a_{i+1}\) 也都消失了。

發現這是一個和外界沒什么關系的孤島,可以斷開。於是可以推出答案就是每次把最靠左的最大值拿出來,把自己和相鄰兩個數刪掉,並讓答案加上自己。

仔細感受一下,發現可以把所有“山峰”(即上面那個 \(i\) )拿出來,然后直接求出山峰之間的數的貢獻。復雜度 \(O(n\log n)\)

P5608 [Ynoi2013] 文化課

不難想,不過一些神奇細節比較神奇。

在沒有區間賦值的情況下看起來線段樹就可以做了:維護區間的前綴積和后綴積,以及中間的和,很容易合並兩邊。

但是區間賦值需要我們維護每個區間的多項式。這也不難,因為多項式的指數之和是 \(O(len)\) ,所以只會有 \(O(\sqrt {len})\) 項,而 \(len\) 每次除 2 ,所以不會帶 \(\log\) 。稍微算一下會發現這樣的空間也是 \(T(n)=2T(n/2)+\sqrt{n}=O(n)\)

然而一個容易被忽視的細節是算點值的時候不能每一項都用快速冪,否則就會多帶 \(\log\) 。然后沙雕出題人又卡空間,所以沒法存下光速冪。解決方法是從 \(x^i\) 推到 \(x^j\) 的時候用 \(O(\log(j-i))\) 的時間算。用神奇數學方法可以分析出這樣做的 \(\log\) 就沒了。

P6019 [Ynoi2010] Brodal queue

被打傻了,只能復讀題解了。

根號分治太難處理小顏色的情況了,所以只能對序列分塊。

詢問

把答案拆成三部分:零散塊內部、零散塊 $\to $ 整塊、整塊內部。因為一些暫時還不知道的原因,如果一個塊內顏色相同,那么把它也算進零散塊的范圍中。下面的 \(cnt,f\) 同樣包括純色塊。

零散塊只需要掃一遍即可。

零散塊 $\to $ 整塊。需要維護 \(cnt_{i,j}\) 表示前 \(i\) 個塊中 \(j\) 的出現次數,然后直接算。

整塊內部。設詢問包含了 \([L,R]\) 的整塊。設 \(f_{i,j}=\sum_x cnt_{i,x}cnt_{j,x}\) ,那么答案大約是 \(f_{R,R}-2f_{L-1,R}+f_{L-1,L-1}\) ,再帶上一些常數。

目前看來沒有太大問題。

修改

需要維護 \(cnt,f\) 。不妨假設 \(f_{i,j}\)\(i\le j\)

區間覆蓋所常用的復雜度分析要求“刪除一個顏色連續段”,但一個顏色連續段覆蓋的塊可能有很多個,這會給 \(f\) 的維護帶來麻煩。所以把純色塊單獨拿出來考慮,這樣刪除顏色連續段就只會對 \(O(1)\) 個塊帶來影響了。

所以可以變成 \(O(n+m)\) 次修改,每次修改給第 \(k\) 個塊的顏色 \(x\) 出現次數增加 \(c\)

每次修改直接暴力重構 \(x\) 對應的 \(cnt\) 即可。

\(C\) 為修改之前的 \(cnt\) ,那么對 \(f_{i,j}\) 帶來的 \(\Delta\) 則是

\[\begin{align*} &(C_{i,x}+c\cdot [i\ge k])(C_{j,x}+c\cdot [j\ge k])-C_{i,x}C_{j,x}=[i\ge k]C_{j,x}c+[j\ge k]C_{i,x}c+[i\ge k] c^2 \end{align*} \]

\([i\ge k]\) 的部分枚舉 \(j\) ,用差分更新。\([j\ge k]\) 的部分則枚舉 \(i\) 。詢問的時候分別枚舉一遍把差分的貢獻拉上來。

沒什么顯然做法的時候就先考慮詢問會用到什么信息,然后憑着信仰去維護它。

P6774 [NOI2020] 時代的眼淚

一個口胡的不知道對不對的做法,暫且放在這里。

對值域分治,每次值域除以二的時候長度也會除以二,問題不大。

當詢問的值域區間完全包含當前段的時候用簡單的區間逆序對做法求解。我們只需要在分治點被詢問值域區間包含的時候算出上面對下面的貢獻。

對序列分塊,那么發現塊間的貢獻很好處理:上下的關系和左右的關系都確定了。

對於單獨一塊的貢獻,注意到同一塊中只有 \(O(n)\) 個本質不同的值域區間,全部用二維前綴和預處理出答案即可。

時間復雜度 \(O((n+q)\sqrt n)\) 。只要把該離線的離線了應該空間是 \(O(n)\)

LOJ#6507. 「雅禮集訓 2018 Day7」A

這沒做出來,非常生氣(

與和或的兩種思路:1. 一個數變化次數不多。 2. 相同的數很多。

因為同時出現了兩種運算,所以要從相同的位入手。

對於一個區間來說,一旦某次操作覆蓋了一整個區間,就會把某些位刷成完全一致。因此設置勢能為區間中不同的位數,然后只要某一位不是完全一致且需要修改就直接往下遞歸。一次操作只會使得 \(O(\log n)\) 個節點的一致性被破壞,所以問題不大。

P7476 苦澀

線段樹,每個節點維護一個堆,標記永久化。

一次刪除操作,如果在某個節點中遇到了需要刪除的數,就可以直接把一次刪除換成兩次加入。否則暴力遞歸即可。

可以發現,一次刪除至多轉換為 2 個加入,問題不大。

P6792 [SNOI2020] 區間和

這種題是不是先寫個暴力然后隨便改改就能過了?

肯定要 segment tree beats ,然后需要思考每個節點具體要維護什么信息。

一個想法是直接把整個關於最小值的分段函數直接維護出來。但是這個分段函數的長度是 \(O(cnt)\) 的,而且很容易發生變化,所以感覺沒什么前途。

(如果分塊之后每個塊分別暴力線段樹可以嗎?)

下一個想法是只維護現在這個分段函數的最近這一段,一旦超出了這一段就暴力往下遞歸更新。

我們發現答案的變化一定是因為某個前綴最小值或后綴最小值的長度發生了變化,而這兩者都是單調不降,所以一個點最多變化 \(O(len)\) 次。每次變化都可能需要遞歸到這個點來更新,所以是 \(O(\sum len\times dep)=O(n\log^2 n)\)

P4786 [BalkanOI2018]Election

我們要讓前綴和和后綴和全部非負。

對於每個 \(-1,-2,\cdots\) ,把取到這個值的最靠左的前綴和最靠右的后綴的位置拿出來。第 \(i\) 個前綴/后綴里至少要刪掉 \(i\)\(-1\)

考慮貪心:每個前綴的位置都在最右邊刪,然后把剩下不合法的后綴刪掉。

可以發現,把前綴看做右括號,后綴看做左括號,那么我們可以省下來的步數就是能匹配的括號對數。

離線,從左往右枚舉右端點,維護現在的后綴最小后綴和的位置,然后用樓房重建的討論維護左端點。遞歸到一個區間時,如果可用的右括號都在右邊,那么把左邊的左括號先匹配掉,然后遞歸右邊;否則只需要直接拿出右邊的信息,然后遞歸左邊。修改的 pushup 也是類似。

然而這樣是兩個 \(\log\) ,爆了。

考慮括號匹配還有什么操作。我們可以把右括號看做 \(-1\) ,左括號看做 \(1\) ,然后求最小前綴和,這樣也能得到匹配數。

發現這樣的定義和原數列有很強的聯系:考慮第 \(i\) 個右括號,它此處的原數列的前綴和恰好也是 \(-i\)

因此,我們只需要考慮第 \(i\) 個右括號左邊有多少個左括號。反過來就是右邊有多少個左括號。假設右邊有 \(j\) 個,那么括號序列的最小前綴和就和 \(-i-j\) 有關。

我們發現最大化 \(j\) 的時候恰好會最小化括號序列的最小前綴和。再進一步會發現, \(pos_i<pos_j\) 可以是任選的前后綴,但是只有是相鄰的最小前綴/后綴的時候才會最優。再取反,就是要最大化 \(sum(pos_i+1,pos_j-1)\) ,也就是最大子段和。

因此是 \(O(n\log n)\)


免責聲明!

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



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