長鏈剖分隨想

2021-07-27 05:56:22 字數 2583 閱讀 1943

之前寫了那麼長一篇blog…現在不如寫篇小短文…說一下另一種樹鏈剖分方法——長鏈剖分的事情。它可以比重鏈剖分更快地完成一些東西。

樹鏈剖分的原始版本重鏈剖分非常經典,這裡就不從頭介紹了。

原本的剖分方法是按照子樹大小剖分,與子樹點數最多的兒子連成鏈,所以叫做重鏈剖分…然後顯然就有乙個點到根的路徑上至多$o(\log n)$條輕邊這個性質(因為沿著輕邊走,每次子樹大小一定小於父親的一半)。有了這個性質就可以做各種路徑相關的查詢,暴力每次跳到重鏈開頭就好…

而在一些問題裡,有這麼一種奇妙的剖分方式可以取得更好的效果。那就是按照子樹深度剖分,與最深的兒子連成鏈。之前一直不知道這個應該怎麼叫,直到冬令營上聽到敦敦敦提到「長鏈剖分」這個詞,我才知道這個應該這麼叫…

先說一下第乙個:$o(n)$統計每個點子樹中可合併的以深度為下標的資訊。(如某深度的點數,點權和,最值)

暴力的做法是$o(n^2)$的,因為乙個點的多個子樹的資訊我們無法快速合併,合併複雜度可以達到$o(n)$。

但是我們對於重鏈剖分的方法可以想出乙個$o(n \log n)$的方法:自底向上統計,對於每個點,讓它繼承自己的重兒子的資訊,然後我們暴力遍歷其它子樹並統計資訊。這樣做的話,每個點會在它到根路徑上的$o(\log n)$條輕邊被計算的時候被遍歷,所以總複雜度是$o(n \log n)$的。

看起來這個已經很優了,而且我們也用上了輕邊數量這個性質,感覺沒有浪費什麼東西。再想想的話可以發現,其中遍歷其它子樹這一步有點浪費,因為我們統計的是可合併的以深度為下標的東西,我們其實只要迴圈一遍其他子樹已經統計出來的資訊就好了,這樣我們的代價就不是子樹大小而是深度了。但是這樣還是不夠的,複雜度沒有變化。不過我們注意到繼承重兒子這一點現在看起來就不是那麼完美了,因為我們只需要深度為下標的資訊,但是重鏈剖分是按照點數為標準的,所以我們可能繼承了乙個連出很多點但是深度很淺的掃把形重兒子,而其它輕兒子雖然點數不多,但是可能深度反而更深,所以可能選乙個輕兒子更優。所以我們改變策略,選擇繼承子樹最深的兒子的資訊,然後迴圈其它子樹的深度把資訊統計到這個點上。

這樣的複雜度是什麼呢?如果我們仍然按照上面的方法分析,我們發現我們的複雜度可能不太對,因為到根路徑上的輕邊數量不再***了。但是如果我們換一種方法考慮就可以得到乙個很好的複雜度。我們考慮每個子樹被作為輕兒子暴力統計的代價,代價是它的深度,而它的深度其實就是它為頂端的長鏈的長度。每個點都是乙個長鏈的開頭,而所有長鏈都是不相交的,也就是說所有子樹被作為輕兒子暴力統計的代價和是$o(n)$的。而被作為重兒子統計的代價,因為父親直接繼承了它的陣列,所以每個點是$o(1)$的。於是我們就可以用$o(n)$的複雜度統計一棵樹的每棵子樹內的可合併的以深度為下標的資訊。

先說一下別的做法…可以離線的話,我們顯然有乙個非常水的總時間$o(n+q)$的單次詢問$o(1)$的做法…dfs過程中直接找棧裡的某乙個即可。

不能離線的話也有一些傳統做法。比如重鏈剖分,還是根據$o(\log n)$條輕鏈的性質,如果$k$級組先就在當前重鏈上則直接找到,否則往上一條重鏈跳。複雜度$o(\log n)$

還有一種就是樹上倍增,預處理出每個點的$2^0,2^1,2^2,...,2^$級祖先,然後詢問的時候我們考慮$k$的二進位制表示,每次往為1的那些位的祖先上跳(可以看作用若干個$2^i$的和表示$k$)。這樣預處理的複雜度是$o(n \log n)$,詢問的複雜度是$o(\log n)$。總體上比重鏈剖分還差。

還有另外乙個相比起來複雜度比較糟糕的做法,就是記錄乙個點往上的前$\sqrt$級祖先,詢問的時候暴力往上跳,顯然預處理的複雜度是$o(n \sqrt)$,單次詢問$o(\sqrt)$。

如果我們想突破到單次詢問$o(1)$,我們依靠不了重鏈剖分這種經典做法。它對於它的思想來說已經相當優了,沒有什麼改進的空間。樹上倍增的特點是,可以一步跳很遠,但是如果要精準地跳到第$k$級,就一定要遍歷$k$的每乙個二進位制位。而我們想出第三種做法的基本思路是,如果我們對每個點都維護它的所有祖先,我們就能$o(1)$回答詢問。但是實際上$o(n^2)$的空間複雜度和預處理複雜度是不能承受的,於是我們折中地選擇根號。

注意到如果我們查詢的$k$大於很多個$\sqrt$的話,我們是一步一步跳$\sqrt$級跳上去的,效率很低。這時我們其實可以考慮用樹上倍增來優化。樹上倍增要精準跳到$k$級祖先複雜度比較高的問題可以用第三種做法的特點來彌補。如果$k$在$\sqrt$以內的話,我們可以$o(1)$跳過去。這樣做的結果是什麼呢?其實不太好,我們花了高昂的$o(n \sqrt)$的代價預處理,得到的結果僅僅是查詢時我們不再需要遍歷最低的幾個二進位制位。

但是這種思想還是可以繼續沿用的,接下來就是長鏈剖分出場的時候了。我們的目的是,讓樹上倍增進行盡量少的跳躍後就可以通過其他資訊找到$k$級祖先。我們首先可以像重鏈剖分一樣維護一下每條長鏈,然後我們往上跑,求出長鏈頂端往上長鏈長度這麼多級的祖先。這樣做的時空複雜度仍然是$o(n)$。這樣有什麼用?這樣做以後,我們的樹上倍增只用跳最大的一步。

顯然這個最大的一步的長度必定大於$k/2$,於是我們跳到的那個點往下的長鏈長度至少就有$k/2$,所以就算$k$級祖先不在這條長鏈上,也一定可以從我們跳到的那個點的已知資訊裡直接求到(因為剩下的步數已經小於$k/2$了,預處理的祖先長度$=$往下的長鏈長度$>k/2$)。於是我們只要再預處理出對於每個數,它最大的二進位制位是多少,我們就可以$o(1)$地求出任意乙個點的$k$級祖先了。(不過樹上倍增的預處理複雜度仍然是$o(n \log n)$,所以只有詢問個數很多的時候才能有明顯的效果)

長鏈剖分隨想

之前寫了那麼長一篇blog 現在不如寫篇小短文 說一下另一種樹鏈剖分方法 長鏈剖分的事情。它可以比重鏈剖分更快地完成一些東西。樹鏈剖分的原始版本重鏈剖分非常經典,這裡就不從頭介紹了。原本的剖分方法是按照子樹大小剖分,與子樹點數最多的兒子連成鏈,所以叫做重鏈剖分 然後顯然就有乙個點到根的路徑上至多 o...

關於長鏈剖分

看這樣乙個題 dsu on the tree 給你一棵樹,每個節點有一種顏色,問你每個子樹x的顏色數最多的那種顏色,如果顏色數相同,那麼種類數相加。考慮最暴力的暴力,對於每個點遍歷它的子樹,統計答案,然後再撤銷。但是這樣太傻了,每個點顯然可以繼承乙個兒子的資訊,我們選擇繼承它的重兒子的資訊,只 df...

長鏈剖分總結

長鏈剖分和輕重鏈剖分十分相似,都是將一棵樹節點的資訊分成多條鏈的資訊,但是前者是以深度剖分,後者則是以子樹大小來剖分。同時長鏈剖分還借鑑了 dsu on tree 的一些 trick 使得它能十分高效地合併子樹資訊。破天荒地寫了證明 所有鏈長度之和為節點數 證明 任意乙個點 k 級祖先所在長鏈的長度...