《淺談亞 log 數據結構在 OI 中的應用》 - 學習筆記
向 $ 哥哥學習!
需要解決的問題:插入、刪除、前驅、后繼。不需要考慮相同元素。
2 壓位 trie
平衡樹和樹狀數組都沒什么優化空間,把它們丟進垃圾堆里。
考慮 trie 有沒有什么操作。此時想起來 trie 似乎並不只能是二叉。
但是多叉有一個大問題:詢問的時候,如果子樹中沒有合法值,那就要在其他兒子里找最大/最小值。也就是要在兒子集合里尋找前驅后繼。
也就是說,對於一個 \(w\) 叉樹,需要支持大小為 \(w\) 的集合的快速插入刪除前驅后繼。
比如 \(w=64\) ,那就可以通過二進制壓位來維護這個集合。前驅后繼都可以使用神奇的二進制操作 \(O(1)\) 實現。
於是復雜度 \(O(\log_w V)\) ,其中 \(V\) 是值域。
假裝值域不是太大,那么各種操作可以自底向上實現,帶來各種剪枝,並且常數也比遞歸小很多。
當然如果值域太大那就只能動態開點了吧。
空間復雜度其實是 \(O(V/w)\) ,因為大小不超過 \(w\) 的時候就只需要 \(O(1)\) 個整形了。但是如果動態開點就需要記錄兒子編號,就變回 \(O(V)\) 了。
拓展
插入、求 rank 。刪除可以當做是插到另外一棵樹里。
此時的大問題是不能快速求前 \(k\) 個子樹的元素個數。
設叉數是 \(B\) 。如果每次插入都重新算一遍前綴和,那就 \(O(B\log_B V)\) 了,非常垃圾。
修改和詢問均衡一下,變成一個節點插入 \(B\) 次之后再重構,那么插入的復雜度均攤下來就沒有問題。問題在於 \(B\) 個零散元素怎么 \(O(1)\) 查詢。
仍然壓位。用 1 的個數表示某棵子樹內的零散元素個數,不同子樹之間用 0 隔開。那么就只需要對一個二進制位查詢第 \(k\) 個 0 的位置。可以預處理。為了讓預處理時間不爆炸只能 \(B<\log n\) 。
(我覺得)更簡單的做法:直接令 \(B=\sqrt{64}\) ,把 \(B^2\) 個位置均勻分給 \(B\) 個子樹,然后插入的時候直接填在對應位置即可。
復雜度是單次操作均攤 \(O(\log_B V)\) ,如果取 \(B=O(\log n)\) 就是 \(O({\log V\over \log\log n})\) 。
3 vEB tree
壓位 trie 的瓶頸在於需要用二進制操作維護兒子集合,所以叉數不能太大。
但是發現對兒子集合的詢問也不外乎插入刪除、前驅后繼,這提示我們可以套娃。
對於一個大小為 \(2^k\) 的 vEB tree ,令 \(m=k/2\) ,那么就分出 \(2^{k-m}\) 個子樹。另外再開一個大小為 \(2^m\) 的 vEB tree 維護所有兒子。
設 \(high(x),low(x)\) 表示 \(x\) 的高位低位;設 \(A_y\) 表示 \(y\) 對應的子樹;設 \(B\) 表示維護兒子的樹。
樹中維護 \(min,max\) ,表示集合的最大最小值。
較為特殊的一點:我們不把 \(min\) 插入到子樹中,僅僅放在根節點的位置。這是為了保證復雜度。
插入
如果 \(x<min\) 那么 swap 一下。
如果對應兒子已經存在,那么 \(B\) 中就不需要修改,直接給兒子插入即可。
否則新建對應兒子,然后把這個兒子插入到 \(B\) 里。兒子里只有一個元素所以不需要遞歸。
刪除
如果 \(x=min\) 那么只需要更新新的 \(min\) 。在 \(B\) 和對應的 \(A\) 分別拿出 \(min\) 即可。然后變成在 \(B\) 和對應的 \(A\) 里刪除新的 \(min\) 。
如果 \(x\) 對應的 \(A\) 大小為 1 那么直接刪掉,然后在 \(B\) 里面刪除;否則 \(B\) 不需要修改,在 \(A\) 里面刪除。
前驅后繼
如果 \(x\) 對應的 \(A\) 里面有合法元素那么直接遞歸。否則在 \(B\) 里面找到前驅/后繼,然后直接把 min/max 拿走。
注意特判沒有插入的 \(min\) 。
簡單優化
當大小不超過 \(64\) 時直接用位運算干掉。極大地減小遞歸深度。
復雜度
任何操作都只會遞歸一邊,每次遞歸會讓大小取根號,所以復雜度 \(O(\log\log V)\) 。但是由於大部分情況 \(V\approx 2^{24}\) ,而在大小為 \(2^6\) 時就已經可以不用再遞歸了,所以遞歸次數極少。
空間復雜度不太會分析也懶得分析,論文說是 \(O({2^k\over \sqrt w})\) 。
4 例題
第一題的壓位 trie 用法非常 trivial ,重點看第二題的樹上壓位 trie 合並。
【ZJOI 2019】語言
原做法是維護一些點的虛樹大小,要支持合並兩個子樹對應的虛樹。無腦做法就是線段樹合並。
我們考慮壓位 trie 能否支持合並。
因為現在是動態開點壓位 trie ,所以要維護所有子節點的編號。合並的時候需要把其中一棵樹的兒子編號拉到另外一棵樹去,但是暴力拉就給復雜度乘了 \(w\) ,不太行。
一個兒子至少對應一個節點,所以用啟發式合並即可。
每拉一個子樹之后還要記得更新相鄰元素的距離和。
這時候又發現空間是 \(O(nw\log_w n)\) 有點爆炸。
重鏈剖分,每次直接把重兒子的 trie 拉過來。這樣任何時刻只有 \(O(\log n)\) 個壓位 trie ,每個 trie 只有 \(n/w\) 個點,空間復雜度變成 \(O(n\log n)\) 。
常數較小?不是很懂。
