維護可重集的合並
一般手法
- 權值線段樹: 均攤時間 \(O(n\log n)\),還可以支持分裂,但空間開銷巨大
- 平衡樹啟發式合並:空間 \(O(n)\) 但總時間高達兩個 \(\log\)。
非旋 Treap 合並
這個科技的時間復雜度為均攤 \(O(n\log n)\),但我不會證(帶分裂應該是假的)。在這里感謝 Mr_Spade 給我介紹這個(並不算非常復雜的)科技。
考慮現在有兩棵 Treap,根為 \(x, y\)。我們先比較兩個結點的隨機值,欽定隨機值小的作為當前的根。這里假定為 \(x\)。然后我們需要搞出 \(x\) 的左右子樹,分別為兩棵 Treap 並集(除去 \(x\))中 \(< 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; // 更新信息
}
實際上這樣常數並不大,在空間優於線段樹的同時也不會比線段樹慢。
效率測試
題目:Luogu P5494 【模板】線段樹分裂。由於此題帶分裂所以復雜度有點問題,但可以測試效率。
先放一個 評測結果(I/O 優化,O2)。這個排在時間最優解第一頁(2020-10-23,現在好像被一堆卡常巨怪擠出去了QAQ),去掉 I/O 優化的話空間用的也很少。