1. 程式人生 > 其它 >左偏樹學習筆記

左偏樹學習筆記

作為可並堆的一種,左偏樹算是又好寫功能全且複雜度比較優的了

首先介紹一下結構:
左偏是指定義的 \(dis\) 值左子樹比右子樹大
\(dis\) 指的是 \(min(son_0,son_1)+1\),葉節點為零
注意這裡的 \(dis\) 並不是深度,左偏樹的深度是沒有保證的,哪怕是一條鏈,只要滿足左偏的性質就是符合的
所以要查詢堆頂並不能暴力跳父親,而是要額外開並查集來儲存
能保證複雜度大概是因為只要有右子樹是空的那麼就能插入,那麼 \(dis\) 的定義方式就很科學

那麼接下來是變形操作
由於左偏樹有著二叉樹結構,那麼也是支援懶標記的,一定注意每次刪除堆頂前要先 \(spread\)
當然,理論上也可以可持久化,然而並沒有遇見過這樣的毒瘤題

以下是一些例題:


用於簡單維護集合元素

P3261 [JLOI2015]城池攻佔

堆中維護當前活著的騎士集合,由於騎士的死是一次性的且單調,那麼每個點暴力彈出即可
每個城有增益效果,懶標記維護即可


用於優化 dp 轉移

P7011 [CERC2013]Escape

\(f[u][i]\) 表示子樹 \(u\)\(i\) 的血量進入能得到的血量
那麼答案直接取 \(f[u][0]\) 即可,可以把 \(t\) 下面掛一個權值為 \(inf\) 的點
發現轉移很浪費時間
那麼不妨把 \(f\) 轉化成多個二元組 \((x,y)\),表示以 \(x\) 的血量進入最多增加的血量 \(y\)
過程變成了用當前血量對應從小到大取二元組直到不滿足
那麼每個點的二元組用堆來維護
那麼每個點首先要把子樹的堆合併起來
考慮加入 \(a_u\)

後的貢獻:
如果 \(a_u\) 大於零,那麼直接增加 \((0,a_u)\)
否則考慮進行合併:首先以 \(-a_u\) 作為初始血量來進入這個點,\(a_u\) 為初始的增加量
那麼從小到大來取子樹中的二元組來進行合併,直到不能合併為止
發現如果增加量為負一定不優,那麼這時候需要繼續拿,那麼相應的血量應該變為 \(x-y'\),指先消耗 \(y'\) 還能進行拿取操作,直到增加為正為止


CF671D Roads in Yusland

對於每個點的堆中存放所有覆蓋了子樹中所有邊以及父親邊的方案,每次取出合法的堆頂即是最小值,設為 \(f_x\)
對於一個節點合併子樹時,設 \(sum\)\(\sum f_{son}\)

,那麼應該對子樹 \(v\) 的所有方案加上 \(sum-f_v\)
同時對於每個堆頂排除不合法的方案


P4331 [BalticOI 2004]Sequence 數字序列

提供兩種方法:

首先假設為簡化版:\(b\) 的限制條件是小於等於
觀察發現,對於一段遞增的 \(a\),那麼 \(b\) 是對應的最優
對於一段遞減的 \(a\),那麼 \(b\)\(a\) 的中位數時最優
那麼考慮將原數列劃分為許多段遞減的段,每一段都取中位數
發現這樣取完 \(b\) 後可能會出現前一個 \(b\) 比後一個 \(b\) 大的情況
那麼把前一段和後一段的數合併後再求中位數即可
中位數可以用堆求,彈出直到剩下一半
由於有合併操作,用左偏樹維護

這裡提供一個更加精妙的做法:
考慮設計一個 \(dp\)\(f[i][j]\) 表示前 \(i\) 個最後一個為 \(j\) 的最小值
轉移有兩個:\(f[i][j]=f[i-1][j]+|a_i-j|\),\(f[i][j]=min(f[i][j],f[i][j-1])\)
如果對於每個 \(i\)\(dp\) 陣列看成一個關於 \(j\) 的函式影象
一定是一個下凸函式


P3642 [APIO2016]煙火表演