【科技】單 log 合併兩棵有交集 FHQ-Treap 的方法
維護可分裂 & 合併的可重集
考慮這樣一個問題:
維護 \(n\) 個 可重集 \(S_1, S_2, \cdots, S_n\),元素值域為 \([1, U]\),初始集合為空。支援一下操作:
- 將 \(S_p\) 中 \(\in [x, y]\) 的所有元素 割離 出來並插入集合 \(S_q\) 中,注意不用複製;
- 將 \(S_q\) 中所有元素 合併 到 \(S_p\) 中並將 \(S_q\) 清空;
- 計算 \(S_p\) 中 \(\in[x, y]\) 的元素個數;
- 求 \(S_p\) 中第 \(k\) 小的元素;
- 在 \(S_p\) 中插入 \(x\) 個 \(y\) 元素。
一共需要進行 \(Q\) 次操作。
一般的維護方法
動態開點權值線段樹
這種問題最主流的做法,它可以方便支援以上所有操作。
時空複雜度都是 \(O(n\log n)\)。
平衡樹
主要會用 FHQ-Treap 來寫,因為 split 和 merge 操作非常方便。
一般來說合並會使用啟發式合併,每次都是小的到大的合併,可以證明每個元素最多被合併 \(O(\log n)\) 次。於是時間複雜度為 \(O(n\log^2 n)\),但是平衡樹只需要 \(O(n)\) 的空間。
\(O(\log n)\) 的有交集平衡樹合併
這裡使用 FHQ-Treap 作為維護的資料結構。這個 \(O(\log n)\)
先放一個 評測結果(I/O 優化,O2)。這個排在時間最優解第一頁,去掉 I/O 優化的話空間用的也很少。因為這是一個優秀的 \(O(n\log n)\) 時間,\(O(n)\) 空間的優秀做法。
我們發現上面平衡樹的解法的瓶頸在於合併,然而這好像並沒有優化餘地。於是嘗試設計一個新的合併思路。在這裡感謝 Mr_Spade 給我介紹這個(並不算非常複雜的)科技。
考慮現在有兩棵 Treap,根為 \(x, y\)。我們先比較兩個結點的隨機值,欽定隨機值小的作為當前的根。這裡假定為 \(x\)。然後我們需要搞出 \(x\) 的左右子樹,分別為兩棵 Treap 並集(除去 \(x\)
對於 \(x\),顯然它的左右子樹(\(l_1, r_1\))就滿足上面那個要求;而對於 \(y\) 我們則可以直接按 \(x\) 的權值 split,得到 \(l_2, r_2\) 兩棵樹。
最後我們發現這是一個可以遞迴處理的問題,因為我們再對 \(l_1, l_2\)、\(r_1,r_2\) 分別做這樣的合併即可,兩次合併的結果就可以作為 \(x\) 的兩個子樹。
參考程式碼實現:
int join(int x, int y) {
if (!x || !y) return x | y; // 有一個空間的即可返回
if (t[x].pty > t[y].pty) swap(x, y); // 取隨機權值小的作為根
int L1 = t[x].ch[0], R1 = t[x].ch[1], L2 = 0, R2 = 0, equ = 0; // x 直接是左右子樹
split(y, t[x].val, L2, R2), split(L2, t[x].val - 1, L2, equ); // y 按權值 split
if (equ) t[x].cnt += t[equ].siz, t[x].siz += t[equ].siz; // 相等特殊處理
t[x].ch[0] = join(L1, L2), t[x].ch[1] = join(R1, R2); // 遞迴合併
return pushup(x), x; // 更新資訊
}
實際上這樣常數並不大,在空間優於線段樹的同時也不會比線段樹慢。注意 rand()
很慢,使用不建議每次都隨機一下。
後記
- 原文地址:https://www.cnblogs.com/-Wallace-/p/13865869.html
- 本文作者:@-Wallace-
- 轉載請附上出處。