1. 程式人生 > 其它 >Dsu on tree 和 點分治

Dsu on tree 和 點分治

寫的有點亂,意識流 blog /yun

感謝 @Mr_Spade 對我的幫助。

從 Dsu on tree 到點分治

Dsu on Tree 的主要思想是輕重鏈剖分,使每個點作為根時,都繼承重兒子的資訊並快速加入根的影響,並對每個輕兒子暴力向重兒子合併。由於每個點被合併 \(O(\log n)\) 次,複雜度是正確的。

這種問題也可以用點分治實現。對於無根樹上的問題,對點分得到的若干部分分別遍歷,然後用和上面同樣的資料結構維護查詢和插入操作。

而對於有根樹中“對每個子樹求解”的問題則相對複雜。考慮有根點分治,記分治中心為 \(x\)\(x\) 下方的子樹為 \(x_1, x_2,\cdots, x_k\)

\(x\) 上方的祖先為 \(y_1,y_2,\cdots, y_p\)​,整個連通塊的根為 \(r\)(不包含在 \(y\)​ 中)。考慮先計運算元樹 \(x\) 下方的答案,遞迴 \(x_{1\sim k}\)。合併和上面是一樣的。然後對於每個 \(y_i\)\(r\) 都分別遞迴,然後將其依次併入 \(x\)​ 子樹的資訊中。途中可以直接完成 \(x, y_{1\sim p}\) 答案的計算——仔細觀察點分治的遞迴方式可以發現除了一些與 \(r\)​ 之間相連的部分不被當前連通塊包含,其他都是完整的,可以準確得到答案。那麼對於 \(r\) 我們保留合併得到的資訊,接應之後可能的合併。

可以看到基本上 Dsu on tree 可以做的點分治也可以做,但實際上本質是差不多的。考慮你想要把 \(n\)​ 個一次多項式乘起來,你可以套用哈夫曼樹的方法,維護一個優先佇列,每次取兩個小的相乘然後放回堆中;或者使用分治,先將 \(f_l\times f_{l+1}\times \cdots \times f_{mid}\)​ 以及 \(f_{mid+1}\times f_{mid+2}\times \cdots \times f_r\)​ 算好,再計算 \([l,r]\)​​ 全部相乘的結果。這分別對應著 Dsu on tree 和點分治,兩者都是將 \(n\)​​ 個點合併,只是保證複雜度的方式不同。

一般而言做有根這種題都優選 Dsu on tree,因為效果幾乎一樣並且常數和程式碼都比較優秀。上面這段文字只是說明了:

Dsu on tree 能解決的問題為點分治的真子集

聽起來是暴論但實際上幾乎正確。考慮 Dsu on tree 的操作條件是:可以快速對重兒子加上根的影響;並且支援將輕兒子的內容高效動態插入。

然而一旦滿足這些條件,點分治也可以操作。但你發現在這方面的問題點分治的做法都比較繁瑣。這是因為點分治的擅長之處完全不在動態插入類問題。

點分治與靜態計算

考慮這樣一個問題:

給定兩棵樹 \(S,T\),帶邊權且 不一定非負。求 \(\max\limits_{u,v}\ \textit{dist}_S(u,v)+\textit{dist}_T(u,v)\)。​

由於邊權非負,合併點集並由原來兩對最遠點得到新的最遠點的結論已不適用了。下面需要用到一個合併優化的技巧:

合併優化:對於當前分治中心連線的若干部分 \(x_1, x_2, \cdots, x_k\)​​​,每次選取大小最小的合併並放回作為一個新的部分。這樣可以證明效率能達到邊分治一樣的複雜度,具體證法可見 zzy 論文(?)。這個 trick 的功效在於,可以想邊分治一樣只用考慮兩個不同的部分而不是多個,可以為解題掃清一些障礙。

對於兩個在樹 \(S\) 上的點集 \(V_1,V_2\),我們建出 \(V_1\cup V_2\)\(T\)​ 上的虛樹。首先求出 \(S\) 中每個點到分治中心的距離作為在 \(T\) 虛樹 \(T'\) 的點權。那麼只要得到 \(T'\)​ 上距離加上兩邊點權的最大值即可。具體實現類似於樹型 dp。可以發現這實際上實現了一個靜態問題的求解過程:建虛樹然後 dp。

那麼如果強行轉化為動態又將如何?設我們維護出 \(V\subseteq S\),然後一個新的點 \(x\)​ 需要被計算貢獻。那麼唯一的辦法是求出 \(T\)\(V\) 中的點的點權加上與 \(x\) 距離的最大值,這本身就是極其複雜的問題。因此 Dsu on tree 直接怎麼做是不太行的。

可以發現,點分治擅長統計可以靜態計算的資訊。動態維護往往會比前者更加困難,或者是被轉化為一個“更強的問題”。那也不奇怪為什麼 Dsu on tree 的適用範圍更小了。

快速新增根的影響?

這是經常忽略的一個要點。考慮這樣一個題:

\(n\) 個點的樹,每個點 \(i\) 有一次多項式 \(x-a_i\)。求每個點到根結點 \(1\) 路徑上多項式的乘積,輸出它們求和後的結果。

這題有一個基於長鏈剖分鏈分治的做法,這裡略去。

暴力做法是對子結點求和,並乘上自己,複雜度平方。考慮有根點分治(一下記號和上文一致):對於分治中心 \(x\)​​,我們先遞迴計算 \(x_1, x_2, \cdots, x_k\)​,對它們的多項式求和記作 \(g\),這裡複雜度不超過 \(x\) 子樹大小。然後遞迴 \(x\) 父親方向的連通塊,那麼這時除了子樹 \(x\) 都完成對 \(r\)​​ 貢獻的統計了。

唯一的問題是 \(x\to r\) 的多項式 \(f\) 怎麼算。比較無腦的有分治 FFT,這樣總複雜度 \(O(n\log ^3 n)\),可以利用之前遞迴子問題結果的技巧將其去掉一個 \(\log\)​,這個暫且不談。

嘗試使用 Dsu on tree?對於重兒子求出答案,乘上根直接作為根的答案,然後對所有輕兒子合併?不幸的是“乘上根”這個操作複雜度就和重子樹相關了——多項式乘法沒法像資料結構那樣快速新增影響。實際上稍微冷靜一下發現這玩意就是暴力。

更多的時候,這一點可能表現為”難以全域性打 tag“之類的情況。

本質原因……

具體的,dsu on tree 的短板歸納為兩點:

  • 在靜態計算簡單但維護動態新增困難的時候,無法快速合併輕子樹;
  • 無法高效加入根的影響;

可以發現上面兩點說明了一件事:dsu on tree 高度依賴動態新增的高效方法。

為什麼會這樣?注意到點分治之所以可以隨意遍歷整個連通塊,是因為重心分解的分治結構做了保障。而 Dsu on tree 卻必須靠規避重子樹來保證複雜度。

例子

給定一顆樹,帶邊權,求路徑上邊權平均值 \(\in [L,R]\) 的路徑數。

首先記 \((c,s)\) 為一條邊數為 \(c\) 總權值為 \(s\) 的路徑。那麼對於兩條路徑 \((c_1,s_1),(c_2,s_2)\),由合法條件可以推出兩個不等式:

\[s_1-c_1R\le -(s_2-c_2R) \\ s_1-c_1L\ge -(s_2-c_2L) \\ \]

這個式子一邊都只與一條路徑有關,抽象成兩維座標就是數點問題了。考慮點分治做法,如果是採用合併優化並靜態統計的方法其實很容易,只是簡單的掃描線和資料結構。這樣總複雜度為 \(O(n\log^2 n)\)

而另一種動態新增的思路就比較困難——天然的強制線上使得問題棘手了很多,我們似乎避不開高階資料結構,甚至可能還有更劣的複雜度。

本文來自部落格園,作者:-Wallace-,轉載請註明原文連結:https://www.cnblogs.com/-Wallace-/p/dsu-on-tree-and-vdac.html